{{- result_open_link(result.url, "url_wrapper") -}}
+ {% if not rtl %}
+ {%- if favicon_resolver != "" %}
+
+
+
+ {%- endif -%}
+ {%- endif -%}
{%- for part in get_pretty_url(result.parsed_url) -%}
{{- part -}}
{%- endfor %}
+ {% if rtl %}
+ {%- if favicon_resolver != "" %}
+
+
+
+ {%- endif -%}
+ {%- endif -%}
{{- result_close_link() -}}
{%- if result.thumbnail %}{{ result_open_link(result.url) }}{{ result_close_link() }}{% endif -%}
{{ result_link(result.url, result.title|safe) }}
diff --git a/searx/templates/simple/preferences.html b/searx/templates/simple/preferences.html
index 825a98fe2..bc96e1198 100644
--- a/searx/templates/simple/preferences.html
+++ b/searx/templates/simple/preferences.html
@@ -173,6 +173,9 @@
{%- if 'autocomplete' not in locked_preferences -%}
{%- include 'simple/preferences/autocomplete.html' -%}
{%- endif -%}
+ {%- if 'favicon' not in locked_preferences -%}
+ {%- include 'simple/preferences/favicon.html' -%}
+ {%- endif -%}
{% if 'safesearch' not in locked_preferences %}
{%- include 'simple/preferences/safesearch.html' -%}
{%- endif -%}
diff --git a/searx/templates/simple/preferences/favicon.html b/searx/templates/simple/preferences/favicon.html
new file mode 100644
index 000000000..207bf2a24
--- /dev/null
+++ b/searx/templates/simple/preferences/favicon.html
@@ -0,0 +1,17 @@
+{{- '' -}}
diff --git a/searx/webapp.py b/searx/webapp.py
index dd79defcb..8046dc392 100755
--- a/searx/webapp.py
+++ b/searx/webapp.py
@@ -123,6 +123,7 @@ from searx.locales import (
# renaming names from searx imports ...
from searx.autocomplete import search_autocomplete, backends as autocomplete_backends
+from searx.favicon_resolver import search_favicon, backends as favicon_backends
from searx.redisdb import initialize as redis_initialize
from searx.sxng_locales import sxng_locales
from searx.search import SearchWithPlugins, initialize as search_initialize
@@ -297,6 +298,24 @@ def morty_proxify(url: str):
return '{0}?{1}'.format(settings['result_proxy']['url'], urlencode(url_params))
+def favicon_proxify(url: str):
+ # url is a FQDN (e.g. example.com, en.wikipedia.org)
+
+ resolver = request.preferences.get_value('favicon_resolver')
+
+ # if resolver is empty, just return nothing
+ if not resolver:
+ return ""
+
+ # check resolver is valid
+ if resolver not in favicon_backends:
+ return ""
+
+ h = new_hmac(settings['server']['secret_key'], url.encode())
+
+ return '{0}?{1}'.format(url_for('favicon_proxy'), urlencode(dict(q=url.encode(), h=h)))
+
+
def image_proxify(url: str):
if url.startswith('//'):
@@ -358,6 +377,7 @@ def get_client_settings():
return {
'autocomplete_provider': req_pref.get_value('autocomplete'),
'autocomplete_min': get_setting('search.autocomplete_min'),
+ 'favicon_resolver': req_pref.get_value('favicon_resolver'),
'http_method': req_pref.get_value('method'),
'infinite_scroll': req_pref.get_value('infinite_scroll'),
'translations': get_translations(),
@@ -388,6 +408,7 @@ def render(template_name: str, **kwargs):
# values from the preferences
kwargs['preferences'] = request.preferences
kwargs['autocomplete'] = request.preferences.get_value('autocomplete')
+ kwargs['favicon_resolver'] = request.preferences.get_value('favicon_resolver')
kwargs['infinite_scroll'] = request.preferences.get_value('infinite_scroll')
kwargs['search_on_category_select'] = request.preferences.get_value('search_on_category_select')
kwargs['hotkeys'] = request.preferences.get_value('hotkeys')
@@ -431,6 +452,7 @@ def render(template_name: str, **kwargs):
# helpers to create links to other pages
kwargs['url_for'] = custom_url_for # override url_for function in templates
kwargs['image_proxify'] = image_proxify
+ kwargs['favicon_proxify'] = favicon_proxify
kwargs['proxify'] = morty_proxify if settings['result_proxy']['url'] is not None else None
kwargs['proxify_results'] = settings['result_proxy']['proxify_results']
kwargs['cache_url'] = settings['ui']['cache_url']
@@ -873,6 +895,42 @@ def autocompleter():
return Response(suggestions, mimetype=mimetype)
+@app.route('/favicon', methods=['GET'])
+def favicon_proxy():
+ """Return proxied favicon results"""
+ url = request.args.get('q')
+
+ # malformed request
+ if not url:
+ return '', 400
+
+ # malformed request / does not have authorisation
+ if not is_hmac_of(settings['server']['secret_key'], url.encode(), request.args.get('h', '')):
+ return '', 400
+
+ resolver = request.preferences.get_value('favicon_resolver')
+
+ # check if the favicon resolver is valid
+ if not resolver or resolver not in favicon_backends:
+ return '', 400
+
+ # parse query
+ raw_text_query = RawTextQuery(url, [])
+
+ resp = search_favicon(resolver, raw_text_query)
+
+ # return 404 if the favicon is not found
+ if not resp:
+ theme = request.preferences.get_value("theme")
+ # return favicon from /static/themes/simple/img/empty_favicon.svg
+ # we can't rely on an onerror event in the img tag to display a default favicon as this violates the CSP.
+ # using redirect to save network bandwidth (user will have this location cached).
+ return redirect(url_for('static', filename='themes/' + theme + '/img/empty_favicon.svg'))
+
+ # will always return a PNG image
+ return Response(resp, mimetype='image/png')
+
+
@app.route('/preferences', methods=['GET', 'POST'])
def preferences():
"""Render preferences page && save user preferences"""
@@ -1020,6 +1078,7 @@ def preferences():
],
disabled_engines = disabled_engines,
autocomplete_backends = autocomplete_backends,
+ favicon_backends = favicon_backends,
shortcuts = {y: x for x, y in engine_shortcuts.items()},
themes = themes,
plugins = plugins,
diff --git a/tests/unit/settings/user_settings.yml b/tests/unit/settings/user_settings.yml
index c4c4d74ef..ed82c450a 100644
--- a/tests/unit/settings/user_settings.yml
+++ b/tests/unit/settings/user_settings.yml
@@ -5,6 +5,7 @@ general:
search:
safe_search: 0
autocomplete: ""
+ favicon_resolver: ""
default_lang: ""
ban_time_on_fail: 5
max_ban_time_on_fail: 120