Compare commits

...

308 Commits
v0.5.4 ... main

Author SHA1 Message Date
Ben Busby
70df88b825
Add sourcehut to readme [skip ci] 2022-10-13 15:52:05 -06:00
Arya K
4d7254e74d
Add ~vern instances (clearnet, onion, i2p) [skip ci] (#858)
Co-authored-by: Ben Busby <contact@benbusby.com>
2022-10-03 11:30:26 -06:00
watchakorn-18k
4b2b0bf3c9
Include thai keyword in ads blacklist (#857) 2022-10-03 11:21:17 -06:00
watchakorn-18k
3943b2bc2c
Add thai translations (#856) 2022-10-03 11:20:40 -06:00
João
219fc58401
Fix handling of bangs (#851)
Changed the implementation to work if the bang is at anyplace in the query.

Added a check to not spend time looking for an operator if a "!" is not present
in the query.

No longer allowed to have the bang at the "!" char at the end, since this may
cause some conflicts like the issue cited before, where the ! is after a word
in the query, which is natural in most languages.
2022-09-30 14:39:13 -06:00
João
74503d542e
Encode config params in URL (#842)
Adds support for encoding (and optionally encrypting) user config values as
a single string that can be passed to any endpoint with the "preferences" url
param.

Co-authored-by: Ben Busby <contact@benbusby.com>
2022-09-22 14:14:56 -06:00
Biên
11275a7796
Add filter for ads in Vietnamese (#847) 2022-09-20 11:11:58 -06:00
João
c42640e21c
Use read_config_bool for vars in app init (#848) 2022-09-20 11:11:27 -06:00
João
1aad47f2af
Fix bad internal redirection for google links (#850) 2022-09-20 11:10:27 -06:00
Cx
6bb9c8448b
Add Kurdish translation (#837) 2022-09-07 13:00:43 -06:00
João
8f59b7c340
Allow different true values for config vars (#841)
* Fixes read_config_bool to allow several true params

* add upper case comment
2022-09-07 12:54:43 -06:00
Ben Busby
32ad39d0e1
Refactor session behavior, remove Flask-Session dep
Sessions are no longer validated using the "/session/..." route. This
created a lot of problems due to buggy/unexpected behavior coming from
the Flask-Session dependency, which is (more or less) no longer
maintained.

Sessions are also no longer strictly server-side-only. The majority of
information that was being stored in user sessions was aesthetic only,
aside from the session specific key used to encrypt URLs. This key is
still unique per user, but is not (or shouldn't be) in anyone's threat
model to keep absolutely 100% private from everyone. Especially paranoid
users of Whoogle can easily modify the code to use a randomly generated
encryption key that is reset on session invalidation (and set
invalidation time to a short enough period for their liking).

Ultimately, this should result in much more stable sessions per client.
There shouldn't be decryption issues with element URLs or queries
during result page navigation.
2022-08-29 13:36:40 -06:00
Ben Busby
77f617e984
Simplify Tor logging restriction
Can use the "Log min-max <location>" syntax instead of declaring a
separate value for each logging level.
2022-08-11 10:20:27 -06:00
Ben Busby
81a802e3fc
Only allow warn+err lvl logging for Tor service
The Tor service logs often confuse Whoogle users, since they're a lot
more verbose than anything Whoogle ever reports. The bulk of these logs
use "notice" level logging and are not helpful for the average user, so
everything between debug and notice is now directed to /dev/null.

Fixes #825
2022-08-11 10:17:41 -06:00
Ben Busby
a6a97aa9c7
Catch failure to restore adv search state
Shouldn't throw any errors if this fails to be restored from local
storage for any reason. It's purely a nice-to-have feature.
2022-08-03 17:59:08 -06:00
Ben Busby
cab1105169
Add an "advanced search" toggle in result tabs
Adds a new advanced search icon alongside the result tabs for switching
to a different country from the result page.

This will obviously get populated with other methods of filtering
results, but for now it's just the country selector.
2022-08-03 17:55:26 -06:00
Ben Busby
2eee0b87d5
Include full path when determining proxy host url
Session validation includes a method for determining the proxy host url,
but previously did not include the path for the initial request. This
caused a situation where users with a new session would not be able to
complete their first search, since the session validation follow-through
url did not include the actual path for their search query.

The method now includes a flag for only extracting the root url, which
is needed for creating full urls in the content filter.

Fixes #708
2022-08-02 10:57:59 -06:00
Ben Busby
aa198ed562
Include leading slash in path replacement for result config changes 2022-08-01 14:35:43 -06:00
Ben Busby
3f363b0175
Allow temp region selection from result view
This adds a new "temporary" config section of the results view, where a
user can now change the country that their results come from without
changing their default config settings.

Closes #322
2022-08-01 14:32:24 -06:00
Ben Busby
8e867a5ace
Remove pep8 workflow from ci
PEP-8 enforcements in the project are more of an annoyance than
anything. It doesn't really seem to add much value, and adds a lot of
friction to pull requests from developers who aren't familiar with the
style guide. Stylistic enforcements should just be done during PRs if
necessary (or a different style guide should be enforced).
2022-08-01 13:50:27 -06:00
Ben Busby
73dd5b80b5
Remove google prefs link for mismatched language queries
Queries performed in a different language than what is configured
contain a result div that prompts the user to configure their language
preferences using google's preferences page.

Since we want all language configuration to occur on Whoogle only, we
can safely remove this result div.

Fixes #444
Fixes #386
2022-08-01 13:46:06 -06:00
Ben Busby
839683b4e1
Allow result navigation w/ Tab and Shift+Tab
Closes #457
2022-08-01 13:01:12 -06:00
Ben Busby
78614877f2
Fix redirect for misspelled queries starting with /
Fixes #818
2022-08-01 12:12:55 -06:00
Ben Busby
bf92944b95
Support quora and imdb alts through Farside
Farside can now redirect quora links to querte instances and imdb links
to libremdb instances. This updates Whoogle to perform link replacements
for both services when site alts are configured.
2022-08-01 11:49:09 -06:00
Ben Busby
fde2c4db1e
Only select default country in config if none are selected 2022-08-01 11:33:38 -06:00
Ben Busby
96b9cce70c
Use WHOOGLE_TOR_SERVICE to enable/disable bg Tor service
Allows skipping the Tor startup script if WHOOGLE_TOR_SERVICE is set to
0. This is separate from WHOOGLE_CONFIG_TOR, which only allows
enabling/disabling user configuration of passing searches through
Tor.

Closes #631
2022-08-01 10:54:20 -06:00
Ben Busby
75a57ede07
Update instructions for fly.io [skip ci]
Closes #772
2022-07-18 10:20:37 -06:00
Ben Busby
a1adf60b30
PEP-8 fix 2022-07-13 10:31:55 -06:00
Ben Busby
5db72a9552
Use scheme in alt replacement if defined
For users running local instances of service alternatives such as
invidious, the alt replacement procedure broke if the scheme of the
original service (almost always https) didn't match the scheme of their
defined local service (likely http).

This adds a small check to see if the alt has a defined scheme, and if
so, removes the original scheme for that result.

Fixes #806
2022-07-13 10:25:51 -06:00
Kian-Meng Ang
2a8519be30
Fix typos [skip ci] (#813) 2022-07-13 10:08:44 -06:00
MadcowOG
03eeb3fad1
Strip newlines when parsing tor password (#801)
When parsing control.conf or password file, a newline character could cause
Authentication Errors.
2022-07-06 09:52:02 -06:00
Ben Busby
f688b88bd8
Preserve wikipedia language setting for wikiless redirects
Wikipedia -> Wikiless redirects always result in an english language
result, even if the Wikipedia result would've been in a non-english
language. This is due to Wikipedia using language specific subdomains
(i.e. de.wikipedia.org, en.wikipedia.org, etc) whereas Wikiless uses a
"lang" url param.

This has been fixed by inspecting the subdomain of the wikipedia link
and passing that value to Wikiless as the lang param if it's determined
to be a language specific value (currently just looking for a 2-char
subdomain).

See #805
2022-07-06 09:49:43 -06:00
J2D9
7164d066c3
Add instance to instances.txt [skip ci] (#808) 2022-07-06 09:30:24 -06:00
J2D9
4473f8ee1d
Add new instance [skip ci] (#807)
https://search.wef.lol
2022-07-06 09:29:56 -06:00
MadcowOG
6a24a785ee
Add Tor Documentation [skip ci] (#800) 2022-07-05 10:02:33 -06:00
Marcell Fülöp
ee2d3726af
Use X-Forwarded-Host as url_root when present (#799)
If Whoogle is accessed on a non-standard port _and_ proxied,
this port is lost to the application and `element['src']`s are
incorrectly formed (omitting port).

HTTP x-Forwarded-Host will contain this front port number in
a typical Nginx reverse proxy configuration.
2022-07-05 10:01:47 -06:00
Ben Busby
c1d9373d55
Include X-Forwarded-Proto in nginx sample [skip ci] 2022-06-27 12:54:00 -06:00
Ben Busby
a51fbb1b0e
Add sample Nginx config headers to readme [skip ci]
Closes #774
2022-06-27 12:47:51 -06:00
Ben Busby
cada4efe1d
Fix missing os import in routes 2022-06-27 12:36:45 -06:00
Joao A. Candido Ramos
0d2d5fff5d
Fixes handling of maps (#792)
* fixes map url, e.g. when no q parameter is given

* move maps_args from results to filter where it is used
2022-06-27 12:33:08 -06:00
jan Anja
90e160094d
Add more OpenSearch definitions (for images etc.) (#786) 2022-06-27 12:30:41 -06:00
CAB233
877785c3ca
Update Simplified Chinese translation (#794) 2022-06-24 10:52:27 -06:00
Joao A. Candido Ramos
d05ec08abf
Remove wildcard imports (#791) 2022-06-24 10:51:15 -06:00
Joao A. Candido Ramos
ddb8931e68
Fix image links not being opened in new tab (#790)
The majority of image links and links that are not handle by whoogle are not
opening in new tabs, this allow links that are not related to the application
to open in new tabs.
2022-06-24 10:50:14 -06:00
jan Anja
194b2eae74
Fix a crash with protected Tor control port (#785) 2022-06-22 10:23:58 -06:00
Ben Busby
966644baa0
Broaden session validation exception handling
Due to how instances installed with pip seem to have issues storing
unrelated files in the same directory as sessions, exception handling
during session validation has been expanded to blindly ignore all
exceptions. This portion of the code is more for maintainers of large
public instances with a bunch of users who block cookies anyways, so
having basic app functionality break down as a result shouldn't be the
default.
2022-06-16 15:46:18 -06:00
Ben Busby
ddc73a53fe
Flip country config check in template
Country config value should be checked against the valid value when
updating the home page config, not the other way around. This can lead
to a state where a user sets up an invalid country value, but can still
be matched against a correct value that is part of the invalid value
(i.e. "countryUK" is invalid, but would match against the correct value,
"UK")

Also minor refactor of where the session file size validation occurs.
2022-06-16 12:11:23 -06:00
Ben Busby
cb5557cc2e
Check file sizes in session dir before validation
For pip installed instances of Whoogle, there seems to be an issue where
files other than sessions are being stored in the same directory as the
sessions. From a brief investigation, this does not seem to be caused by
Whoogle, since Flask-Session objects are the only files stored in that
directory. It could be an issue with the library that is being used for
sessions, however.

Regardless, the app shouldn't crash when trying to validate and remove
invalid sessions, so a file size limit of 4KB was imposed during
validation. Any file found in the session directory that exceeds this
size limit will be ignored.

Fixes #777
Fixes #793
2022-06-16 11:50:13 -06:00
MadcowOG
c9ee9dcc8b
Tor password authentication (#746)
Added password authentication for tor control port.

For user configuration of access to tor control port. This file should be
heavily restricted in file system.

Co-authored-by: MadcowOG <madcowog@Arch-Main.localdomain>
2022-06-16 11:05:41 -06:00
Ben Busby
dc03022e27
Remove parked public instance
(whooglesearch.net is no longer an active instance)
2022-06-16 10:42:31 -06:00
Ben Busby
b03fe74f10
Ensure currency link parent exists before parsing
Fixes #782
2022-06-16 10:28:06 -06:00
Rajesh Rajendran
2600ad5a05
Add traefik instance configuration (#781)
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2022-06-13 10:40:55 -06:00
Ben Busby
d512745767
Bump version to 0.7.4 2022-06-13 10:37:28 -06:00
Ben Busby
d51be4f529
Fix missing box shadow for light theme results
Related to 65796fd1a5

Fixes an issue where box shadows were missing for light theme results.
2022-06-10 08:58:31 -06:00
Ben Busby
35ac5ac82f
Fix autocomplete behavior on result page
Similar issue to #629, but the result page uses a different script for
handling user input, so the fix was not applied appropriately.

It has been fixed for this view now.
2022-06-09 16:40:49 -06:00
Ben Busby
65796fd1a5
Counter latest result page style changes
Google updated their styling of the result page, which broke some
components of Whoogle's result page styling (namely the result div
backgrounds for dark mode).

The GClasses class has been updated to keep track of what class names
have been updated to, and roll them back to a value that works for
Whoogle. A function was added that loops through new class names and
replaces them with their older counterparts.
2022-06-09 16:35:02 -06:00
Ben Busby
a9e1f0d1bc
Refactor autocomplete/suggestion behavior (front-end only)
The previous implementation of autocomplete/suggestions on the front end
resulted in a situation where input and keydown events were constantly
being added to the search input bar. This was refactored to set up the
events only once and process suggestion navigation and appending
suggestions separately with different functions.

This has been tested on both an Android simulator, as well as an Android
tablet and seems to work as expected.

Fixes #370
Fixes #629
2022-06-07 11:01:14 -06:00
Ben Busby
f9ff781df3
Skip buildx on-success check for tagged builds
Tagged builds seem to erroneously fail the on-success check due to the
tests not having finished when the build begins. Since tagged builds are
only ever submitted once the tagged commit is confirmed to pass all
tests, this check will now be skipped.
2022-06-03 14:46:36 -06:00
Ben Busby
47df4da4b5
Bump version to 0.7.3 2022-06-03 14:33:53 -06:00
Ben Busby
f22e5ac171
Catch and ignore unpickling errors in pip installs
This seems to be caused by an odd behavior related to Flask sessions and
instances of Whoogle installed via pip. I didn't investigate it too
much, since catching and ignoring the result doesn't impact Whoogle
functionality at all (configuration and session values persist as
normal). Since this doesn't affect non-pip instances, I don't believe it
to be a fault within Whoogle itself.

Fixes #765
2022-06-03 14:29:57 -06:00
Ben Busby
ef98d85dc5
Ensure searches with a leading slash are treated as queries
A user reported a bug where searches with a leading slash (in this case:
"/e/OS apps" were interpreted as a Google specific link when clicking
the next page of results.

This was due to the behavior that Google's search results exhibit, where
internal links for pages like support.google.com are delivered with
params like "?q=/support" rather than a direct link. This fixes that
scenario by checking the "q" param value against the user's original
query to ensure they don't match before assuming that the result is
intended as a redirect.

Fixes #776
2022-06-03 14:03:57 -06:00
dependabot[bot]
57d9ae9351
Bump waitress from 2.1.1 to 2.1.2 (#773)
Bumps [waitress](https://github.com/Pylons/waitress) from 2.1.1 to 2.1.2.
- [Release notes](https://github.com/Pylons/waitress/releases)
- [Changelog](https://github.com/Pylons/waitress/blob/v2.1.2/CHANGES.txt)
- [Commits](https://github.com/Pylons/waitress/compare/v2.1.1...v2.1.2)

---
updated-dependencies:
- dependency-name: waitress
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-03 13:41:12 -06:00
PrivacyDevel
ce477ef997
Add new public instance [skip ci] (#764)
https://whoogle.privacydev.net
2022-06-03 13:40:48 -06:00
Joao A. Candido Ramos
fb6627a9cc
Remove duplicated handling of /url result links (#769)
It appears that result links beginning with '/url' were mistakenly
commited with an inefficient filtering process in its place. With the
way the code is structured, this less effective '/url' link filter took
precedence over the previous link filter, and also caused users with the
"open link in new tab" config enabled to no longer have access to that
feature.

Fixes #769
2022-05-25 11:37:34 -06:00
invis-z
9bcd9931f7
Replace leading slash for image links (#762)
The leading slash was previously removed without noticing it was part of a
string replacement in #734. This caused the href of "View Image" contain a
leading "/" which is wrong.
2022-05-25 11:18:17 -06:00
Ben Busby
fb600d6fc8
Improve G page distinction between footer and results
Pages in the Whoogle footer that by default route to Google pages were
previously being removed, but caused results that also routed to similar
pages to no longer be accessible. This was due to the removal of the
'/url' endpoint that Google uses for each result.

To fix this, the result link is now parsed so that the domain of the
result can be checked against the disallowed G page list. Since results
are delivered in a "/url?q=<domain>" format -- even for pages to
Google's own products -- and the footer links are formatted as
"<product>.google.com", footer links are removed and result links are
parsed correctly.

Fixes #747
2022-05-16 09:53:48 -06:00
Ben Busby
f5d599e7d2
Use lax for session SameSite value (not strict)
SESSION_COOKIE_SAMESITE must be set to 'lax' to allow the user's
previous session to persist when accessing the instance from an external
link. Setting this value to 'strict' causes Whoogle to revalidate a new
session, and fail, resulting in cookies being disabled.

This could be re-evaluated if Whoogle ever switches to client side
configuration instead.

Fixes #749
2022-05-10 17:40:58 -06:00
Nico
5d521be5d9
Update and add instances [skip ci] (#750)
Updates Garudalinux instance
Add dr460nf1r3.org instance
2022-05-10 16:08:11 -06:00
invis-z
0f6226ce51
Use window from Endpoint enum for anon view (#748)
Removes previously hardcoded "/window" from anon view links
2022-05-10 16:06:57 -06:00
Ben Busby
194ddc33f3
Replace public instance url
s.alefvanoon.xyz -> s.tokhmi.xyz

Fixes #743
2022-05-02 12:36:39 -06:00
hoschi1337
b809c88fa5
Fix german translation error (#742)
"Nachrichten" is the correct translation of "News"
2022-05-02 11:56:21 -06:00
xatier
7486697d41
Update zh-tw translation (#736) 2022-05-02 11:53:33 -06:00
invis-z
afc93b8a21
Add WHOOGLE_URL_PREFIX to app.json (#737) 2022-04-27 14:26:56 -06:00
invis-z
b4d9f1f5e5
Remove "/" before endpoints & tags (#734)
Removes the leading slash before imgres and other endpoints

Fix #733
2022-04-27 14:25:14 -06:00
Sandro
ad112e236e
Fix pipx dependencies (#738)
Missing cssutils
2022-04-27 13:01:06 -06:00
Ben Busby
8a0b872337
Bump version to 0.7.2 2022-04-26 16:49:30 -06:00
Ben Busby
2490089645
Remove unused /url endpoint
The `/url` endpoint was previously used as a way of mirroring the
`/url?q=<result domain>` formatting of locations in search results from
Google. Rather than have this unnecessary intermediary step, the result
path was extracted and used as the immediate path for each result item
instead.

This endpoint hasn't been in use for many versions and has been in need
of removal for quite some time.
2022-04-26 16:28:04 -06:00
Ben Busby
62d7491936
Only create ip card if main result div is found
The ip address card that is created for searches like "my ip" only needs
to be created/inserted if a main result div id is found.

Fixes #735
2022-04-26 15:18:29 -06:00
Ben Busby
abc30d7da3
Render error message w/o safe filter
The error message shown in the error template does not need to be
rendered using the safe filter, and furthermore opens up an XSS
vulnerability.
2022-04-26 09:28:05 -06:00
Warren Spits
d62ceb8423
Add proxyfix to honor X-Forwarded-Proto header (#731)
Fixes #730
2022-04-22 11:07:36 -06:00
Ben Busby
b2c524bc3e
Update test for bang searches without a query
The new behavior for bang searches is to redirect to the proper result
site, rather than redirecting to the Whoogle home page.
2022-04-20 14:58:39 -06:00
Ben Busby
a9b675cd24
Strip trailing slash on root url in filter
If a trailing slash is defined here, it causes the Whoogle instance to
redirect these element requests back to the home page, causing unwanted
behavior.
2022-04-20 14:55:19 -06:00
Ben Busby
5c8be4428b
Fall back to netloc for bang search if query is empty
Previously, empty bang searches would redirect to the Whoogle instance
home page. This now redirects to the specific site for the bang search
instead (i.e. "!yt" without a query redirects to "youtube.com", "!gh" to
"github.com", etc)

Fixes #719
2022-04-20 14:50:32 -06:00
Ben Busby
7688c1a233
Revert anon-view key change from #724
The "anon-view" translation key is the correct one to use for accessing
anonymous view within the search results. "config-anon-view" is only for
the configuration menu on the home page.
2022-04-20 14:11:29 -06:00
gdm85
6d362ca5c7
Add support for relative search results (#715)
* Relativization of search results

* Fix JavaScript error when opening images

* Replace single-letter logo and remove sign-in link

* Add `WHOOGLE_URL_PREFIX` env var to support relative path redirection

The `WHOOGLE_URL_PREFIX` var can now be set to fix internal app
redirects, such as the `/session` redirect performed on the first visit
to the Whoogle home page.

Co-authored-by: Ben Busby <contact@benbusby.com>
2022-04-18 15:27:45 -06:00
gdm85
94b4eb08a2
Return 401 when token is invalid (#714)
In some rare instances (a race condition perhaps?) a
`cryptography.fernet.InvalidToken` exception is thrown resulting in
a broken connection.

This change gracefully returns a 401 error instead.
2022-04-18 13:06:44 -06:00
Ilya Prokopenko
cded1e0272
Fix Russian translation (#726) 2022-04-18 12:46:02 -06:00
glitsj16
ca80bb0caa
Fix 'anon-view' KeyError (#724) 2022-04-18 12:45:20 -06:00
Ben Busby
9317d9217f
Support proxying results through Whoogle (aka "anonymous view") (#682)
* Expand `/window` endpoint to behave like a proxy

The `/window` endpoint was previously used as a type of proxy, but only
for removing Javascript from the result page. This expands the existing
functionality to allow users to proxy search result pages (with or without
Javascript) through their Whoogle instance.

* Implement filtering of remote content from css

* Condense NoJS feature into Anonymous View

Enabling NoJS now removes Javascript from the Anonymous View, rather
than creating a separate option.

* Exclude 'data:' urls from filter, add translations

The 'data:' url must be allowed in results to view certain elements on
the page, such as stars for review based results.

Add translations for the remaining languages.

* Add cssutils to requirements
2022-04-13 11:29:07 -06:00
gdm85
7d01620316
[Chrome] Mention requirements to add a search engine via OpenSearch [skip ci] (#716) 2022-04-07 13:55:03 -06:00
gdm85
739a5092cc
Do not offer opensearch.xml as attachment (#713)
Sending opensearch.xml as an attachment is unnecessary. 

This will also allow inspecting the XML file via browser without downloading
it.
2022-04-07 13:52:17 -06:00
Ben Busby
2fcfeacd44
Reduce search bar font size on mobile
24px->20px

Fixes #477
2022-04-06 14:44:17 -06:00
Ben Busby
0e5630f33a
Add ability to listen on unix sockets
Introduces a way to tell the app to listen on unix socket instead of
host:port.

Fixes #436
2022-04-06 14:11:52 -06:00
Ben Busby
470e2932ad
Set default css for new heroku deployments
During yesterday's stream, it was brought to my attention that Heroku
deployments with the default blank value set for custom CSS causes a
bizarre appearance (all black and white with missing UI elements).

Setting the custom css variable to the default seems to fix this
problem.
2022-03-31 13:26:40 -06:00
Ben Busby
797372ecaa
Ignore blank alts if site alt config is enabled
If the alt for a particular service is blank, the original source is
used instead.

Example:
1. Site alts enabled in config
2. User wants wikipedia links, not wikiless
3. WHOOGLE_ALT_WIKI set to ""
4. All available alt links redirected to farside, except wikipedia

Fixes #704
2022-03-30 14:46:33 -06:00
Ben Busby
788730cdc2
Update default bibliogram link in Dockerfile
Bibliogram uses a slightly different URL format than Instagram, and
requires a "u/" before the username when replacing Instagram links. This
was already implemented everywhere else except the Dockerfile.
2022-03-28 10:18:54 -06:00
green1052
0d6901aaa2
Add korean translation (#700) 2022-03-28 10:11:57 -06:00
138138138
5ecd4fe931
Add "nofollow noopener noreferrer" to all links (#698)
Old iOS 12 devices will pass the Referer HTTP header to the site user clicks.
Websites will know those traffic come from Whoogle search.
Adding "nofollow noopener noreferrer" solves the issue.
2022-03-28 10:11:09 -06:00
xatier
e575fad324
Fix incorrect translation (zh-TW & zh-CN) (#697)
Translation for `maps` and `videos` were swapped in this commit.

11099f7b1d (diff-fcd1e088df6519cbd45d012f89a0d2722b7414c94189ee41595a3a101b4c11ad)
2022-03-28 10:10:18 -06:00
domokosdcs0
4c91667b6f
Update whoogle.dcs0.hu in readme [skip ci] (#696)
whoogle.dcs0.hu no longer uses cloudflare
2022-03-28 10:07:51 -06:00
Ben Busby
3ec1f46fe8
Fix instance country in readme
https://whoogle.lunar.icu is actually hosted in Germany
2022-03-25 12:46:07 -06:00
Ben Busby
73ab9f29a5
Add https://whoogle.lunar.icu instance
Closes #694
2022-03-25 12:18:31 -06:00
Ben Busby
f5c47234de
Fix time filter background color
The time filter (past day/hour/month/etc) was using the result element
background color instead of the page background color, which wasn't
providing enough contrast with the default text color.
2022-03-25 12:14:57 -06:00
dependabot[bot]
605338e998
Bump waitress from 1.4.3 to 2.1.1 (#691)
Bumps [waitress](https://github.com/Pylons/waitress) from 1.4.3 to 2.1.1.
- [Release notes](https://github.com/Pylons/waitress/releases)
- [Changelog](https://github.com/Pylons/waitress/blob/master/CHANGES.txt)
- [Commits](https://github.com/Pylons/waitress/compare/v1.4.3...v2.1.1)

---
updated-dependencies:
- dependency-name: waitress
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-22 09:39:26 -06:00
Peter Bottenberg
9c4351a174
Increase /var/lib/tor tmpfs size to 12MB (#693)
After an uptime of 109 days, the usage of /var/lib/tor was still 10.9 MB. A
reply in issue #648 reported a higher usage, which was fixed by setting the
size a bit higher (12MB instead of 11MB).
2022-03-22 09:37:11 -06:00
Ben Busby
0048c2f9aa
Update remaining alternative frontends to use Farside
Wikipedia, imgur, and translate alternatives were all still using
hardcoded URLs when replaced with their respective alternative frontend.
This updates them to use farside instead.
2022-03-21 10:08:52 -06:00
Ben Busby
a58f70ca7e
Fix wikipedia->wikiless domain replacement
Was previously using wikipedia.com not wikipedia.org, causing wikiless
replacements to not occur.

Fixes #686
2022-03-21 10:01:21 -06:00
Ben Busby
2a0ad8796c
Switch to defusedxml for xml parsing
xml.etree.ElementTree.fromstring is considered insecure, see:
https://docs.python.org/3/library/xml.etree.elementtree.html

The defusedxml package contains several Python-only workarounds and
fixes for denial of service and other vulnerabilities in Python's XML
libraries: https://github.com/tiran/defusedxml

Fixes #670
2022-03-01 12:54:32 -07:00
Ben Busby
f7e3650728
Only remove G links in footer
Links that were directed at G domains were previously removed
universally, when really they only needed to be removed from the footer
to reduce possible confusion caused by mixed Whoogle and G links.

Fixes #656
2022-03-01 12:48:33 -07:00
Ben Busby
69f845a047
Add test for empty bang behavior
Also fix pep8 issue
2022-03-01 12:13:40 -07:00
Ben Busby
809520ec70
Fallback to home page for empty bang searches
Bang searches without an actual query (i.e. just searching "!gh") will
now redirect to the home page. I guess people do this for some reason
and don't like that it redirects to the correct bang result URL, but
without an actual search term.

Fixes #595
2022-03-01 12:06:59 -07:00
Ben Busby
b28fa86e33
Update ad filter
Recent changes to ads in search results caused Whoogle to display ads
for certain searches. In particular, ads recently started appearing
grouped into one div, as opposed to a singular ad per div. This was
accompanied by the div label "ads" (instead of just "ad"), which threw
off the existing ad filter. The ad keyword blacklist has been updated
accordingly, and has been enhanced to only check against alpha chars for
each label.

This only seems to have affected English language searches, and only for
very specific searches.
2022-02-25 23:02:58 -07:00
jan Anja
5069838e69
Configure setup() using setup.cfg (#667)
Dependencies are not read from requirements.txt intentionally, so only
direct dependencies without version pinning are included.

Setuptools documentation:
https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
2022-02-25 15:29:54 -07:00
Albony Cal
c3634a5135
Upgrade Python image in Dockerfile (#669)
Vulnerable Python image upgraded to python:3.11.0a5-alpine
2022-02-23 09:33:46 -07:00
Ben Busby
e72d8437f7
[Docker] Split config dir creation/set permissions
If the config dir already exists, setting the mode (`-m 777`) doesn't
actually work as it should. This change splits the command into two
separate commands for directory creation and enabling the directory to
be writable by all.

Fixes #658
2022-02-21 09:33:30 -07:00
Ben Busby
9984158ec1
Ensure valid str->float conv in currency calc
Currency amounts returned by google seem to randomly include unicode
chars ('\xa0' noted in #642) which broke the currency calculator
included in the project. This ensures that only strings that can be
converted to float are ever used in the conversion.

Fixes #642
2022-02-17 16:33:44 -07:00
Nitish Yadav
0e711beca7
Give Accept-Language div its own class (#659)
Fixes accidental assignment of "get-only" class to the
"Accept-Language" config option
2022-02-16 09:23:38 -07:00
Ben Busby
23402e27e1
Check for updates using 24 hour time delta
Rather than only checking for an available update on app init, the check
for updates now performs the check once every 24 hours on the first
request sent after that period.

This also now catches the requests.exceptions.ConnectionError that is
thrown if the app is initialized without an active internet connection.

Fixes #649
2022-02-14 12:19:02 -07:00
Ben Busby
d33e8241dc
Fix "my ip" search regression
Removes dependency on class names for creating the "my ip" info card in
the results list for searches pertaining to the user's public IP.

Adds test to prevent this from happening again.

Note to anyone reading this and looking to contribute: please avoid
using hardcoded class names at all costs. This approach of
creating/removing content just results in issues if/when Google decides
to introduce/remove class names from the result page.

Fixes #657
2022-02-14 11:40:11 -07:00
DUO Labs
b2c048af92
Fix collapse_sections for MINIMAL_MODE (#654) 2022-02-11 14:44:08 -07:00
DUO Labs
7c5094d37b
Check for soup body in remove_site_blocks (#651)
Fixes error with `remove_site_blocks` in the Images tab
2022-02-11 14:42:11 -07:00
Ben Busby
c6c9965335
Add new public instances to txt list [skip ci]
Missing from #650
2022-02-10 12:32:57 -07:00
Kainoa Kanter
4eafe0a5b0
Add gowogle.voring.me as public instance (#650)
Also removes fosshost instance from readme

From @benbusby:
I'm unable to get in touch with fosshost support about the whoogle
instance being unavailable, and am no longer interested in
maintaining the instance due to the lack of communication.
2022-02-10 12:30:33 -07:00
Ben Busby
070c327642
Add public instance to instance list [skip ci]
https://whoogle.esmailelbob.xyz

Amendment to #647
2022-02-08 11:22:07 -07:00
Esmail EL BoB
558a627a73
Add new instance to readme [skip ci] (#647)
https://whoogle.esmailelbob.xyz
2022-02-08 11:20:23 -07:00
DUO Labs
502067addc
Clean "Show more results" of all site blocks (#646) 2022-02-08 10:57:00 -07:00
Joao A. Candido Ramos
11099f7b1d
Use consistent header for all result types (#535)
Introduces a header for switching between result types (i.e. "All", "News",
etc) that is consistent between the different result types. Previously, image
results had a tab header that was formatted in a drastically different manner,
which was jarring when switching from a different result page to the Images
page.

Created a G class enum to reference class names returned in search
results. As noted in the class doc, this should only be used/updated as
a last resort, as class names change frequently. For some instances,
such as replacing the tbm tab, it's a lot easier to just replace by
header name than attempting to replace it based on how the element is
structured.

Also updated a few styles to revert the latest styling changes being
applied by Google.

Co-authored-by: jacr13 <ramos.joao@protonmail.com>
Co-authored-by: Ben Busby <contact@benbusby.com>
2022-02-07 10:47:25 -07:00
සයුරි | Sayuri
4aa94a5d75
Fix Sinhala translation for farside search (#594) 2022-02-04 16:16:56 -07:00
DUO Labs
500942cb99
Update minimal mode for new Google formatting (#637)
Google's latest formatting changes broke the modifications made when enabling
`WHOOGLE_MINIMAL`. This updates the result filtering to work with the new
changes.

Fixes #634
2022-02-02 12:57:05 -07:00
Ben Busby
b393e68d1d
Fix incorrect min-width for mobile screen sizes
min-width was previously set to 736px for all screen sizes, which forced
content off screen for smaller devices such as mobile phones. This
modifies the search stylesheet to only apply a min-width style to
devices > 800px wide.
2022-02-01 20:36:53 -07:00
Ben Busby
63301efb28
Push images to ghcr.io
Alternative container registries like ghcr.io are a good option for anyone
seeking to avoid things like docker hub's latest changes to rate limiting
2022-02-01 18:02:59 -07:00
Ben Busby
e3394e29dd
Amend body width formatting in search css
`min-width` is a better field to override than `max-width`, since some
users prefer full width results.
2022-02-01 17:24:12 -07:00
Ben Busby
9ba73331aa
Override new Google search result formatting
There have been some recent formatting changes made by Google for search
results that do not look good (especially for dark themes). This
mostly overrides those styles to resemble the original Whoogle
result formatting.
2022-02-01 17:15:48 -07:00
Ben Busby
33f56bb0cb
Read WHOOGLE_CONFIG_DISABLE var as bool in app init
Fixes #636, which pointed out that the var was being interpreted as
"active" (config hidden) regardless of the value that was set.
2022-02-01 15:29:22 -07:00
Ben Busby
fef280a0c9
Add note for fosshost instance [skip ci]
The fosshost team decommissioned the region that Whoogle was hosted in,
but hasn't provided an option to transfer the domain record to the new VM. Until
that is fixed, the instance is inaccessible.
2022-02-01 12:39:10 -07:00
Ben Busby
df6aa59fbf
Run buildx workflow on new tag
Fixes #630
2022-02-01 10:55:41 -07:00
Ben Busby
3918c60d87
Remove broken public instance [skip ci]
search.exonip.de now redirects to startpage

Fixes #635
2022-02-01 10:11:59 -07:00
Ben Busby
1af4566991
Bump version to 0.7.1 2022-01-26 10:41:41 -07:00
Ben Busby
4dd2c581ac
Add nightly container vuln scan
Introduces a new 'scan' workflow for scanning the main branch container for
vulnerabilities nightly. By default, this will fail for any 'medium' or higher
vulnerability. 

Fixes #613
2022-01-25 13:52:43 -07:00
Ben Busby
9cbd7bd9d3
Remove bash dependency
Depending on bash wasn't strictly necessary, as the two minimal scripts
in the repo were both nearly POSIX anyways.

Aside from simplifying the repo's dependencies a little bit, this also
helps reduce the overall Docker image size as an added bonus.
2022-01-25 13:07:21 -07:00
Ben Busby
2e3c647591
Use test image tag for docker-compose tests
Also adds the ability to overwrite the image in docker-compose.yml,
which allows the CI build to use the same image for all docker tests.
The default is still 'benbusby/whoogle-search' though.
2022-01-25 12:42:24 -07:00
Ben Busby
863cbb2b8d
Remove trailing whitespace 2022-01-25 12:31:19 -07:00
Ben Busby
72e5a227c8
Move bangs init to bg thread
Initializing the DDG bangs when running whoogle for the first time
creates an indeterminate amount of delay before the app becomes usable,
which makes usability tests (particularly w/ Docker) unreliable. This
moves the bang json init to a background thread and writes a temporary
empty dict to the bangs json file until the full bangs json can be used.
2022-01-25 12:28:06 -07:00
Ben Busby
6d178342ee
Refactor Docker CI workflows
Split previous docker test CI into one for PRs and one for triggering
the main buildx workflow that deploys new images to Docker Hub.

Note that this needs to be further refactored soon to use reusable
workflows. The main portion of docker/docker-compose tests is duplicated
between the new main + test workflows.
2022-01-25 11:42:29 -07:00
nakoo
0b70962e0c
Fix docker-compose.yml permission errors (#623) 2022-01-25 11:06:46 -07:00
ras07
ecb4277e69
Run container as non-root whoogle user (#617)
Creates a non-root user ("whoogle"), and runs the container as that user.
2022-01-21 13:51:51 -07:00
ras07
09a0039a38
Make /config directory writable by all (#616)
The `/config` directory needs to be writable by all in order to run the container
as a non-root user.
2022-01-21 12:16:51 -07:00
Nitish Yadav
fc50359752
Improve formatting of collapsible infobox (#612) 2022-01-18 13:47:35 -07:00
DUO Labs
257e3f33ef
Skip loading autocomplete.js if WHOOGLE_AUTOCOMPLETE=0 (#611)
Bypasses autocomplete.js if `WHOOGLE_AUTOCOMPLETE` is set to 0
2022-01-18 13:39:56 -07:00
Ben Busby
4dd01cdfda
Fix Dockerfile syntax errors 2022-01-14 10:05:24 -07:00
DUO Labs
74cb48086c
Introduce site alts for imgur and wikipedia (#609)
* Add `WHOOGLE_ALT_IMG` for a replacement for imgur.

* Add `WHOOGLE_ALT_WIKI` for Wikipedia
2022-01-14 09:59:03 -07:00
Ben Busby
ded787547a
Exclude opensearch route from session validation
Fixes #588
2022-01-11 10:50:35 -07:00
domokosdcs0
31f4c00aee
Add new instance [skip ci] (#604)
https://whoogle.dcs0.hu
2022-01-11 10:06:57 -07:00
Ben Busby
f4b65be876
Catch invalid XML in suggestion response
As reported in #593, the XML response body returned for search
suggestions can apparently contain invalid XML elements. This catches
the error and returns an empty suggestion list instead of erroring.

Fixes #593
2021-12-28 11:38:18 -07:00
Ben Busby
362b6a75c8
Include plaintext instance list in repo [skip ci]
Including a list of instances that are easily machine-readable allows
services such as Farside (https://github.com/benbusby/farside) to read
these and have an up to date list of valid instances.
2021-12-23 17:24:11 -07:00
Ben Busby
8c92b381a2
Remove default country param
The country URL param ('gl') is no longer set to 'US' by default, and is
omitted from the search entirely unless explicitly set by the user. This
change was made in an attempt to cut back on the number of captchas
experienced by certain users self-hosting who experienced a decreased
amount of captchas when this configuration setting was removed.

Fixes #558
2021-12-23 17:01:49 -07:00
Ben Busby
95be59eaab
Roll back crypto library version
This is a temporary reversion to 3.3.2 for the cryptography library.
There's an issue with buildx failing for the arm/v7 build, which is
directly related to cryptography versions > 3.3.2 (after the switch to
rust).

It might be acceptable to include the rust toolchain for armv7 builds,
but that adds a comical amount of time to the full cross platform build.
2021-12-21 17:03:49 -07:00
Ben Busby
a2d5a23c43
docker: Upgrade pip before installing requirements
Outdated pip versions require a rust compiler to install the
cryptography package. Ensuring that pip is up to date should eliminate
the recent buildx errors where a prebuilt cryptography wheel is not
available.
2021-12-21 14:27:18 -07:00
Ben Busby
d02a7d90b9
Use UTF-8 encoding when loading json files
Fixes #581
2021-12-21 14:11:55 -07:00
Ben Busby
6d9df65d02
Catch FileNotFound when clearing invalid sessions
The server now consumes the FNF error if an invalid session is found but
is deleted in an earlier thread.

Fixes #577
2021-12-21 14:03:24 -07:00
Ben Busby
b745460a87
Bump cryptography version 2021-12-21 14:02:13 -07:00
Albony Cal
fd802aac06
Update screenshots in readme [skip ci] (#583)
Add new screenshots to reflect recent layout and theme changes
2021-12-20 23:54:03 -07:00
Roy Zuo
dec6d80dda
Use alpine docker image (#573) 2021-12-19 11:59:06 -07:00
f6c0843183
Update systemd instructions [skip ci] (#571) 2021-12-19 11:52:15 -07:00
glitsj16
c637eb28dd
Add missing env vars to readme [skip ci] (#584) 2021-12-19 11:42:52 -07:00
Ben Busby
119437a07c
Fix test for blocking site from results
Previously the logic for testing site blocking was essentially "assert
blocked_site not part of result_site". This caused test failures, since
site blocking does not extend to subdomains for the blocked site. The
reversed logic makes more sense with what the test was trying to
accomplish.
2021-12-19 11:22:47 -07:00
Albony Cal
84b5987ac5
Remove lsof dependency in replit deploy (#569)
Use `killall -q python3` instead
2021-12-15 17:16:56 -07:00
Ben Busby
3d8da1db58
Bump version to 0.7.0 2021-12-08 17:57:22 -07:00
Ben Busby
634d179568
Use farside.link for frontend alternatives in results (#560)
* Integrate Farside into Whoogle

When instances are ratelimited (when a captcha is returned instead of
the user's search results) the user can now hop to a new instance via
Farside, a new backend service that redirects users to working instances
of a particular frontend. In this case, it presents a user with a
Farside link to a new Whoogle (or Searx) instance instead, so that the
user can resume their search.

For the generated Farside->Whoogle link, the generated link includes the
user's current Whoogle configuration settings as URL params, to ensure a
more seamless transition between instances. This doesn't translate to
the Farside->Searx link, but potentially could with some changes.

* Expand conversion of config<->url params

Config settings can now be translated to and from URL params using a
predetermined set of "safe" keys (i.e. config settings that easily
translate to URL params).

* Allow jumping instances via Farside when ratelimited

When instances are ratelimited (when a captcha is returned instead of
the user's search results) the user can now hop to a new instance via
Farside, a new backend service that redirects users to working instances
of a particular frontend. In this case, it presents a user with a
Farside link to a new Whoogle (or Searx) instance instead, so that the
user can resume their search.

For the generated Farside->Whoogle link, the generated link includes the
user's current Whoogle configuration settings as URL params, to ensure a
more seamless transition between instances. This doesn't translate to
the Farside->Searx link, but potentially could with some changes.

Closes #554

Closes #559
2021-12-08 17:27:33 -07:00
Vansh Comar
7bea6349a0
Add tools for currency conversion in search results (#536)
This implements a method for converting between various currencies. When a user
searches "<currency A> to <currency B>" (including when prefixed by a specific
amount), they are now presented with a table for quickly converting between the
two. This makes use of the currency ratio returned as the first "card" in
currency related searches, and the table is inserted into this same card.
2021-12-06 22:56:13 -07:00
Ben Busby
10a15e06e1
Fix incorrect request type for image searches
Previously had hardcoded POST requests for all requests that didn't use
the header template (which currently is only the image tab).

Also refactored how the Filter class works. It now requires a valid
Config model to be provided, which is then set up as a class var that
the filtering functions can use as needed, rather than setting specific
values from the config as individual values (which was confusing and
sloppy).

Fixes #561
2021-12-06 21:39:50 -07:00
Ming Di Leom
1867e7ad01
docs(instance): search.sethforprivacy.com (#562)
- https://blog.sethforprivacy.com/about/#privacy-preserving-front-ends-and-tools
2021-12-06 20:44:50 -07:00
Ben Busby
e16038bf28
Make country var value compatible with gl param 2021-11-30 20:18:40 -07:00
Ben Busby
b75ff0782d
pep8: fix CSP header line length 2021-11-29 15:58:19 -07:00
Ben Busby
3e20788857
Disable in-app CSP unless enabled via WHOOGLE_CSP
The default CSP is only helpful for some, and can break instances for
others. Since these aren't always necessary and are occasionally set by
the user's preferred reverse proxy, it is being disabled unless
explicitly enabled by setting `WHOOGLE_CSP`.

Fixes #493
2021-11-29 15:52:28 -07:00
Ben Busby
f73e4b9239
Fix height for homepage logo 2021-11-29 15:34:13 -07:00
Ben Busby
27051363ff
Adjust logo css for mobile devices
Fixes #557
2021-11-27 20:03:06 -07:00
alefvanoon
15391379be
Remove dead instances & add onion instance (#555) 2021-11-26 15:08:44 -07:00
Ben Busby
9c96f0fd57
Improve default response headers
Reponse headers now include the following:
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- Strict-Transport-Security: max-age=63072000
  - Only when HTTPS_ONLY is set

https://infosec.mozilla.org/guidelines/web_security#http-strict-transport-security
https://infosec.mozilla.org/guidelines/web_security#x-content-type-options
https://infosec.mozilla.org/guidelines/web_security#x-frame-options
2021-11-26 08:38:26 -07:00
Ben Busby
30d4337783
Add new public instance
https://whoogle.fossho.st is now an "official" public instance of
Whoogle, since it is the only instance maintained and validated by
the developer(s) of Whoogle (currently only me).

Closes #533
2021-11-26 07:54:58 -07:00
Ben Busby
73f631b1f9
Import logo stylesheet before applying custom css
This fixes #551, and allows custom css to be applied to the Whoogle
logo.
2021-11-24 12:38:56 -07:00
Ben Busby
3c06519130
Use 'gl' search param to set country
This switches the param used for the "country" config setting from "cr"
(which only filters results by the country the result is hosted in) to
"gl" (which overrides server/hosting location and produces results that
are more accurate for the user's current country).

Before this change, the country config setting was (imo) pretty useless.
Allowing a user to override an instance's hosting location with their
preferred country though is way more useful, especially for public
instances that are hosted in a different country than the user.

Closes #544
2021-11-23 13:48:54 -07:00
Ben Busby
1d3e7c0255
Pin config buttons to bottom of config menu
Previously the load/save/apply buttons in the config menu were hidden
below all available config options and required the user to scroll to
the bottom to save changes. This made for bad ux, since for new users,
it isn't immediately apparent that selecting a new dropdown value, for
instance, doesn't instantly save the new setting. The new layout should
make it more clear that hitting "Apply" is required to save config
changes.
2021-11-23 12:27:59 -07:00
Ben Busby
a8afd49f84
Move docker tests after api/unit testing
It makes more sense to structure the order of tests to go from api and
unit testing -> validate docker image works as expected -> build and
deploy docker image.
2021-11-23 10:58:31 -07:00
Ilya Prokopenko
79a4a17311
Add Russian translation (#552) 2021-11-23 10:36:52 -07:00
Ben Busby
baffb5fc81
Simplify docker tests
Only the healthcheck is really necessary for the workflow's purpose.
Running the full test suite is redundant.
2021-11-22 00:34:48 -07:00
Ben Busby
5a27d748d1
Create separate test workflow for docker
This expands on the current testing suite a bit by introducing a new
workflow for testing functionality within the docker container. It runs
the same test suite as the regular "test" workflow, but also performs a
health check after running the app for 10 seconds to ensure
functionality.

The buildx workflow now waits for the docker test script to finish
successfully, rather than the regular test workflow. This will hopefully
avoid situations where new images are pushed with issues that aren't
detected in regular testing of the app.
2021-11-22 00:26:25 -07:00
Ben Busby
6f5f3d8ca7
Fix incorrect redirect protocol used by Flask
Flask's `request.url` uses `http` as the protocol, which breaks
instances that enforce `https`, since the session redirect relies on
`request.url` for the follow-through URL.

This introduces a new method for determining the correct URL to use for
these redirects by automatically replacing the protocol with `https` if
the `HTTPS_ONLY` env var is set for that instance.

Fixes #538

Fixes #545
2021-11-21 23:21:04 -07:00
Ben Busby
0c5578937e
Remove 308 redirect for http->https
HTTPS upgrades should be handled outside of Whoogle, since Flask often
doesn't detect the right protocol when being used behind a reverse proxy
such as Nginx.
2021-11-20 16:43:57 -07:00
Ben Busby
de28e06d8f
Improve cookie security when HTTPS_ONLY is set
Adds the "Secure" flag and "__Secure-" prefix if the `HTTPS_ONLY`
environment variable is enabled.

Fixes #539
2021-11-20 16:34:37 -07:00
Ben Busby
a768c1b5aa
Revert "Allow executing run script w/o prior setup"
This reverts commit 7f91de7399.

Fixes #540
2021-11-20 16:03:10 -07:00
Ben Busby
7f91de7399
Allow executing run script w/o prior setup
This change allows a bit quicker and simpler setup on new servers.
Rather than setting up dependencies, virtual environment, etc, a systemd
daemon, for example, can just ExecStart the script from any location
without having to perform any preliminary setup. The only prerequisite
step now is having Python3+ installed.
2021-11-19 20:30:13 -07:00
Ben Busby
e06ff85579
Improve public instance session management (#480)
This introduces a new approach to handling user sessions, which should
allow for users to set more reliable config settings on public instances.

Previously, when a user with cookies disabled would update their config,
this would modify the app's default config file, which would in turn
cause new users to inherit these settings when visiting the app for the
first time and cause users to inherit these settings when their current
session cookie expired (which was after 30 days by default I believe).
There was also some half-baked logic for determining on the backend
whether or not a user had cookies disabled, which lead to some issues
with out of control session file creation by Flask.

Now, when a user visits the site, their initial request is forwarded to
a session/<session id> endpoint, and during that subsequent request
their current session id is matched against the one found in the url. If
the ids match, the user has cookies enabled. If not, their original
request is modified with a 'cookies_disabled' query param that tells
Flask not to bother trying to set up a new session for that user, and
instead just use the app's fallback Fernet key for encryption and the
default config.

Since attempting to create a session for a user with cookies disabled
creates a new session file, there is now also a clean-up routine included
in the new session decorator, which will remove all sessions that don't
include a valid key in the dict. NOTE!!! This means that current user
sessions on public instances will be cleared once this update is merged
in. In the long run that's a good thing though, since this will allow session
mgmt to be a lot more reliable overall for users regardless of their cookie
preference.

Individual user sessions still use a unique Fernet key for encrypting queries,
but users with cookies disabled will use the default app key for encryption
and decryption.

Sessions are also now (semi)permanent and have a lifetime of 1 year.
2021-11-17 19:35:30 -07:00
Joao A. Candido Ramos
1f18e505ab
Include "chips" param in image search (#534)
"chips" is used in image tabs to pass the optional "filter" to add to the
given search term

Fixes #299
2021-11-17 16:17:27 -07:00
Ben Busby
257b23e89e
Kill app before re-running on replit
Addresses an issue where re-running an instance on replit caused an
`[ERNO 98] Address already in use` error. Now it kills whatever process
is running on the default Whoogle port (5000) before running the app.

Fixes #531
2021-11-15 20:34:18 -07:00
Ben Busby
e93507f148
Catch connection error during Tor validation step
Validation of the Tor connection occasionally fails with a
ConnectionError from requests, which was previously uncaught. This is
now handled appropriately (error message shown and connection dropped).

Fixes #532
2021-11-12 17:19:45 -07:00
gnuhead-chieb
3f40a6c485
Add Japanese translation (#528) 2021-11-09 08:37:49 -07:00
Robert Blaine
24cc07c20a
feat: Simple Helm Chart (#522)
Add a simple Kubernetes Helm Chart to deploy Whoogle
2021-11-07 10:48:55 -07:00
Albony Cal
b742b6fc0d
Add new public instance to readme (#525)
https://search.albony.xyz
2021-11-07 10:44:23 -07:00
KokoTheBest
c91103a45b
Add new public instance to readme (#512)
https://www.whooglesearch.ml
2021-11-07 10:41:26 -07:00
Fabian Schilling
9ad1d60a47
Improve URL parsing for full size images (#521)
Skip URLs that are not two-element lists

Fixes #520
2021-11-02 16:22:24 -06:00
Vansh Comar
3784d897d9
Add "update available" indicator to footer (#517)
This checks the latest released version of Whoogle against
the current app version, and shows an "update available"
message if the current version num < latest release num.

Closes #305
2021-11-02 10:35:40 -06:00
Ben Busby
b73c14c7cc
Set max height for config menu
The config menu has gotten out of control recently, but rather than
reducing functionality, I'm just going to set a max height for the div
and allow scrolling within the menu.

Ultimately though this indicates that the app is getting a bit too
complicated (imo). Striking a balance between customization and
minimalism is less of a priority for me nowadays though, hence why I'm
willing to let it slide for now. At some point, maybe when there are
more contributors, it could be nice to refactor this in some way so that
it isn't overwhelming to new users who are looking to customize their
instance (that's just me speculating btw, I haven't actually heard from
anyone who thinks there are too many options in that menu).
2021-11-01 16:55:33 -06:00
Ben Busby
c766554eea
Bang refactor PEP-8 fix
Addresses PEP-8 formatting issue in previous commit
2021-11-01 16:53:19 -06:00
Ben Busby
ddf951de35
Use replace in bang query formatting
Using `format` for formatting bang queries caused a KeyError for some
searches, such as !hd (HUDOC). In that example, the URL returned in the
bangs json was `http://...#{%22fulltext%22:[%22{}%22]...`, where
standard formatting would not work due to the misidentification of
"fulltext" as a formatting key.

The logic has been updated to just replace the first occurence of "{}"
in the URL returned by the bangs dict.

Fixes #513
2021-11-01 16:47:48 -06:00
Ben Busby
829903fb9c
Reset build dir in script before run
Fixes #515 which isn't really a bug, but can occasionally cause
confusion when switching environments for the app
2021-11-01 16:20:22 -06:00
gripped
d1c9b7f803
Remove styling from NoJS liks (#511)
Fixes #510
2021-11-01 16:03:47 -06:00
Ben Busby
7fe066b4ea
Escape result html after bolding search terms
Fixes #518
2021-11-01 15:35:57 -06:00
gripped
c2ced23073
Improve formatting with NoJS enabled (#509)
Removes line breaks, divider, and link location from all NoJS
links in results when NoJS mode is enabled
2021-10-29 09:28:05 -06:00
Ben Busby
0a78c524fa
Expand 'my ip' to work for proxied requests
Adds a check for the HTTP_X_FORWARDED_FOR header, and uses the value
from the request if found.
2021-10-28 21:31:24 -06:00
Ben Busby
26b560da1d
Pass response as str to bsoup for "my ip" card
Due to how the response is now reformed into a new bsoup object when
bolding search query terms, creating an ip card for "my ip" searches
threw an error due to how the new bsoup object was initialized for the
"my ip" card. This passes the response in as a string instead.

Fixes #504
2021-10-28 21:22:51 -06:00
Ben Busby
cad1e2ab4d
Include translation mapping in nojs windows
The translation map was missing for links opened via the nojs feature,
causing a server error.

Fixes #507
2021-10-28 21:06:52 -06:00
DUO Labs
5189cdb072
Update "skip bolding" regex to fix some edge cases (#500)
Should address errors caused by the "bold query" feature replacing
tags and style elements, resulting in unformatted response pages.
2021-10-28 12:54:27 -06:00
Vansh Comar
f04c7c5557
Support DDG style bangs with bang at the end (#503)
DDG style bang searches can now have the bang (!) at the end of
the search (i.e. "bologna w!" will now redirect to wikipedia just like
"bologna !w" would)
2021-10-28 12:39:33 -06:00
Ben Busby
190b684469
Reformat view templates 2021-10-27 12:30:55 -06:00
Ben Busby
b96e3a0acb
Make base search url a member of the request class
Since the request class is loaded prior to values being read from the
user's dotenv, the WHOOGLE_RESULT_PER_PAGE var wasn't being used for
searches.

This moves the definition of the base search url to be intialized in the
request class to address this issue.

Fixes #497
2021-10-27 11:02:14 -06:00
DUO Labs
d8dcdc7455
Skip bolding search terms that are not alphanumeric (#496)
Fixes #494
2021-10-27 10:50:21 -06:00
Ben Busby
1abd040428
Remove redundant loading of variables.css
variables.css doesn't need to be loaded by any template, since
WHOOGLE_CONFIG_STYLE loads those values by default when not set
explicitly. Loading the stylesheet caused the logo colors to be
persistent unless set individually.

Sorry @gripped for sneaking all of this unnecessary color in...

Fixes #492
2021-10-26 21:11:46 -06:00
Ben Busby
591ed4a6d6
Use f-string in bold query regex
by @DUOLabs333
2021-10-26 16:21:30 -06:00
Ben Busby
f154b5f2e2
PEP-8 formatting fix 2021-10-26 16:17:38 -06:00
Ben Busby
6decab5a51
Improve regex for bolding search terms
Co-authored by @DUOLabs333
2021-10-26 16:15:24 -06:00
Ben Busby
6763c2e99d
Remove test for deprecated feature
Setting config using the URL is a feature that is being deprecated in
the next release, so the test for confirming its functionality has been
removed.
2021-10-26 15:04:21 -06:00
Ben Busby
d16ef6d011
Unescape search response before rendering template
Fixes a small issue with the previous commit where bolded search terms
had the <b> tags escaped, rather than being applied as actual html.
2021-10-26 15:00:39 -06:00
DUO Labs
2c9cf3ecc6
Bold search query in results (#487)
This modifies the search result page by bold-ing all appearances
of any word in the original query. If portions of the query are in
quotes (i.e. "ice cream"), only exact matches of the sequence of
words will be made bold.

Co-authored-by: Ben Busby <noreply+git@benbusby.com>
2021-10-26 14:59:23 -06:00
Ben Busby
90441b2668
Add WHOOGLE_MINIMAL to docs, tweak min mode logic
Activating minimal mode should also remove all collapsed sections, if
any are found.

WHOOGLE_MINIMAL now documented in readme and app.json (for heroku).
2021-10-26 10:38:20 -06:00
DUO Labs
543f2b2a01
Add a "minimal mode" for condensing results (#485)
If WHOOGLE_MINIMAL is set, all non-link results are
removed from the view.
2021-10-26 10:35:12 -06:00
DUO Labs
5a05bfb6de
Allow setting number of results per page (#486)
Add `WHOOGLE_RESULTS_PER_PAGE` var, allowing users to 
specify the number of results per page. The default is 10.
2021-10-26 10:28:38 -06:00
Vansh Comar
5118ddb8b8
Allow setting "Accept-Language" header (#483)
Closes #445
2021-10-25 15:49:09 -06:00
Ben Busby
999248d71b
Use externally accessible links for images in readme 2021-10-24 18:41:13 -06:00
Ben Busby
19e89de5d9
Expand on "features" section of readme
The "no JS" and "no cookies" portions of the readme warranted further
explanation. Since Whoogle uses JS and server-side cookies, it might be
confusing to a passerby what is actually meant by this. 

Note that both JS and cookies can be blocked and Whoogle will still be
able to perform searches perfectly well. 

Also updated the "theme" feature description
2021-10-24 00:17:38 -06:00
Ben Busby
91002ec6be
Update default theme css
I've gotten a bit bored of the current light/dark themes, so I'm
switching the default theme over to the Doppelganger theme, which is a
better template/jumping off point for users to use when creating custom
themes since it also provides examples for coloring each of the Whoogle
logo letters.
2021-10-23 23:56:38 -06:00
Ben Busby
8f70236403
Update domains used for scribe.rip replacements
The levelup.gitconnected.com site is a Medium site that can also be
replaced with scribe.rip whenever privacy respecting site alternatives
are enabled in the config.

Also modified how link descriptions are updated when that config is
enabled (before it was missing replacements on quite a few
descriptions).
2021-10-23 23:23:37 -06:00
Ben Busby
05c492bf82
Update pytest to 6.2.5 2021-10-21 12:45:25 -06:00
Ben Busby
782d4e160e
Update cffi dep to 1.15.0 2021-10-21 12:41:23 -06:00
Vansh Comar
771bf34ce9
Show client IP for "my ip" searches (#469)
This introduces a new UI element for displaying the client IP
address when a search for "my ip" is used.

Note that this does not show the IP address seen by Google
if Whoogle is deployed remotely. It uses `request.remote_addr`
to display the client IP address in the UI, not the actual address
of the server (which is what Google sees in requests sent from
remote Whoogle instances).
2021-10-21 10:42:31 -06:00
Ben Busby
aff7b6c72f
Fix latest image build workflow condition 2021-10-20 20:41:04 -06:00
Yadomin
284a8102c8
Block by result title or url using regex (#473)
Allows blocking search results using a regex filter for either
result title or result url
2021-10-20 20:01:04 -06:00
fredster33
f7b8b30e9d
Fix typo in readme (#478) 2021-10-20 15:37:17 -06:00
Ben Busby
d4e5984ccd
Add check for event trigger in buildx action 2021-10-20 15:32:45 -06:00
Ben Busby
1b7d3edd30
Split latest and tagged buildx actions
Reduces redundancy with builds when creating a tag
2021-10-20 12:39:04 -06:00
Ben Busby
6a229eba5f
Skip copying whoogle.env in Dockerfile 2021-10-19 12:44:44 -06:00
Ben Busby
e6bca2d35f
Update branch for heroku quick deploy
Fixes #474
2021-10-19 10:37:11 -06:00
Ben Busby
ca782875c2
Conditionally load .env file in Dockerfile
With 843632a, whoogle.env is now gitignored and should only be created
by users from the whoogle.template.env file. Since the file no longer
exists, the docker build cannot copy it in by default. This just
conditionally copies the file in if it exists.
2021-10-18 15:12:20 -06:00
Ben Busby
843632a22c
Refactor whoogle.env -> whoogle.template.env
Renamed to avoid collision issues for users who update the env file when
running their instance.

Non-template env file is gitignored to avoid accidental tracking.

Fixes #467
2021-10-18 15:02:49 -06:00
Vansh Comar
79fb7531be
Implement scribe.rip replacement for medium.com results (#463)
scribe.rip is a privacy respecting front end for medium.com. This
feature allows medium.com results to be replaced with scribe.rip links,
and works for both regular medium.com domains as well as user specific
subdomains (i.e. user.medium.com).

[scribe.rip website](https://scribe.rip)
[scribe.rip source code](https://git.sr.ht/~edwardloveall/scribe)

Co-authored-by: Ben Busby <noreply+git@benbusby.com>
2021-10-16 12:22:00 -06:00
Ben Busby
ee6a27e541
Add link to user css themes in config menu 2021-10-14 20:20:12 -06:00
Ben Busby
87fbb04c59 Add new theme template 2021-10-14 20:10:01 -06:00
Ben Busby
ff885e4fde
Disable autocomplete via WHOOGLE_AUTOCOMPLETE var
Setting WHOOGLE_AUTOCOMPLETE to 0 now disables the autocomplete/search
suggestion feature.

Closes #462
2021-10-14 18:59:10 -06:00
Ben Busby
18688705be
Update libraries 2021-10-14 17:57:05 -06:00
Ben Busby
c1d8a0c625
Add build status badge to readme 2021-10-13 21:09:29 -06:00
Ben Busby
a76d39ec86
Fix missing translations in config menu
Closes #374
2021-10-13 21:07:42 -06:00
Ben Busby
9097c3ae23
Add /home endpoint to header template
Used in header templates for navigating back to the home page when
behind a reverse proxy config where the app is running from a subpath of
a domain (i.e. "https://something/whoogle/")

Fixes #403
2021-10-13 20:55:26 -06:00
Ben Busby
20976f2ab9
Exclude test dir from docker actions 2021-10-11 21:13:59 -06:00
Ben Busby
c6716e6d46
Enable tag builds for pypi and buildx workflows 2021-10-11 20:22:29 -06:00
Ben Busby
60b36c3b19
Update buildx workflow
Buildx workflow now waits for tests to pass before building/uploading
new images.

There's also a separate step for building a properly formatted tag image
if triggered by a new tag.
2021-10-11 20:11:31 -06:00
Ben Busby
334aabacb7
Bump version to 0.6.0 2021-10-11 17:44:57 -06:00
Albony Cal
c89353cfec
Add hindi translation (#448) 2021-10-11 14:32:03 -06:00
rn83
f18400b1f1
Strip SKIP_PREFIX for SITE_ALTS only (#452)
Domain prefixes (www, mobile, m) are now striped for site alternatives only.
2021-10-11 14:25:21 -06:00
Ben Busby
2dd86fcf97
Update systemd instructions
Fixes #453
2021-10-11 14:22:11 -06:00
Ben Busby
002e2103ad
Simplify buildx workflow
There doesn't really need to be a 'develop' branch anymore, since all
work is committed directly to 'main', with tags to indicate
production-ready builds.

As a result, the buildx-dev workflow is pretty pointless.
2021-09-29 20:54:48 -06:00
Ben Busby
b189ea3963
Fix hardcoded search method in header template
Should use GET if user has configured "GET only" in their config

Closes #446
2021-09-29 20:40:56 -06:00
Oscar
57a7bf6e95
Remove whoogle.silkky.cloud public instance (#443) 2021-09-29 20:33:50 -06:00
Ben Busby
1729324fbd
Run pep8 check on PRs 2021-09-27 20:40:51 -06:00
BorislavGeorgiev
10b60d9373
Add Bulgarian translation (#440) 2021-09-27 20:39:38 -06:00
BlissOWL
f12b0e62c5
Make bang searches case insensitive (#438)
Bang searches now ignore the capitalization of the operator

Co-authored-by: Ben Busby <noreply+git@benbusby.com>
2021-09-27 19:39:58 -06:00
Ben Busby
27d978f232
Hide overflow on all result divs
Mostly addresses the small amount of visible overflow on sections like
"Top Stories".
2021-09-18 21:19:05 -06:00
drugal
46da74fe8a
Add search language to public instances table (#431) 2021-09-16 16:30:56 -06:00
Kang-min Liu
c3fd84b942
Update name of "Taiwan" in country list (#429) 2021-09-16 16:22:44 -06:00
Ben Busby
817b51eb48
Document WHOOGLE_CONFIG_NEAR env var
Fixes #406
2021-09-15 16:00:36 -06:00
Kang-min Liu
5289b4ceb3
Add zh-TW translations (#428)
There are a few conventional choices but this one should be friendly
and generally accepted by local reader.

Previous version is still comprehensible but lesser users (perhaps
used in Japanese documents) and may give local users a pause.
2021-09-15 15:30:53 -06:00
Peter Dave Hello
ed963933d9
Add ini highlighting to readme (#426) 2021-09-15 15:27:50 -06:00
Ben Busby
b4e2add146
Run GH action tests on PRs 2021-09-15 15:27:11 -06:00
FlawCra
598b58a43d
Update location of public instance (#425)
https://search.flawcra.cc -> DE
2021-09-15 15:25:51 -06:00
Flux Industries
f093fd26c1
Add public instance to readme (#423)
search.flux.industries
2021-09-15 15:25:00 -06:00
Ben Busby
d1e0a06ebd
Move timestamp for dev builds into build process 2021-08-31 09:34:28 -06:00
Ben Busby
c298b8447c
Split PyPI action into separate jobs 2021-08-31 09:18:07 -06:00
Ben Busby
5d86326ae6
Append timestamp to TestPyPI build versions
This should allow the same "version" to be uploaded for each commit.
2021-08-31 09:00:55 -06:00
Ben Busby
118c9da813
Remove ARM specific Docker tag note from readme
ARM devices can now use the `latest` tag, so the `buildx-experimental`
tag is no longer necessary.
2021-08-31 08:15:28 -06:00
Ben Busby
4ad4ed5ff7
Publish to PyPI using GitHub Actions
Regular commits are all built and publish to TestPyPI, tagged commits
are published to PyPI.

This should finish the process of moving away from Travis CI, now that
both testing and PyPI deployments are handled in github actions.
2021-08-31 08:03:08 -06:00
Ben Busby
9f84a8ad83
Remove form action from csp
Restricting form-action to 'self' in the content security policy
prevented Chrome (and likely other browsers) from using !bangs on the
home page.

Fixes #408
2021-08-31 07:57:50 -06:00
Ben Busby
ad2b2554c1
Use UTF-8 encoding when loading languages json
Fixes #371
2021-08-30 17:23:19 -06:00
Ben Busby
9320d8e230
Remove travis yml, update readme test badge 2021-08-30 16:47:49 -06:00
Ben Busby
648a540126
Move all testing to github actions
The Travis CI folks are updating stuff and broke my tests, so I'm moving
over to github actions instead since that is (hopefully) less likely to
change moving forward.

Will need to move PyPi deployment to github actions as well.
2021-08-30 16:39:52 -06:00
alefvanoon
981c7d28f8
Add persian (farsi) translation (#400) 2021-08-30 16:17:14 -06:00
davidfrickert
71070ee921
Fix portuguese translations (#405) 2021-08-30 16:11:32 -06:00
alefvanoon
be3714f074
Fix rtl lang problem in search box (#399)
Adds auto dir to index, search and header input html
2021-08-30 16:10:07 -06:00
alefvanoon
388f51cfb7
Add table for public instances (#398)
Includes new "Country" and "Cloudflare" columns
2021-08-30 16:07:23 -06:00
Trottel
f34490d4f1
Add Czech translation (#397) 2021-08-30 16:05:19 -06:00
Ben Busby
b44762d157
Split buildx action into main and dev builds
Since Docker Hub no longer allows automated builds for free tier users,
the build process for new images needs to be moved to GitHub Actions.
The existing buildx workflow has worked pretty well for the most part,
but was only enabled for the develop branch and only pushed the
buildx-experimental tag. This addition allows pushes to the main branch
to build updates for the "latest" tag as well, which is more commonly
used I think.
2021-08-24 09:38:35 -06:00
Darkempire
4f5ed37c0a
Add French translation (#391) 2021-08-24 09:12:34 -06:00
alefvanoon
14a5e63ad9
Remove dead instance, add new public instance (#387)
https://s.alefvanboon.xyz
2021-08-24 09:10:38 -06:00
gripped
8d24f8abdd
Fix white background on dropdown for result selectors, time etc (#384) 2021-08-24 09:07:34 -06:00
Laurent le Beau-Martin
1a3790c7b1
Only open external links in a new tab (#380) 2021-08-24 09:06:41 -06:00
සයුරි | Sayuri
8e91564600
Update translations (#373) 2021-07-22 09:13:09 -06:00
KokoTheBest
a69ec74cfd
Make replit install all requirements first (#378)
* Make replit install all requirements first

This should install all requirements from requirements.txt. It makes this a one click experience, without the user having to run `pip install -r requirements.txt` and then tap the run button. I myself had to first run that command in my repl, so I have made this change so others don't have to do the same.

repl.it also runs on linux based systems, so `&&` is the correct bash syntax.

* Running in Bash

I applied the same change I made on onBoot to the run variable, and made the language bash as the syntax `./` and `&&` belong to bash.
2021-07-22 09:11:08 -06:00
Ben Busby
694642ccb3
Set bg color for "top stories" elements 2021-07-05 00:18:28 -04:00
Ben Busby
38c38a772f
Find valid parent element when collapsing result content
Previously if a result element marked for collapsing didn't have a valid
"parent" element, the collapsing was skipped altogether. This loops
through child elements until a valid parent is found (or if one isn't
found, the element will not be collapsed).
2021-07-04 15:20:19 -04:00
Ben Busby
958faed1b6
Set user ownership of static build dir 2021-07-02 16:21:43 -04:00
Ben Busby
13202cc6b1
Ensure existence of static build dir 2021-07-02 16:21:38 -04:00
Ben Busby
68fdd55482
Use cache busting for css/js files
On app init, short hashes are generated from file checksums to use for
cache busting. These hashes are added into the full file name and used
to symlink to the actual file contents. These symlinks are loaded in the
jinja templates for each page, and can tell the browser to load a new
file if the hash changes.

This is only in place for css and js files, but can be extended in the
future for other file types if needed.
2021-06-30 19:00:01 -04:00
Ben Busby
c41e0fc239
Allow theme to mirror user system settings
Introduces a new config element and environment variable
(WHOOGLE_CONFIG_THEME) for setting the theme of the app. Rather than
just having either light or dark, this allows a user to have their
instance use their current system light/dark preference to determine the
theme to use.

As a result, the dark mode setting (and WHOOGLE_CONFIG_DARK) have been
deprecated, but will still work as expected until a system theme has
been chosen.
2021-06-28 10:26:51 -04:00
Ben Busby
afd01820bb
Collapse long result sections into details/summary elements
Sections such as "People also asked" and "related searches" typically
take up a lot of room on the results page, and don't always have the
most useful information. This checks for result elements with more than
7 child divs, extracts the section title, and wraps all elements in a
"details" element that can be expanded/collapsed by the user.

Note that this functionality existed previously (albeit not implemented
as well), but due to changes in how Google returns searches (switching
from using <h2> elements for section headers to <span> or <div>
elements), the approach to collapsing these sections needed to be
updated.
2021-06-23 18:59:57 -04:00
Ben Busby
d894bd347d
Handle error when parsing image result url 2021-06-16 10:40:18 -04:00
Ben Busby
b21b4f4f57
Skip parsing user agent if absent from request 2021-06-16 10:37:33 -04:00
Ben Busby
bcb1d8ecc9
Add lingva translation support in search (#360)
* Add support for Lingva translations in results

Searches that contain the word "translate" and are normal search queries
(i.e. not news/images/video/etc) now create an iframe to a Lingva url to
translate the user's search using their configured search language.

The Lingva url can be configured using the WHOOGLE_ALT_TL env var, or
will fall back to the official Lingva instance url (lingva.ml).

For more info, visit https://github.com/TheDavidDelta/lingva-translate

* Add basic test for lingva results

* Allow user specified lingva instances through csp frame-src

* Fix pep8 issue
2021-06-15 10:14:42 -04:00
deluxghost
82ccace647
Add zh-CN translation (#355) 2021-06-11 11:33:01 -04:00
Aikatsui
a6b4252210
Add Sinhala translation (#353) 2021-06-11 10:22:25 -04:00
Ben Busby
83d19d7644
Update instructions for Firefox 89+
Also adds steps to take for allowing searches using Firefox Containers

Closes #352
2021-06-10 11:52:30 -04:00
89 changed files with 5560 additions and 1448 deletions

View File

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

38
.github/ISSUE_TEMPLATE/new-theme.md vendored Normal file
View File

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

View File

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

28
.github/workflows/docker_main.yml vendored Normal file
View File

@ -0,0 +1,28 @@
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 Normal file
View File

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

View File

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

68
.github/workflows/pypi.yml vendored Normal file
View File

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

19
.github/workflows/scan.yml vendored Normal file
View File

@ -0,0 +1,19 @@
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 Normal file
View File

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

6
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

329
README.md
View File

@ -2,11 +2,18 @@
[![Latest Release](https://img.shields.io/github/v/release/benbusby/whoogle-search)](https://github.com/benbusby/shoogle/releases) [![Latest Release](https://img.shields.io/github/v/release/benbusby/whoogle-search)](https://github.com/benbusby/shoogle/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Build Status](https://travis-ci.com/benbusby/whoogle-search.svg?branch=master)](https://travis-ci.com/benbusby/whoogle-search) [![tests](https://github.com/benbusby/whoogle-search/actions/workflows/tests.yml/badge.svg)](https://github.com/benbusby/whoogle-search/actions/workflows/tests.yml)
[![pep8](https://github.com/benbusby/whoogle-search/workflows/pep8/badge.svg)](https://github.com/benbusby/whoogle-search/actions?query=workflow%3Apep8) [![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) [![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) [![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. 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 Contents
@ -21,25 +28,26 @@ Contents
6. [Manual](#f-manual) 6. [Manual](#f-manual)
7. [Docker](#g-manual-docker) 7. [Docker](#g-manual-docker)
8. [Arch/AUR](#arch-linux--arch-based-distributions) 8. [Arch/AUR](#arch-linux--arch-based-distributions)
9. [Helm/Kubernetes](#helm-chart-for-kubernetes)
4. [Environment Variables and Configuration](#environment-variables) 4. [Environment Variables and Configuration](#environment-variables)
5. [Usage](#usage) 5. [Usage](#usage)
6. [Extra Steps](#extra-steps) 6. [Extra Steps](#extra-steps)
1. [Set Primary Search Engine](#set-whoogle-as-your-primary-search-engine) 1. [Set Primary Search Engine](#set-whoogle-as-your-primary-search-engine)
2. [Prevent Downtime (Heroku Only)](#prevent-downtime-heroku-only) 2. [Prevent Downtime (Heroku Only)](#prevent-downtime-heroku-only)
3. [Manual HTTPS Enforcement](#https-enforcement) 3. [Manual HTTPS Enforcement](#https-enforcement)
4. [Using with Firefox Containers](#using-with-firefox-containers)
5. [Reverse Proxying](#reverse-proxying)
1. [Nginx](#nginx)
7. [Contributing](#contributing) 7. [Contributing](#contributing)
8. [FAQ](#faq) 8. [FAQ](#faq)
9. [Public Instances](#public-instances) 9. [Public Instances](#public-instances)
10. [Screenshots](#screenshots) 10. [Screenshots](#screenshots)
11. Mirrors (read-only)
1. [GitLab](https://gitlab.com/benbusby/whoogle-search)
2. [Gogs](https://gogs.benbusby.com/benbusby/whoogle-search)
## Features ## Features
- No ads or sponsored content - No ads or sponsored content
- No javascript - No JavaScript\*
- No cookies - No cookies\*\*
- No tracking/linking of your personal IP address\* - No tracking/linking of your personal IP address\*\*\*
- No AMP links - No AMP links
- No URL tracking tags (i.e. utm=%s) - No URL tracking tags (i.e. utm=%s)
- No referrer header - No referrer header
@ -47,14 +55,18 @@ Contents
- Autocomplete/search suggestions - Autocomplete/search suggestions
- POST request search and suggestion queries (when possible) - POST request search and suggestion queries (when possible)
- View images at full res without site redirect (currently mobile only) - View images at full res without site redirect (currently mobile only)
- Dark mode - 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 - Randomly generated User Agent
- Easy to install/deploy - Easy to install/deploy
- DDG-style bang (i.e. `!<tag> <query>`) searches - DDG-style bang (i.e. `!<tag> <query>`) searches
- Optional location-based searching (i.e. results near \<city\>) - Optional location-based searching (i.e. results near \<city\>)
- Optional NoJS mode to disable all Javascript in results - Optional NoJS mode to view search results in a separate window with JavaScript blocked
<sup>*If deployed to a remote server, or configured to send requests through a VPN, Tor, proxy, etc.</sup> <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>
## Dependencies ## Dependencies
If using Heroku Quick Deploy, **you can skip this section**. If using Heroku Quick Deploy, **you can skip this section**.
@ -71,15 +83,17 @@ If using Heroku Quick Deploy, **you can skip this section**.
There are a few different ways to begin using the app, depending on your preferences: There are a few different ways to begin using the app, depending on your preferences:
### A) [Heroku Quick Deploy](https://heroku.com/about) ### A) [Heroku Quick Deploy](https://heroku.com/about)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/benbusby/whoogle-search/tree/heroku-app-beta) [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/benbusby/whoogle-search/tree/main)
*Note: Requires a (free) Heroku account*
Provides: Provides:
- Free deployment of app - Free deployment of app
- Free HTTPS url (https://\<your app name\>.herokuapp.com) - Free HTTPS url (https://\<your app name\>.herokuapp.com)
- Downtime after periods of inactivity \([solution](https://github.com/benbusby/whoogle-search#prevent-downtime-heroku-only)\) - Downtime after periods of inactivity \([solution](https://github.com/benbusby/whoogle-search#prevent-downtime-heroku-only)\)
Notes:
- Requires a (free) Heroku account
- Sometimes has issues with auto-redirecting to `https`. Make sure to navigate to the `https` version of your app before adding as a default search engine.
### B) [Repl.it](https://repl.it) ### B) [Repl.it](https://repl.it)
[![Run on Repl.it](https://repl.it/badge/github/benbusby/whoogle-search)](https://repl.it/github/benbusby/whoogle-search) [![Run on Repl.it](https://repl.it/badge/github/benbusby/whoogle-search)](https://repl.it/github/benbusby/whoogle-search)
@ -93,30 +107,19 @@ Provides:
### C) [Fly.io](https://fly.io) ### C) [Fly.io](https://fly.io)
You will need a [Fly.io](https://fly.io) account to do this. Fly requires a credit card to deploy anything, but you can have up to 3 shared-CPU VMs running full-time each month for free. You will need a **PAID** [Fly.io](https://fly.io) account to deploy Whoogle.
#### Install the CLI: #### Install the CLI: https://fly.io/docs/hands-on/installing/
#### Deploy the app
```bash ```bash
curl -L https://fly.io/install.sh | sh flyctl auth login
``` flyctl launch --image benbusby/whoogle-search:latest
#### Deploy your app
```bash
fly apps create --org personal --port 5000
# Choose a name and the Image builder
# Enter `benbusby/whoogle-search:latest` as the image name
fly deploy
``` ```
Your app is now available at `https://<app-name>.fly.dev`. Your app is now available at `https://<app-name>.fly.dev`.
You can customize the `fly.toml`:
- Remove the non-https service
- Add environment variables under the `[env]` key
- Use `fly secrets set NAME=value` for more sensitive values like `WHOOGLE_PASS` and `WHOOGLE_PROXY_PASS`.
### D) [pipx](https://github.com/pipxproject/pipx#install-pipx) ### D) [pipx](https://github.com/pipxproject/pipx#install-pipx)
Persistent install: Persistent install:
@ -155,7 +158,7 @@ See the [available environment variables](#environment-variables) for additional
### F) Manual ### F) Manual
*Note: `Content-Security-Policy` headers are already sent by Whoogle -- you don't/shouldn't need to apply a CSP header yourself* *Note: `Content-Security-Policy` headers can be sent by Whoogle if you set `WHOOGLE_CSP`.*
Clone the repo and run the following commands to start the app in a local-only environment: Clone the repo and run the following commands to start the app in a local-only environment:
@ -170,9 +173,9 @@ pip install -r requirements.txt
See the [available environment variables](#environment-variables) for additional configuration. See the [available environment variables](#environment-variables) for additional configuration.
#### systemd Configuration #### systemd Configuration
After building the virtual environment, you can add the following to `/lib/systemd/system/whoogle.service` to set up a Whoogle Search systemd service: After building the virtual environment, you can add something like the following to `/lib/systemd/system/whoogle.service` to set up a Whoogle Search systemd service:
``` ```ini
[Unit] [Unit]
Description=Whoogle Description=Whoogle
@ -187,17 +190,29 @@ Description=Whoogle
#Environment=WHOOGLE_PROXY_LOC=<proxy host/ip> #Environment=WHOOGLE_PROXY_LOC=<proxy host/ip>
# Site alternative configurations, uncomment to enable # Site alternative configurations, uncomment to enable
# Note: If not set, the feature will still be available # Note: If not set, the feature will still be available
# with default values. # with default values.
#Environment=WHOOGLE_ALT_TW=nitter.net #Environment=WHOOGLE_ALT_TW=farside.link/nitter
#Environment=WHOOGLE_ALT_YT=invidious.snopyta.org #Environment=WHOOGLE_ALT_YT=farside.link/invidious
#Environment=WHOOGLE_ALT_IG=bibliogram.art/u #Environment=WHOOGLE_ALT_IG=farside.link/bibliogram/u
#Environment=WHOOGLE_ALT_RD=libredd.it #Environment=WHOOGLE_ALT_RD=farside.link/libreddit
#Environment=WHOOGLE_ALT_MD=farside.link/scribe
#Environment=WHOOGLE_ALT_TL=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
# Load values from dotenv only # Load values from dotenv only
#Environment=WHOOGLE_DOTENV=1 #Environment=WHOOGLE_DOTENV=1
Type=simple Type=simple
User=root User=<username>
WorkingDirectory=<whoogle_directory> # If installed as a package, add:
ExecStart=<whoogle_directory>/venv/bin/python3 -um app --host 0.0.0.0 --port 5000 ExecStart=<python_install_dir>/python3 <whoogle_install_dir>/whoogle-search --host 127.0.0.1 --port 5000
# For example:
# ExecStart=/usr/bin/python3 /home/my_username/.local/bin/whoogle-search --host 127.0.0.1 --port 5000
# Otherwise if running the app from source, add:
ExecStart=<whoogle_repo_dir>/run
# For example:
# ExecStart=/var/www/whoogle-search/run
ExecReload=/bin/kill -HUP $MAINPID ExecReload=/bin/kill -HUP $MAINPID
Restart=always Restart=always
RestartSec=3 RestartSec=3
@ -213,6 +228,51 @@ sudo systemctl enable whoogle
sudo systemctl start whoogle sudo systemctl start whoogle
``` ```
#### Tor Configuration *optional*
If routing your request through Tor you will need to make the following adjustments.
Due to the nature of interacting with Google through Tor we will need to be able to send signals to Tor and therefore authenticate with it.
There are two authentication methods, password and cookie. You will need to make changes to your torrc:
* Cookie
1. Uncomment or add the following lines in your torrc:
- `ControlPort 9051`
- `CookieAuthentication 1`
- `DataDirectoryGroupReadable 1`
- `CookieAuthFileGroupReadable 1`
2. Make the tor auth cookie readable:
- This is assuming that you are using a dedicated user to run whoogle. If you are using a different user replace `whoogle` with that user.
1. `chmod tor:whoogle /var/lib/tor`
2. `chmod tor:whoogle /var/lib/tor/control_auth_cookie`
3. Restart the tor service:
- `systemctl restart tor`
4. Set the Tor environment variable to 1, `WHOOGLE_CONFIG_TOR`. Refer to the [Environment Variables](#environment-variables) section for more details.
- This may be added in the systemd unit file or env file `WHOOGLE_CONFIG_TOR=1`
* Password
1. Run this command:
- `tor --hash-password {Your Password Here}`; put your password in place of `{Your Password Here}`.
- Keep the output of this command, you will be placing it in your torrc.
- Keep the password input of this command, you will be using it later.
2. Uncomment or add the following lines in your torrc:
- `ControlPort 9051`
- `HashedControlPassword {Place output here}`; put the output of the previous command in place of `{Place output here}`.
3. Now take the password from the first step and place it in the control.conf file within the whoogle working directory, ie. [misc/tor/control.conf](misc/tor/control.conf)
- If you want to place your password file in a different location set this location with the `WHOOGLE_TOR_CONF` environment variable. Refer to the [Environment Variables](#environment-variables) section for more details.
4. Heavily restrict access to control.conf to only be readable by the user running whoogle:
- `chmod 400 control.conf`
5. Finally set the Tor environment variable and use password variable to 1, `WHOOGLE_CONFIG_TOR` and `WHOOGLE_TOR_USE_PASS`. Refer to the [Environment Variables](#environment-variables) section for more details.
- These may be added to the systemd unit file or env file:
- `WHOOGLE_CONFIG_TOR=1`
- `WHOOGLE_TOR_USE_PASS=1`
### G) Manual (Docker) ### G) Manual (Docker)
1. Ensure the Docker daemon is running, and is accessible by your user account 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` - To add user permissions, you can execute `sudo usermod -aG docker yourusername`
@ -221,8 +281,6 @@ sudo systemctl start whoogle
#### Docker CLI #### Docker CLI
***Note:** For ARM machines, use the `buildx-experimental` Docker tag.*
Through Docker Hub: Through Docker Hub:
```bash ```bash
docker pull benbusby/whoogle-search docker pull benbusby/whoogle-search
@ -279,6 +337,13 @@ You may also edit environment variables from your apps Settings tab in the He
#### Arch Linux & Arch-based Distributions #### Arch Linux & Arch-based Distributions
There is an [AUR package available](https://aur.archlinux.org/packages/whoogle-git/), as well as a pre-built and daily updated package available at [Chaotic-AUR](https://chaotic.cx). There is an [AUR package available](https://aur.archlinux.org/packages/whoogle-git/), as well as a pre-built and daily updated package available at [Chaotic-AUR](https://chaotic.cx).
#### Helm chart for Kubernetes
To use the Kubernetes Helm Chart:
1. Ensure you have [Helm](https://helm.sh/docs/intro/install/) `>=3.0.0` installed
2. Clone this repository
3. Update [charts/whoogle/values.yaml](./charts/whoogle/values.yaml) as desired
4. Run `helm install whoogle ./charts/whoogle`
#### Using your own server, or alternative container deployment #### Using your own server, or alternative container deployment
There are other methods for deploying docker containers that are well outlined in [this article](https://rollout.io/blog/the-shortlist-of-docker-hosting/), but there are too many to describe set up for each here. Generally it should be about the same amount of effort as the Heroku deployment. There are other methods for deploying docker containers that are well outlined in [this article](https://rollout.io/blog/the-shortlist-of-docker-hosting/), but there are too many to describe set up for each here. Generally it should be about the same amount of effort as the Heroku deployment.
@ -295,41 +360,58 @@ There are a few optional environment variables available for customizing a Whoog
- With `docker-compose`: Uncomment the `env_file` option - With `docker-compose`: Uncomment the `env_file` option
- With `docker build/run`: Add `--env-file ./whoogle.env` to your command - With `docker build/run`: Add `--env-file ./whoogle.env` to your command
| Variable | Description | | Variable | Description |
| ------------------ | ----------------------------------------------------------------------------------------- | | -------------------- | ----------------------------------------------------------------------------------------- |
| WHOOGLE_DOTENV | Load environment variables in `whoogle.env` | | WHOOGLE_URL_PREFIX | The URL prefix to use for the whoogle instance (i.e. "/whoogle") |
| WHOOGLE_USER | The username for basic auth. WHOOGLE_PASS must also be set if used. | | WHOOGLE_DOTENV | Load environment variables in `whoogle.env` |
| WHOOGLE_PASS | The password for basic auth. WHOOGLE_USER must also be set if used. | | WHOOGLE_USER | The username for basic auth. WHOOGLE_PASS must also be set if used. |
| WHOOGLE_PROXY_USER | The username of the proxy server. | | WHOOGLE_PASS | The password for basic auth. WHOOGLE_USER must also be set if used. |
| WHOOGLE_PROXY_PASS | The password of the proxy server. | | WHOOGLE_PROXY_USER | The username of the proxy server. |
| WHOOGLE_PROXY_TYPE | The type of the proxy server. Can be "socks5", "socks4", or "http". | | WHOOGLE_PROXY_PASS | The password of the proxy server. |
| WHOOGLE_PROXY_LOC | The location of the proxy server (host or ip). | | WHOOGLE_PROXY_TYPE | The type of the proxy server. Can be "socks5", "socks4", or "http". |
| EXPOSE_PORT | The port where Whoogle will be exposed. | | WHOOGLE_PROXY_LOC | The location of the proxy server (host or ip). |
| HTTPS_ONLY | Enforce HTTPS. (See [here](https://github.com/benbusby/whoogle-search#https-enforcement)) | | EXPOSE_PORT | The port where Whoogle will be exposed. |
| WHOOGLE_ALT_TW | The twitter.com alternative to use when site alternatives are enabled in the config. | | HTTPS_ONLY | Enforce HTTPS. (See [here](https://github.com/benbusby/whoogle-search#https-enforcement)) |
| WHOOGLE_ALT_YT | The youtube.com alternative to use when site alternatives are enabled in the config. | | WHOOGLE_ALT_TW | The twitter.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_IG | The instagram.com alternative to use when site alternatives are enabled in the config. | | 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. | | WHOOGLE_ALT_IG | The instagram.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_RD | The reddit.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_TL | The Google Translate alternative to use. This is used for all "translate ____" searches. Set to "" to disable. |
| WHOOGLE_ALT_MD | The medium.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_IMG | The imgur.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_WIKI | The wikipedia.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_IMDB | The imdb.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_QUORA | The quora.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_AUTOCOMPLETE | Controls visibility of autocomplete/search suggestions. Default on -- use '0' to disable. |
| WHOOGLE_MINIMAL | Remove everything except basic result cards from all search queries. |
| WHOOGLE_CSP | Sets a default set of 'Content-Security-Policy' headers |
| WHOOGLE_RESULTS_PER_PAGE | Set the number of results per page |
| WHOOGLE_TOR_SERVICE | Enable/disable the Tor service on startup. Default on -- use '0' to disable. |
| WHOOGLE_TOR_USE_PASS | Use password authentication for tor control port. |
| WHOOGLE_TOR_CONF | The absolute path to the config file containing the password for the tor control port. Default: ./misc/tor/control.conf WHOOGLE_TOR_PASS must be 1 for this to work.|
### Config Environment Variables ### Config Environment Variables
These environment variables allow setting default config values, but can be overwritten manually by using the home page config menu. These allow a shortcut for destroying/rebuilding an instance to the same config state every time. These environment variables allow setting default config values, but can be overwritten manually by using the home page config menu. These allow a shortcut for destroying/rebuilding an instance to the same config state every time.
| Variable | Description | | Variable | Description |
| ------------------------------ | --------------------------------------------------------------- | | ------------------------------------ | --------------------------------------------------------------- |
| WHOOGLE_CONFIG_DISABLE | Hide config from UI and disallow changes to config by client | | WHOOGLE_CONFIG_DISABLE | Hide config from UI and disallow changes to config by client |
| WHOOGLE_CONFIG_COUNTRY | Filter results by hosting country | | WHOOGLE_CONFIG_COUNTRY | Filter results by hosting country |
| WHOOGLE_CONFIG_LANGUAGE | Set interface language | | WHOOGLE_CONFIG_LANGUAGE | Set interface language |
| WHOOGLE_CONFIG_SEARCH_LANGUAGE | Set search result language | | WHOOGLE_CONFIG_SEARCH_LANGUAGE | Set search result language |
| WHOOGLE_CONFIG_BLOCK | Block websites from search results (use comma-separated list) | | WHOOGLE_CONFIG_BLOCK | Block websites from search results (use comma-separated list) |
| WHOOGLE_CONFIG_DARK | Enable dark theme | | WHOOGLE_CONFIG_THEME | Set theme mode (light, dark, or system) |
| WHOOGLE_CONFIG_SAFE | Enable safe searches | | WHOOGLE_CONFIG_SAFE | Enable safe searches |
| WHOOGLE_CONFIG_ALTS | Use social media site alternatives (nitter, invidious, etc) | | WHOOGLE_CONFIG_ALTS | Use social media site alternatives (nitter, invidious, etc) |
| WHOOGLE_CONFIG_TOR | Use Tor routing (if available) | | WHOOGLE_CONFIG_NEAR | Restrict results to only those near a particular city |
| WHOOGLE_CONFIG_NEW_TAB | Always open results in new tab | | WHOOGLE_CONFIG_TOR | Use Tor routing (if available) |
| WHOOGLE_CONFIG_VIEW_IMAGE | Enable View Image option | | WHOOGLE_CONFIG_NEW_TAB | Always open results in new tab |
| WHOOGLE_CONFIG_GET_ONLY | Search using GET requests only | | WHOOGLE_CONFIG_VIEW_IMAGE | Enable View Image option |
| WHOOGLE_CONFIG_URL | The root url of the instance (`https://<your url>/`) | | WHOOGLE_CONFIG_GET_ONLY | Search using GET requests only |
| WHOOGLE_CONFIG_STYLE | The custom CSS to use for styling (should be single line) | | 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) |
## Usage ## Usage
Same as most search engines, with the exception of filtering by time range. Same as most search engines, with the exception of filtering by time range.
@ -342,7 +424,12 @@ To filter by a range of time, append ":past <time>" to the end of your search, w
Browser settings: Browser settings:
- Firefox (Desktop) - Firefox (Desktop)
- Navigate to your app's url, and click the 3 dot menu in the address bar. At the bottom, there should be an option to "Add Search Engine". Once you've clicked this, open your Firefox Preferences menu, click "Search" in the left menu, and use the available dropdown to select "Whoogle" from the list. - 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) - 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: - 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" - Title: "Whoogle"
@ -372,7 +459,7 @@ Browser settings:
2. Go to `Default Results` and click the `Setup fallback results` button. Click `+` and add Whoogle, then drag it to the top. 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 - Chrome/Chromium-based Browsers
- Automatic - Automatic
- Visit the home page of your Whoogle Search instance -- this may automatically add the search engine to your list of search engines. If not, you can add it manually. - 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 - Manual
- Under search engines > manage search engines > add, manually enter your Whoogle instance details with a `<whoogle url>/search?q=%s` formatted search URL. - Under search engines > manage search engines > add, manually enter your Whoogle instance details with a `<whoogle url>/search?q=%s` formatted search URL.
@ -396,6 +483,40 @@ Note: You should have your own domain name and [an https certificate](https://le
- Pip/Pipx: Add the `--https-only` flag to the end of the `whoogle-search` command - 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 - Default `run` script: Modify the script locally to include the `--https-only` flag at the end of the python run command
### Using with Firefox Containers
Unfortunately, Firefox Containers do not currently pass through `POST` requests (the default) to the engine, and Firefox caches the opensearch template on initial page load. To get around this, you can take the following steps to get it working as expected:
1. Remove any existing Whoogle search engines from Firefox settings
2. Enable `GET Requests Only` in Whoogle config
3. Clear Firefox cache
4. Restart Firefox
5. Navigate to Whoogle instance and [re-add the engine](#set-whoogle-as-your-primary-search-engine)
### Reverse Proxying
#### Nginx
Here is a sample Nginx config for Whoogle:
```
server {
server_name your_domain_name.com;
access_log /dev/null;
error_log /dev/null;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://localhost:5000;
}
}
```
You can then add SSL support using LetsEncrypt by following a guide such as [this one](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/).
## Contributing ## Contributing
Under the hood, Whoogle is a basic Flask app with the following structure: Under the hood, Whoogle is a basic Flask app with the following structure:
@ -416,12 +537,12 @@ Under the hood, Whoogle is a basic Flask app with the following structure:
- `search.html`: An iframe-able search page - `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) - `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). - `opensearch.xml`: A template used for supporting [OpenSearch](https://developer.mozilla.org/en-US/docs/Web/OpenSearch).
- `imageresults.html`: An "exprimental" template used for supporting the "Full Size" image feature on desktop. - `imageresults.html`: An "experimental" template used for supporting the "Full Size" image feature on desktop.
- `static/<css|js>` - `static/<css|js>`
- CSS/Javascript files, should be self-explanatory - CSS/Javascript files, should be self-explanatory
- `static/settings` - `static/settings`
- Key-value JSON files for establishing valid configuration values - 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. 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.
@ -440,7 +561,7 @@ def contains(x: list, y: int) -> bool:
""" """
return y in x return y in x
``` ```
#### Translating #### Translating
@ -461,19 +582,45 @@ A lot of the app currently piggybacks on Google's existing support for fetching
## Public Instances ## Public Instances
*Note: Use public instances at your own discretion. Maintainers of Whoogle do not personally validate the integrity of these instances, and popular public instances are more likely to be rate-limited or blocked.* *Note: Use public instances at your own discretion. The maintainers of Whoogle do not personally validate the integrity of any other instances. Popular public instances are more likely to be rate-limited or blocked.*
| Website | Country | Language | Cloudflare |
|-|-|-|-|
| [https://search.albony.xyz](https://search.albony.xyz/) | 🇮🇳 IN | Multi-choice | |
| [https://search.garudalinux.org](https://search.garudalinux.org) | 🇫🇮 FI | Multi-choice | ✅ |
| [https://search.dr460nf1r3.org](https://search.dr460nf1r3.org) | 🇩🇪 DE | Multi-choice | ✅ |
| [https://s.tokhmi.xyz](https://s.tokhmi.xyz) | 🇺🇸 US | Multi-choice | ✅ |
| [https://www.whooglesearch.ml](https://www.whooglesearch.ml) | 🇺🇸 US | English | |
| [https://search.sethforprivacy.com](https://search.sethforprivacy.com) | 🇩🇪 DE | English | |
| [https://whoogle.dcs0.hu](https://whoogle.dcs0.hu) | 🇭🇺 HU | Multi-choice | |
| [https://whoogle.esmailelbob.xyz](https://whoogle.esmailelbob.xyz) | 🇨🇦 CA | Multi-choice | |
| [https://gowogle.voring.me](https://gowogle.voring.me) | 🇺🇸 US | Multi-choice | |
| [https://whoogle.lunar.icu](https://whoogle.lunar.icu) | 🇩🇪 DE | Multi-choice | ✅ |
| [https://whoogle.privacydev.net](https://whoogle.privacydev.net) | 🇺🇸 US | Multi-choice | |
| [https://search.wef.lol](https://search.wef.lol) | 🇮🇸 IC | Multi-choice | |
| [https://wg.vern.cc](https://wg.vern.cc) | 🇺🇸 US | English | |
* A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare.com). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
#### Onion Instances
| Website | Country | Language |
|-|-|-|
| [http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion](http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion) | 🇺🇸 US | Multi-choice
| [http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion](http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion) | 🇩🇪 DE | English
| [http://whoogle.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://whoogle.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion/) | 🇺🇸 US | English |
#### I2P Instances
| Website | Country | Language |
|-|-|-|
| [http://verneks7rfjptpz5fpii7n7nrxilsidi2qxepeuuf66c3tsf4nhq.b32.i2p](http://verneks7rfjptpz5fpii7n7nrxilsidi2qxepeuuf66c3tsf4nhq.b32.i2p) | 🇺🇸 US | English |
- [https://whoogle.sdf.org](https://whoogle.sdf.org)
- [https://whoogle.himiko.cloud](https://whoogle.himiko.cloud)
- [https://whoogle.kavin.rocks](https://whoogle.kavin.rocks) or [http://whoogledq5f5wly5p4i2ohnvjwlihnlg4oajjum2oeddfwqdwupbuhqd.onion](http://whoogledq5f5wly5p4i2ohnvjwlihnlg4oajjum2oeddfwqdwupbuhqd.onion)
- [https://search.garudalinux.org](https://search.garudalinux.org)
- [https://whooglesearch.net/](https://whooglesearch.net/)
- [https://search.flawcra.cc/](https://search.flawcra.cc/)
- [https://search.exonip.de/](https://search.exonip.de/)
- [https://whoogle.silkky.cloud/](https://whoogle.silkky.cloud/)
## Screenshots ## Screenshots
#### Desktop #### Desktop
![Whoogle Desktop](docs/screenshot_desktop.jpg) ![Whoogle Desktop](docs/screenshot_desktop.png)
#### Mobile #### Mobile
![Whoogle Mobile](docs/screenshot_mobile.jpg) ![Whoogle Mobile](docs/screenshot_mobile.png)

View File

@ -15,6 +15,11 @@
], ],
"stack": "container", "stack": "container",
"env": { "env": {
"WHOOGLE_URL_PREFIX": {
"description": "The URL prefix to use for the whoogle instance (i.e. \"/whoogle\")",
"value": "",
"required": false
},
"WHOOGLE_USER": { "WHOOGLE_USER": {
"description": "The username for basic auth. WHOOGLE_PASS must also be set if used. Leave empty to disable.", "description": "The username for basic auth. WHOOGLE_PASS must also be set if used. Leave empty to disable.",
"value": "", "value": "",
@ -47,24 +52,59 @@
}, },
"WHOOGLE_ALT_TW": { "WHOOGLE_ALT_TW": {
"description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.", "description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.",
"value": "nitter.net", "value": "farside.link/nitter",
"required": false "required": false
}, },
"WHOOGLE_ALT_YT": { "WHOOGLE_ALT_YT": {
"description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.", "description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.",
"value": "invidious.snopyta.org", "value": "farside.link/invidious",
"required": false "required": false
}, },
"WHOOGLE_ALT_IG": { "WHOOGLE_ALT_IG": {
"description": "The site to use as a replacement for instagram.com when site alternatives are enabled in the config.", "description": "The site to use as a replacement for instagram.com when site alternatives are enabled in the config.",
"value": "bibliogram.art/u", "value": "farside.link/bibliogram/u",
"required": false "required": false
}, },
"WHOOGLE_ALT_RD": { "WHOOGLE_ALT_RD": {
"description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.", "description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.",
"value": "libredd.it", "value": "farside.link/libreddit",
"required": false "required": false
}, },
"WHOOGLE_ALT_MD": {
"description": "The site to use as a replacement for medium.com when site alternatives are enabled in the config.",
"value": "farside.link/scribe",
"required": false
},
"WHOOGLE_ALT_TL": {
"description": "The Google Translate alternative to use for all searches following the 'translate ___' structure.",
"value": "farside.link/lingva",
"required": false
},
"WHOOGLE_ALT_IMG": {
"description": "The site to use as a replacement for imgur.com when site alternatives are enabled in the config.",
"value": "farside.link/rimgo",
"required": false
},
"WHOOGLE_ALT_WIKI": {
"description": "The site to use as a replacement for wikipedia.com when site alternatives are enabled in the config.",
"value": "farside.link/wikiless",
"required": false
},
"WHOOGLE_ALT_IMDB": {
"description": "The site to use as a replacement for imdb.com when site alternatives are enabled in the config.",
"value": "farside.link/libremdb",
"required": false
},
"WHOOGLE_ALT_QUORA": {
"description": "The site to use as a replacement for quora.com when site alternatives are enabled in the config.",
"value": "farside.link/quetre",
"required": false
},
"WHOOGLE_MINIMAL": {
"description": "Remove everything except basic result cards from all search queries (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_COUNTRY": { "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)", "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": "", "value": "",
@ -90,9 +130,9 @@
"value": "", "value": "",
"required": false "required": false
}, },
"WHOOGLE_CONFIG_DARK": { "WHOOGLE_CONFIG_THEME": {
"description": "[CONFIG] Enable dark mode (set to 1 or leave blank)", "description": "[CONFIG] Set theme to 'dark', 'light', or 'system'",
"value": "", "value": "system",
"required": false "required": false
}, },
"WHOOGLE_CONFIG_SAFE": { "WHOOGLE_CONFIG_SAFE": {
@ -105,6 +145,11 @@
"value": "", "value": "",
"required": false "required": false
}, },
"WHOOGLE_CONFIG_NEAR": {
"description": "[CONFIG] Restrict results to only those near a particular city",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_TOR": { "WHOOGLE_CONFIG_TOR": {
"description": "[CONFIG] Use Tor, if available (set to 1 or leave blank)", "description": "[CONFIG] Use Tor, if available (set to 1 or leave blank)",
"value": "", "value": "",
@ -127,8 +172,18 @@
}, },
"WHOOGLE_CONFIG_STYLE": { "WHOOGLE_CONFIG_STYLE": {
"description": "[CONFIG] Custom CSS styling (paste in CSS or leave blank)", "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": "", "value": "",
"required": false "required": false
},
"WHOOGLE_CONFIG_PREFERENCES_KEY": {
"description": "[CONFIG] Key to encrypt preferences",
"value": "NEEDS_TO_BE_MODIFIED",
"required": false
} }
} }
} }

View File

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

View File

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

View File

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

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

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

46
app/models/g_classes.py Normal file
View File

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

View File

@ -1,15 +1,17 @@
from app.models.config import Config from app.models.config import Config
from app.utils.misc import read_config_bool
from datetime import datetime from datetime import datetime
import xml.etree.ElementTree as ET from defusedxml import ElementTree as ET
import random import random
import requests import requests
from requests import Response, ConnectionError from requests import Response, ConnectionError
import urllib.parse as urlparse import urllib.parse as urlparse
import os import os
from stem import Signal, SocketError from stem import Signal, SocketError
from stem.connection import AuthenticationFailure
from stem.control import Controller from stem.control import Controller
from stem.connection import authenticate_cookie, authenticate_password
SEARCH_URL = 'https://www.google.com/search?gbv=1&q='
MAPS_URL = 'https://maps.google.com/maps' MAPS_URL = 'https://maps.google.com/maps'
AUTOCOMPLETE_URL = ('https://suggestqueries.google.com/' AUTOCOMPLETE_URL = ('https://suggestqueries.google.com/'
'complete/search?client=toolbar&') 'complete/search?client=toolbar&')
@ -38,13 +40,33 @@ class TorError(Exception):
def send_tor_signal(signal: Signal) -> bool: 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: try:
with Controller.from_port(port=9051) as c: with Controller.from_port(port=9051) as c:
c.authenticate() if use_pass:
with open(confloc, "r") as conf:
# Scan for the last line of the file.
for line in conf:
pass
secret = line.strip('\n')
authenticate_password(c, password=secret)
else:
cookie_path = '/var/lib/tor/control_auth_cookie'
authenticate_cookie(c, cookie_path=cookie_path)
c.signal(signal) c.signal(signal)
os.environ['TOR_AVAILABLE'] = '1' os.environ['TOR_AVAILABLE'] = '1'
return True return True
except (SocketError, ConnectionRefusedError, ConnectionError): except (SocketError, AuthenticationFailure,
ConnectionRefusedError, ConnectionError):
# TODO: Handle Tor authentication (password and cookie)
os.environ['TOR_AVAILABLE'] = '0' os.environ['TOR_AVAILABLE'] = '0'
return False return False
@ -60,7 +82,7 @@ def gen_user_agent(is_mobile) -> str:
return DESKTOP_UA.format("Mozilla", linux, firefox) return DESKTOP_UA.format("Mozilla", linux, firefox)
def gen_query(query, args, config, near_city=None) -> str: def gen_query(query, args, config) -> str:
param_dict = {key: '' for key in VALID_PARAMS} param_dict = {key: '' for key in VALID_PARAMS}
# Use :past(hour/day/week/month/year) if available # Use :past(hour/day/week/month/year) if available
@ -97,8 +119,8 @@ def gen_query(query, args, config, near_city=None) -> str:
param_dict['start'] = '&start=' + args.get('start') param_dict['start'] = '&start=' + args.get('start')
# Search for results near a particular city, if available # Search for results near a particular city, if available
if near_city: if config.near:
param_dict['near'] = '&near=' + urlparse.quote(near_city) param_dict['near'] = '&near=' + urlparse.quote(config.near)
# Set language for results (lr) if source isn't set, otherwise use the # Set language for results (lr) if source isn't set, otherwise use the
# result language param provided in the results # result language param provided in the results
@ -108,19 +130,25 @@ def gen_query(query, args, config, near_city=None) -> str:
[_ for _ in lang if not _.isdigit()] [_ for _ in lang if not _.isdigit()]
)) if lang else '' )) if lang else ''
else: else:
param_dict['lr'] = '&lr=' + ( param_dict['lr'] = (
config.lang_search if config.lang_search else '' '&lr=' + config.lang_search
) ) if config.lang_search else ''
# 'nfpr' defines the exclusion of results from an auto-corrected query # 'nfpr' defines the exclusion of results from an auto-corrected query
if 'nfpr' in args: if 'nfpr' in args:
param_dict['nfpr'] = '&nfpr=' + args.get('nfpr') param_dict['nfpr'] = '&nfpr=' + args.get('nfpr')
param_dict['cr'] = ('&cr=' + config.ctry) if config.ctry else '' # 'chips' is used in image tabs to pass the optional 'filter' to add to the
param_dict['hl'] = '&hl=' + ( # given search term
config.lang_interface.replace('lang_', '') if 'chips' in args:
if config.lang_interface else '' param_dict['chips'] = '&chips=' + args.get('chips')
)
param_dict['gl'] = (
'&gl=' + config.country
) if config.country else ''
param_dict['hl'] = (
'&hl=' + config.lang_interface.replace('lang_', '')
) if config.lang_interface else ''
param_dict['safe'] = '&safe=' + ('active' if config.safe else 'off') param_dict['safe'] = '&safe=' + ('active' if config.safe else 'off')
# Block all sites specified in the user config # Block all sites specified in the user config
@ -150,6 +178,8 @@ class Request:
""" """
def __init__(self, normal_ua, root_path, config: Config): def __init__(self, normal_ua, root_path, config: Config):
self.search_url = 'https://www.google.com/search?gbv=1&num=' + str(
os.getenv('WHOOGLE_RESULTS_PER_PAGE', 10)) + '&q='
# Send heartbeat to Tor, used in determining if the user can or cannot # Send heartbeat to Tor, used in determining if the user can or cannot
# enable Tor for future requests # enable Tor for future requests
send_tor_signal(Signal.HEARTBEAT) send_tor_signal(Signal.HEARTBEAT)
@ -157,7 +187,14 @@ class Request:
self.language = ( self.language = (
config.lang_search if config.lang_search else '' config.lang_search if config.lang_search else ''
) )
self.mobile = 'Android' in normal_ua or 'iPhone' in normal_ua
# 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) self.modified_user_agent = gen_user_agent(self.mobile)
if not self.mobile: if not self.mobile:
self.modified_user_agent_mobile = gen_user_agent(True) self.modified_user_agent_mobile = gen_user_agent(True)
@ -205,18 +242,25 @@ class Request:
list: The list of matches for possible search suggestions list: The list of matches for possible search suggestions
""" """
ac_query = dict(hl=self.language, q=query) ac_query = dict(q=query)
if self.language:
ac_query['hl'] = self.language
response = self.send(base_url=AUTOCOMPLETE_URL, response = self.send(base_url=AUTOCOMPLETE_URL,
query=urlparse.urlencode(ac_query)).text query=urlparse.urlencode(ac_query)).text
if not response: if not response:
return [] return []
root = ET.fromstring(response) try:
return [_.attrib['data'] for _ in root = ET.fromstring(response)
root.findall('.//suggestion/[@data]')] return [_.attrib['data'] for _ in
root.findall('.//suggestion/[@data]')]
except ET.ParseError:
# Malformed XML response
return []
def send(self, base_url=SEARCH_URL, query='', attempt=0, def send(self, base_url='', query='', attempt=0,
force_mobile=False) -> Response: force_mobile=False) -> Response:
"""Sends an outbound request to a URL. Optionally sends the request """Sends an outbound request to a URL. Optionally sends the request
using Tor, if enabled by the user. using Tor, if enabled by the user.
@ -242,7 +286,12 @@ class Request:
'User-Agent': modified_user_agent 'User-Agent': modified_user_agent
} }
# FIXME: Should investigate this further to ensure the consent # 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 # view is suppressed correctly
now = datetime.now() now = datetime.now()
cookies = { cookies = {
@ -261,18 +310,23 @@ class Request:
# Make sure that the tor connection is valid, if enabled # Make sure that the tor connection is valid, if enabled
if self.tor: if self.tor:
tor_check = requests.get('https://check.torproject.org/', try:
proxies=self.proxies, headers=headers) tor_check = requests.get('https://check.torproject.org/',
self.tor_valid = 'Congratulations' in tor_check.text 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( raise TorError(
"Tor connection succeeded, but the connection could not " "Error raised during Tor connection validation",
"be validated by torproject.org",
disable=True) disable=True)
response = requests.get( response = requests.get(
base_url + query, (base_url or self.search_url) + query,
proxies=self.proxies, proxies=self.proxies,
headers=headers, headers=headers,
cookies=cookies) cookies=cookies)
@ -282,6 +336,6 @@ class Request:
attempt += 1 attempt += 1
if attempt > 10: if attempt > 10:
raise TorError("Tor query failed -- max attempts exceeded 10") raise TorError("Tor query failed -- max attempts exceeded 10")
return self.send(base_url, query, attempt) return self.send((base_url or self.search_url), query, attempt)
return response return response

View File

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

2
app/static/build/.gitignore vendored Normal file
View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
/* LIGHT THEME COLORS */ /* LIGHT THEME COLORS */
--whoogle-logo: #685e79; --whoogle-logo: #685e79;
--whoogle-page-bg: #ffffff; --whoogle-page-bg: #ffffff;
--whoogle-element-bg: #685e79; --whoogle-element-bg: #4285f4;
--whoogle-text: #000000; --whoogle-text: #000000;
--whoogle-contrast-text: #ffffff; --whoogle-contrast-text: #ffffff;
--whoogle-secondary-text: #70757a; --whoogle-secondary-text: #70757a;
@ -11,18 +11,44 @@
--whoogle-result-title: #1967d2; --whoogle-result-title: #1967d2;
--whoogle-result-url: #0d652d; --whoogle-result-url: #0d652d;
--whoogle-result-visited: #4b11a8; --whoogle-result-visited: #4b11a8;
--whoogle-divider: #dfe1e5;
/* DARK THEME COLORS */ /* DARK THEME COLORS */
--whoogle-dark-logo: #888888; --whoogle-dark-logo: #685e79;
--whoogle-dark-page-bg: #080808; --whoogle-dark-page-bg: #101020;
--whoogle-dark-element-bg: #111111; --whoogle-dark-element-bg: #4285f4;
--whoogle-dark-text: #dddddd; --whoogle-dark-text: #ffffff;
--whoogle-dark-contrast-text: #aaaaaa; --whoogle-dark-contrast-text: #ffffff;
--whoogle-dark-secondary-text: #8a8b8c; --whoogle-dark-secondary-text: #bbbbbb;
--whoogle-dark-result-bg: #111111; --whoogle-dark-result-bg: #212131;
--whoogle-dark-result-title: #dddddd; --whoogle-dark-result-title: #64a7f6;
--whoogle-dark-result-url: #eceff4; --whoogle-dark-result-url: #34a853;
--whoogle-dark-result-visited: #959595; --whoogle-dark-result-visited: #bbbbff;
--whoogle-dark-divider: #111111; }
#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;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
{"name": "Bulgarian (български)", "value": "lang_bg"}, {"name": "Bulgarian (български)", "value": "lang_bg"},
{"name": "Catalan (Català)", "value": "lang_ca"}, {"name": "Catalan (Català)", "value": "lang_ca"},
{"name": "Chinese, Simplified (简体中文)", "value": "lang_zh-CN"}, {"name": "Chinese, Simplified (简体中文)", "value": "lang_zh-CN"},
{"name": "Chinese, Traditional (繁体中文)", "value": "lang_zh-TW"}, {"name": "Chinese, Traditional (正體中文)", "value": "lang_zh-TW"},
{"name": "Croatian (Hrvatski)", "value": "lang_hr"}, {"name": "Croatian (Hrvatski)", "value": "lang_hr"},
{"name": "Czech (čeština)", "value": "lang_cs"}, {"name": "Czech (čeština)", "value": "lang_cs"},
{"name": "Danish (Dansk)", "value": "lang_da"}, {"name": "Danish (Dansk)", "value": "lang_da"},
@ -28,15 +28,17 @@
{"name": "Italian (Italiano)", "value": "lang_it"}, {"name": "Italian (Italiano)", "value": "lang_it"},
{"name": "Japanese (日本語)", "value": "lang_ja"}, {"name": "Japanese (日本語)", "value": "lang_ja"},
{"name": "Korean (한국어)", "value": "lang_ko"}, {"name": "Korean (한국어)", "value": "lang_ko"},
{"name": "Kurdish (Kurdî)", "value": "lang_ku"},
{"name": "Latvian (Latvietis)", "value": "lang_lv"}, {"name": "Latvian (Latvietis)", "value": "lang_lv"},
{"name": "Lithuanian (Lietuvis)", "value": "lang_lt"}, {"name": "Lithuanian (Lietuvis)", "value": "lang_lt"},
{"name": "Norwegian (Norwegian)", "value": "lang_no"}, {"name": "Norwegian (Norwegian)", "value": "lang_no"},
{"name": "Persian (فارسی)", "value": "lang_fa"}, {"name": "Persian (فارسی)", "value": "lang_fa"},
{"name": "Polish (Polskie)", "value": "lang_pl"}, {"name": "Polish (Polskie)", "value": "lang_pl"},
{"name": "Portugese (Português)", "value": "lang_pt"}, {"name": "Portuguese (Português)", "value": "lang_pt"},
{"name": "Romanian (Română)", "value": "lang_ro"}, {"name": "Romanian (Română)", "value": "lang_ro"},
{"name": "Russian (русский)", "value": "lang_ru"}, {"name": "Russian (русский)", "value": "lang_ru"},
{"name": "Serbian (Српски)", "value": "lang_sr"}, {"name": "Serbian (Српски)", "value": "lang_sr"},
{"name": "Sinhala (සිංහල)", "value": "lang_si"},
{"name": "Slovak (Slovák)", "value": "lang_sk"}, {"name": "Slovak (Slovák)", "value": "lang_sk"},
{"name": "Slovenian (Slovenščina)", "value": "lang_sl"}, {"name": "Slovenian (Slovenščina)", "value": "lang_sl"},
{"name": "Spanish (Español)", "value": "lang_es"}, {"name": "Spanish (Español)", "value": "lang_es"},
@ -44,6 +46,8 @@
{"name": "Swedish (Svenska)", "value": "lang_sv"}, {"name": "Swedish (Svenska)", "value": "lang_sv"},
{"name": "Thai (ไทย)", "value": "lang_th"}, {"name": "Thai (ไทย)", "value": "lang_th"},
{"name": "Turkish (Türk)", "value": "lang_tr"}, {"name": "Turkish (Türk)", "value": "lang_tr"},
{"name": "Ukranian (Український)", "value": "lang_uk"}, {"name": "Ukrainian (Український)", "value": "lang_uk"},
{"name": "Vietnamese (Tiếng Việt)", "value": "lang_vi"} {"name": "Vietnamese (Tiếng Việt)", "value": "lang_vi"},
{"name": "Xhosa (isiXhosa)", "value": "lang_xh"},
{"name": "Zulu (isiZulu)", "value": "lang_zu"}
] ]

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,181 +1,260 @@
<html> <html style="background: #000;">
<head> <head>
<link rel="apple-touch-icon" sizes="57x57" href="static/img/favicon/apple-icon-57x57.png"> <link rel="apple-touch-icon" sizes="57x57" href="static/img/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="static/img/favicon/apple-icon-60x60.png"> <link rel="apple-touch-icon" sizes="60x60" href="static/img/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="static/img/favicon/apple-icon-72x72.png"> <link rel="apple-touch-icon" sizes="72x72" href="static/img/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="static/img/favicon/apple-icon-76x76.png"> <link rel="apple-touch-icon" sizes="76x76" href="static/img/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="static/img/favicon/apple-icon-114x114.png"> <link rel="apple-touch-icon" sizes="114x114" href="static/img/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="static/img/favicon/apple-icon-120x120.png"> <link rel="apple-touch-icon" sizes="120x120" href="static/img/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="static/img/favicon/apple-icon-144x144.png"> <link rel="apple-touch-icon" sizes="144x144" href="static/img/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="static/img/favicon/apple-icon-152x152.png"> <link rel="apple-touch-icon" sizes="152x152" href="static/img/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="static/img/favicon/apple-icon-180x180.png"> <link rel="apple-touch-icon" sizes="180x180" href="static/img/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="static/img/favicon/android-icon-192x192.png"> <link rel="icon" type="image/png" sizes="192x192" href="static/img/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/img/favicon/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="static/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="static/img/favicon/favicon-96x96.png"> <link rel="icon" type="image/png" sizes="96x96" href="static/img/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/img/favicon/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="static/img/favicon/favicon-16x16.png">
<link rel="manifest" href="static/img/favicon/manifest.json"> <link rel="manifest" href="static/img/favicon/manifest.json">
<meta name="referrer" content="no-referrer"> <meta name="referrer" content="no-referrer">
<meta name="msapplication-TileColor" content="#ffffff"> <meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="static/img/favicon/ms-icon-144x144.png"> <meta name="msapplication-TileImage" content="static/img/favicon/ms-icon-144x144.png">
<script type="text/javascript" src="static/js/autocomplete.js"></script> {% if autocomplete_enabled == '1' %}
<script type="text/javascript" src="static/js/controller.js"></script> <script src="{{ cb_url('autocomplete.js') }}"></script>
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search"> {% endif %}
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <script type="text/javascript" src="{{ cb_url('controller.js') }}"></script>
<link rel="stylesheet" href="static/css/variables.css"> <link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
<link rel="stylesheet" href="static/css/main.css"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="static/css/{{ 'dark' if config.dark else 'light' }}-theme.css"/> <link rel="stylesheet" href="{{ cb_url('logo.css') }}">
<noscript> {% if config.theme %}
<style> {% if config.theme == 'system' %}
#main { display: inherit !important; } <style>
.content { max-height: 720px; padding: 18px; border-radius: 10px; } @import "{{ cb_url('light-theme.css') }}" screen;
.collapsible { display: none; } @import "{{ cb_url('dark-theme.css') }}" screen and (prefers-color-scheme: dark);
</style> </style>
</noscript> {% else %}
<style>{{ config.style }}</style> <link rel="stylesheet" href="{{ cb_url(config.theme + '-theme.css') }}"/>
<title>Whoogle Search</title> {% endif %}
</head> {% else %}
<body id="main" style="display: none; background-color: {{ '#000' if config.dark else '#fff' }}"> <link rel="stylesheet" href="{{ cb_url(('dark' if config.dark else 'light') + '-theme.css') }}"/>
<div class="search-container"> {% endif %}
<div class="logo-container"> <link rel="stylesheet" href="{{ cb_url('main.css') }}">
{{ logo|safe }} <noscript>
</div> <style>
<form id="search-form" action="search" method="{{ 'get' if config.get_only else 'post' }}"> #main {
<div class="search-fields"> display: inherit !important;
<div class="autocomplete"> }
<input
type="text" .content {
name="q" max-height: 400px;
id="search-bar" padding: 18px;
class="home-search" border-radius: 10px;
autofocus="autofocus" overflow-y: scroll;
autocapitalize="none" }
spellcheck="false"
autocorrect="off" .collapsible {
autocomplete="off"> display: none;
</div> }
<input type="submit" id="search-submit" value="{{ translation['search'] }}"> </style>
</div> </noscript>
</form> <style>{{ config.style }}</style>
{% if not config_disabled %} <title>Whoogle Search</title>
<br/> </head>
<button id="config-collapsible" class="collapsible">{{ translation['config'] }}</button> <body id="main">
<div class="content"> <div class="search-container">
<div class="config-fields"> <div class="logo-container">
<form id="config-form" action="config" method="post"> {{ logo|safe }}
<div class="config-div config-div-ctry"> </div>
<label for="config-ctry">{{ translation['config-country'] }}: </label> <form id="search-form" action="search" method="{{ 'get' if config.get_only else 'post' }}">
<select name="ctry" id="config-ctry"> <div class="search-fields">
{% for ctry in countries %} <div class="autocomplete">
<option value="{{ ctry.value }}" {% if config.preferences %}
{% if ctry.value in config.ctry %} <input type="hidden" name="preferences" value="{{ config.preferences }}" />
selected {% endif %}
{% endif %}> <input
{{ ctry.name }} type="text"
</option> name="q"
{% endfor %} id="search-bar"
</select> class="home-search"
<div><span class="info-text"> — {{ translation['config-country-help'] }}</span></div> autofocus="autofocus"
</div> autocapitalize="none"
<div class="config-div config-div-lang"> spellcheck="false"
<label for="config-lang-interface">{{ translation['config-lang'] }}: </label> autocorrect="off"
<select name="lang_interface" id="config-lang-interface"> autocomplete="off"
{% for lang in languages %} dir="auto">
<option value="{{ lang.value }}" </div>
{% if lang.value in config.lang_interface %} <input type="submit" id="search-submit" value="{{ translation['search'] }}">
selected </div>
{% endif %}> </form>
{{ lang.name }} {% if not config_disabled %}
</option> <br/>
{% endfor %} <button id="config-collapsible" class="collapsible">{{ translation['config'] }}</button>
</select> <div class="content">
</div> <div class="config-fields">
<div class="config-div config-div-search-lang"> <form id="config-form" action="config" method="post">
<label for="config-lang-search">{{ translation['config-lang-search'] }}: </label> <div class="config-options">
<select name="lang_search" id="config-lang-search"> <div class="config-div config-div-country">
{% for lang in languages %} <label for="config-country">{{ translation['config-country'] }}: </label>
<option value="{{ lang.value }}" <select name="country" id="config-country">
{% if lang.value in config.lang_search %} {% for country in countries %}
selected <option value="{{ country.value }}"
{% endif %}> {% if (
{{ lang.name }} config.country != '' and config.country in country.value
</option> ) or (
{% endfor %} config.country == '' and country.value == '')
</select> %}
</div> selected
<div class="config-div config-div-near"> {% endif %}>
<label for="config-near">{{ translation['config-near'] }}: </label> {{ country.name }}
<input type="text" name="near" id="config-near" placeholder="City Name" value="{{ config.near }}"> </option>
</div> {% endfor %}
<div class="config-div config-div-block"> </select>
<label for="config-block">{{ translation['config-block'] }}: </label> </div>
<input type="text" name="block" id="config-block" placeholder="Comma-separated site list" value="{{ config.block }}"> <div class="config-div config-div-lang">
</div> <label for="config-lang-interface">{{ translation['config-lang'] }}: </label>
<div class="config-div config-div-nojs"> <select name="lang_interface" id="config-lang-interface">
<label for="config-nojs">{{ translation['config-nojs'] }}: </label> {% for lang in languages %}
<input type="checkbox" name="nojs" id="config-nojs" {{ 'checked' if config.nojs else '' }}> <option value="{{ lang.value }}"
</div> {% if lang.value in config.lang_interface %}
<div class="config-div config-div-dark"> selected
<label for="config-dark">{{ translation['config-dark'] }}: </label> {% endif %}>
<input type="checkbox" name="dark" id="config-dark" {{ 'checked' if config.dark else '' }}> {{ lang.name }}
</div> </option>
<div class="config-div config-div-safe"> {% endfor %}
<label for="config-safe">{{ translation['config-safe'] }}: </label> </select>
<input type="checkbox" name="safe" id="config-safe" {{ 'checked' if config.safe else '' }}> </div>
</div> <div class="config-div config-div-search-lang">
<div class="config-div config-div-alts"> <label for="config-lang-search">{{ translation['config-lang-search'] }}: </label>
<label class="tooltip" for="config-alts">{{ translation['config-alts'] }}: </label> <select name="lang_search" id="config-lang-search">
<input type="checkbox" name="alts" id="config-alts" {{ 'checked' if config.alts else '' }}> {% for lang in languages %}
<div><span class="info-text"> — {{ translation['config-alts-help'] }}</span></div> <option value="{{ lang.value }}"
</div> {% if lang.value in config.lang_search %}
<div class="config-div config-div-new-tab"> selected
<label for="config-new-tab">{{ translation['config-new-tab'] }}: </label> {% endif %}>
<input type="checkbox" name="new_tab" id="config-new-tab" {{ 'checked' if config.new_tab else '' }}> {{ lang.name }}
</div> </option>
<div class="config-div config-div-view-image"> {% endfor %}
<label for="config-view-image">{{ translation['config-images'] }}: </label> </select>
<input type="checkbox" name="view_image" id="config-view-image" {{ 'checked' if config.view_image else '' }}> </div>
<div><span class="info-text"> — {{ translation['config-images-help'] }}</span></div> <div class="config-div config-div-near">
</div> <label for="config-near">{{ translation['config-near'] }}: </label>
<div class="config-div config-div-tor"> <input type="text" name="near" id="config-near"
<label for="config-tor">{{ translation['config-tor'] }}: {{ '' if tor_available else 'Unavailable' }}</label> placeholder="{{ translation['config-near-help'] }}" value="{{ config.near }}">
<input type="checkbox" name="tor" id="config-tor" {{ '' if tor_available else 'hidden' }} {{ 'checked' if config.tor else '' }}> </div>
</div> <div class="config-div config-div-block">
<div class="config-div config-div-get-only"> <label for="config-block">{{ translation['config-block'] }}: </label>
<label for="config-get-only">{{ translation['config-get-only'] }}: </label> <input type="text" name="block" id="config-block"
<input type="checkbox" name="get_only" id="config-get-only" {{ 'checked' if config.get_only else '' }}> placeholder="{{ translation['config-block-help'] }}" value="{{ config.block }}">
</div> </div>
<div class="config-div config-div-root-url"> <div class="config-div config-div-block">
<label for="config-url">{{ translation['config-url'] }}: </label> <label for="config-block-title">{{ translation['config-block-title'] }}: </label>
<input type="text" name="url" id="config-url" value="{{ config.url }}"> <input type="text" name="block_title" id="config-block"
</div> placeholder="{{ translation['config-block-title-help'] }}"
<div class="config-div config-div-custom-css"> value="{{ config.block_title }}">
<label for="config-style">{{ translation['config-css'] }}:</label> </div>
<textarea <div class="config-div config-div-block">
name="style" <label for="config-block-url">{{ translation['config-block-url'] }}: </label>
id="config-style" <input type="text" name="block_url" id="config-block"
autocapitalize="off" placeholder="{{ translation['config-block-url-help'] }}" value="{{ config.block_url }}">
autocomplete="off" </div>
spellcheck="false" <div class="config-div config-div-anon-view">
autocorrect="off" <label for="config-anon-view">{{ translation['config-anon-view'] }}: </label>
value=""> <input type="checkbox" name="anon_view" id="config-anon-view" {{ 'checked' if config.anon_view else '' }}>
{{ config.style.replace('\t', '') }} </div>
</textarea> <div class="config-div config-div-nojs">
</div> <label for="config-nojs">{{ translation['config-nojs'] }}: </label>
<div class="config-div"> <input type="checkbox" name="nojs" id="config-nojs" {{ 'checked' if config.nojs else '' }}>
<input type="submit" id="config-load" value="{{ translation['load'] }}">&nbsp; </div>
<input type="submit" id="config-submit" value="{{ translation['apply'] }}">&nbsp; <div class="config-div config-div-theme">
<input type="submit" id="config-save" value="{{ translation['save-as'] }}"> <label for="config-theme">{{ translation['config-theme'] }}: </label>
</div> <select name="theme" id="config-theme">
</form> {% for theme in themes %}
</div> <option value="{{ theme }}"
</div> {% if theme in config.theme %}
{% endif %} selected
</div> {% endif %}>
<footer> {{ translation[theme].capitalize() }}
<p style="color: {{ 'var(--whoogle-dark-text)' if config.dark else 'var(--whoogle-text)' }};"> </option>
Whoogle Search v{{ version_number }} || {% endfor %}
<a id="gh-link" href="https://github.com/benbusby/whoogle-search">{{ translation['github-link'] }}</a> </select>
</p> </div>
</footer> <!-- DEPRECATED -->
</body> <!--<div class="config-div config-div-dark">-->
<!--<label for="config-dark">{{ translation['config-dark'] }}: </label>-->
<!--<input type="checkbox" name="dark" id="config-dark" {{ 'checked' if config.dark else '' }}>-->
<!--</div>-->
<div class="config-div config-div-safe">
<label for="config-safe">{{ translation['config-safe'] }}: </label>
<input type="checkbox" name="safe" id="config-safe" {{ 'checked' if config.safe else '' }}>
</div>
<div class="config-div config-div-alts">
<label class="tooltip" for="config-alts">{{ translation['config-alts'] }}: </label>
<input type="checkbox" name="alts" id="config-alts" {{ 'checked' if config.alts else '' }}>
<div><span class="info-text"> — {{ translation['config-alts-help'] }}</span></div>
</div>
<div class="config-div config-div-new-tab">
<label for="config-new-tab">{{ translation['config-new-tab'] }}: </label>
<input type="checkbox" name="new_tab"
id="config-new-tab" {{ 'checked' if config.new_tab else '' }}>
</div>
<div class="config-div config-div-view-image">
<label for="config-view-image">{{ translation['config-images'] }}: </label>
<input type="checkbox" name="view_image"
id="config-view-image" {{ 'checked' if config.view_image else '' }}>
<div><span class="info-text"> — {{ translation['config-images-help'] }}</span></div>
</div>
<div class="config-div config-div-tor">
<label for="config-tor">{{ translation['config-tor'] }}: {{ '' if tor_available else 'Unavailable' }}</label>
<input type="checkbox" name="tor"
id="config-tor" {{ '' if tor_available else 'hidden' }} {{ 'checked' if config.tor else '' }}>
</div>
<div class="config-div config-div-get-only">
<label for="config-get-only">{{ translation['config-get-only'] }}: </label>
<input type="checkbox" name="get_only"
id="config-get-only" {{ 'checked' if config.get_only else '' }}>
</div>
<div class="config-div config-div-accept-language">
<label for="config-accept-language">Set Accept-Language: </label>
<input type="checkbox" name="accept_language"
id="config-accept-language" {{ 'checked' if config.accept_language else '' }}>
</div>
<div class="config-div config-div-root-url">
<label for="config-url">{{ translation['config-url'] }}: </label>
<input type="text" name="url" id="config-url" value="{{ config.url }}">
</div>
<div class="config-div config-div-custom-css">
<a id="css-link"
href="https://github.com/benbusby/whoogle-search/wiki/User-Contributed-CSS-Themes">
{{ translation['config-css'] }}:
</a>
<textarea
name="style"
id="config-style"
autocapitalize="off"
autocomplete="off"
spellcheck="false"
autocorrect="off"
value="">
{{ config.style.replace('\t', '') }}
</textarea>
</div>
<div class="config-div config-div-pref-url">
<label for="config-pref-encryption">{{ translation['config-pref-encryption'] }}: </label>
<input type="checkbox" name="preferences_encrypted"
id="config-pref-encryption" {{ 'checked' if config.preferences_encrypted and config.preferences_key else '' }}>
<div><span class="info-text"> — {{ translation['config-pref-help'] }}</span></div>
<label for="config-pref-url">{{ translation['config-pref-url'] }}: </label>
<input type="text" name="pref-url" id="config-pref-url" value="{{ config.url }}?preferences={{ config.preferences }}">
</div>
</div>
<div class="config-div config-buttons">
<input type="submit" id="config-load" value="{{ translation['load'] }}">&nbsp;
<input type="submit" id="config-submit" value="{{ translation['apply'] }}">&nbsp;
<input type="submit" id="config-save" value="{{ translation['save-as'] }}">
</div>
</form>
</div>
</div>
{% endif %}
</div>
{% include 'footer.html' %}
</body>
</html> </html>

View File

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

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,5 +1,6 @@
import json import json
import requests import requests
import urllib.parse as urlparse
DDG_BANGS = 'https://duckduckgo.com/bang.v255.js' DDG_BANGS = 'https://duckduckgo.com/bang.v255.js'
@ -35,6 +36,7 @@ def gen_bangs_json(bangs_file: str) -> None:
} }
json.dump(bangs_data, open(bangs_file, 'w')) json.dump(bangs_data, open(bangs_file, 'w'))
print('* Finished creating ddg bangs json')
def resolve_bang(query: str, bangs_dict: dict) -> str: def resolve_bang(query: str, bangs_dict: dict) -> str:
@ -51,11 +53,38 @@ def resolve_bang(query: str, bangs_dict: dict) -> str:
wasn't a match or didn't contain a bang operator wasn't a match or didn't contain a bang operator
""" """
split_query = query.split(' ')
for operator in bangs_dict.keys():
if operator not in split_query:
continue
return bangs_dict[operator]['url'].format( #if ! not in query simply return (speed up processing)
query.replace(operator, '').strip()) 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 '' return ''

72
app/utils/misc.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

BIN
docs/screenshot_desktop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

BIN
docs/screenshot_mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

0
letsencrypt/acme.json Normal file
View File

13
misc/instances.txt Normal file
View File

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

1
misc/tor/control.conf Normal file
View File

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

View File

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

View File

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

View File

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

24
run
View File

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

39
setup.cfg Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,24 @@
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from app import app from app import app
from app.models.endpoint import Endpoint
from app.utils.session import generate_user_key, valid_user_session from app.utils.session import generate_user_key, valid_user_session
JAPAN_PREFS = 'uG-gGIJwHdqxl6DrS3mnu_511HlQcRpxYlG03Xs-' \
+ '_znXNiJWI9nLOkRLkiiFwIpeUYMTGfUF5-t9fP5DGmzDLEt04DCx703j3nPf' \
+ '29v_RWkU7gXw_44m2oAFIaKGmYlu4Z0bKyu9k5WXfL9Dy6YKKnpcR5CiaFsG' \
+ 'rccNRkAPYm-eYGAFUV8M59f8StsGd_M-gHKGS9fLok7EhwBWjHxBJ2Kv8hsT' \
+ '87zftP2gMJOevTdNnezw2Y5WOx-ZotgeheCW1BYCFcRqatlov21PHp22NGVG' \
+ '8ZuBNAFW0bE99WSdyT7dUIvzeWCLJpbdSsq-3FUUZkxbRdFYlGd8vY1UgVAp' \
+ 'OSie2uAmpgLFXygO-VfNBBZ68Q7gAap2QtzHCiKD5cFYwH3LPgVJ-DoZvJ6k' \
+ 'alt34TaYiJphgiqFKV4SCeVmLWTkr0SF3xakSR78yYJU_d41D2ng-TojA9XZ' \
+ 'uR2ZqjSvPKOWvjimu89YhFOgJxG1Po8Henj5h9OL9VXXvdvlJwBSAKw1E3FV' \
+ '7UHWiglMxPblfxqou1cYckMYkFeIMCD2SBtju68mBiQh2k328XRPTsQ_ocby' \
+ 'cgVKnleGperqbD6crRk3Z9xE5sVCjujn9JNVI-7mqOITMZ0kntq9uJ3R5n25' \
+ 'Vec0TJ0P19nEtvjY0nJIrIjtnBg=='
def test_generate_user_keys(): def test_generate_user_keys():
key = generate_user_key() key = generate_user_key()
assert Fernet(key) assert Fernet(key)
@ -37,14 +52,27 @@ def test_query_decryption(client):
rv = client.get('/') rv = client.get('/')
cookie = rv.headers['Set-Cookie'] cookie = rv.headers['Set-Cookie']
rv = client.get('/search?q=test+1', headers={'Cookie': cookie}) rv = client.get(f'/{Endpoint.search}?q=test+1', headers={'Cookie': cookie})
assert rv._status_code == 200 assert rv._status_code == 200
with client.session_transaction() as session: with client.session_transaction() as session:
assert valid_user_session(session) assert valid_user_session(session)
rv = client.get('/search?q=test+2', headers={'Cookie': cookie}) rv = client.get(f'/{Endpoint.search}?q=test+2', headers={'Cookie': cookie})
assert rv._status_code == 200 assert rv._status_code == 200
with client.session_transaction() as session: with client.session_transaction() as session:
assert valid_user_session(session) assert valid_user_session(session)
def test_prefs_url(client):
base_url = f'/{Endpoint.search}?q=wikipedia'
rv = client.get(base_url)
assert rv._status_code == 200
assert b'wikipedia.org' in rv.data
assert b'ja.wikipedia.org' not in rv.data
rv = client.get(f'{base_url}&preferences={JAPAN_PREFS}')
assert rv._status_code == 200
assert b'ja.wikipedia.org' in rv.data

View File

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

View File

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

View File

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