diff --git a/searx/settings.yml b/searx/settings.yml
index a82a3432d..b281ea314 100644
--- a/searx/settings.yml
+++ b/searx/settings.yml
@@ -14,6 +14,9 @@ general:
enable_metrics: true
brand:
+ # Both favicon and logo are relative to SearXNG's root path unless they are absolute paths
+ favicon: static/themes/simple/img/favicon.png
+ logo: static/themes/simple/img/searxng.png
new_issue_url: https://github.com/searxng/searxng/issues/new
docs_url: https://docs.searxng.org/
public_instances: https://searx.space
diff --git a/searx/settings_defaults.py b/searx/settings_defaults.py
index 7f657aa54..f66155983 100644
--- a/searx/settings_defaults.py
+++ b/searx/settings_defaults.py
@@ -146,6 +146,8 @@ SCHEMA = {
'enable_metrics': SettingsValue(bool, True),
},
'brand': {
+ 'favicon': SettingsValue(str, "static/themes/simple/img/favicon.png"),
+ 'logo': SettingsValue(str, "static/themes/simple/img/searxg.png"),
'issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues'),
'new_issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues/new'),
'docs_url': SettingsValue(str, 'https://docs.searxng.org'),
diff --git a/searx/templates/simple/base.html b/searx/templates/simple/base.html
index a31ff07ee..b53987db4 100644
--- a/searx/templates/simple/base.html
+++ b/searx/templates/simple/base.html
@@ -24,8 +24,8 @@
{% block head %}
{% endblock %}
-
-
+
+
diff --git a/searx/templates/simple/page_with_header.html b/searx/templates/simple/page_with_header.html
index d4466b612..a636092ec 100644
--- a/searx/templates/simple/page_with_header.html
+++ b/searx/templates/simple/page_with_header.html
@@ -1,5 +1,5 @@
{% set body_class = "page_with_header" %}
{% extends "simple/base.html" %}
{% block header %}
-
+
{% endblock %}
diff --git a/searx/webapp.py b/searx/webapp.py
index 79255652f..4dd57a703 100755
--- a/searx/webapp.py
+++ b/searx/webapp.py
@@ -294,6 +294,79 @@ def get_result_template(theme_name: str, template_name: str):
return 'result_templates/' + template_name
+def get_favicon_or_logo(imgtype: str):
+ # This method returns a favicon or regular image depending on the imgtype parameter
+ # It is used to not repeat code for /favicon.ico and /logo
+ # Called by logo() and favicon()
+ theme = request.preferences.get_value("theme")
+ path = os.path.expanduser(settings.get("brand").get(imgtype))
+ relpath = os.path.join(app.root_path, path)
+ mimetype = 'image/vnd.microsoft.icon' if imgtype == "favicon" else None
+ fallback = send_from_directory(
+ os.path.join(app.root_path, settings['ui']['static_path'], 'themes', theme, 'img'), # pyright: ignore
+ 'favicon.png' if imgtype == "favicon" else "searxng.png",
+ mimetype=mimetype,
+ )
+
+ # If path is a URL
+ if "://" in path:
+ resp_ok = False
+ resp = None
+ # TODO: Make a a function for this used by both this function and the image proxy # pylint: disable=fixme
+ # ASAP to avoid repeated code like this
+
+ try:
+ # Pull image from it
+ request_headers = {
+ 'User-Agent': gen_useragent(),
+ 'Accept': 'image/webp,*/*',
+ 'Accept-Encoding': 'gzip, deflate',
+ 'Sec-GPC': '1',
+ 'DNT': '1',
+ }
+ resp, stream = http_stream(method='GET', url=path, headers=request_headers, allow_redirects=True)
+ if resp.status_code != 200:
+ logger.debug(f"{imgtype}: Bad status code %i", resp.status_code)
+ return fallback
+ resp_ok = True
+ except httpx.HTTPError:
+ logger.debug(f"{imgtype}: HTTP error")
+ return fallback
+ finally:
+ if resp and not resp_ok:
+ try:
+ resp.close()
+ except httpx.HTTPError:
+ logger.exception(f'{imgtype}: HTTP error on closing')
+
+ def close_stream():
+ nonlocal resp, stream
+ try:
+ if resp:
+ resp.close()
+ del resp
+ del stream
+ except httpx.HTTPError as e:
+ logger.debug(f'{imgtype}: Exception while closing response', e)
+
+ try:
+ headers = dict_subset(resp.headers, {'Content-Type', 'Content-Encoding', 'Content-Length', 'Length'})
+ response = Response(stream, mimetype=resp.headers['Content-Type'], headers=headers, direct_passthrough=True)
+ response.call_on_close(close_stream)
+ return response
+ except httpx.HTTPError:
+ close_stream()
+ return fallback
+
+ elif not os.path.isfile(path) and not os.path.isfile(relpath):
+ # If path doesn't exist neither relatively nor absolutely, fallback to whatever is in the theme
+ return fallback
+ else:
+ # Otherwise we're good to go, send whatever is specified.
+ # Works with both relative and absolute.
+ return send_from_directory(os.path.dirname(path), os.path.basename(path), mimetype=mimetype)
+
+
def custom_url_for(endpoint: str, **values):
suffix = ""
if endpoint == 'static' and values.get('filename'):
@@ -1286,12 +1359,12 @@ def opensearch():
@app.route('/favicon.ico')
def favicon():
- theme = request.preferences.get_value("theme")
- return send_from_directory(
- os.path.join(app.root_path, settings['ui']['static_path'], 'themes', theme, 'img'), # pyright: ignore
- 'favicon.png',
- mimetype='image/vnd.microsoft.icon',
- )
+ return get_favicon_or_logo("favicon")
+
+
+@app.route('/logo')
+def logo():
+ return get_favicon_or_logo("logo")
@app.route('/clear_cookies')