Compare commits

..

4 Commits

Author SHA1 Message Date
vojkovic 300695de5c [fix] crash when lock is omitted 2026-05-31 01:37:37 +08:00
Markus Heiser bd863f16b1 [build] /static 2026-05-30 22:43:50 +08:00
Markus Heiser 4ac822fd7f [mod] typification of the preference settings
no functional change / except the missing online doc which is now available::

    $ make docs.live
    $ xdg-open "http://127.0.0.1:8000/admin/settings/settings_preferences.html"

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-30 22:43:50 +08:00
vojkovic e1d25c5078 [mod] enable image proxy for public instances 2026-05-30 22:43:50 +08:00
13 changed files with 129 additions and 84 deletions
-1
View File
@@ -5,7 +5,6 @@ import type { KeyBindingLayout } from "./main/keyboard.ts";
// synced with searx/webapp.py get_client_settings // synced with searx/webapp.py get_client_settings
type Settings = { type Settings = {
plugins?: string[]; plugins?: string[];
advanced_search?: boolean;
autocomplete?: string; autocomplete?: string;
autocomplete_min?: number; autocomplete_min?: number;
doi_resolver?: string; doi_resolver?: string;
+1
View File
@@ -19,6 +19,7 @@ Settings
settings_search settings_search
settings_server settings_server
settings_ui settings_ui
settings_preferences
settings_redis settings_redis
settings_valkey settings_valkey
settings_outgoing settings_outgoing
@@ -0,0 +1,8 @@
.. _settings preferences:
================
``preferences:``
================
.. autoclass:: searx._settings.SettingsPref
:members:
+1
View File
@@ -47,6 +47,7 @@
activated: activated:
- :py:obj:`searx.botdetection.link_token` in the :ref:`limiter` - :py:obj:`searx.botdetection.link_token` in the :ref:`limiter`
- :ref:`image_proxy`
.. _image_proxy: .. _image_proxy:
+8 -1
View File
@@ -10,6 +10,7 @@ from os.path import dirname, abspath
import logging import logging
import msgspec import msgspec
from ._settings import SettingsPref
# Debug # Debug
LOG_FORMAT_DEBUG: str = '%(levelname)-7s %(name)-30.30s: %(message)s' LOG_FORMAT_DEBUG: str = '%(levelname)-7s %(name)-30.30s: %(message)s'
@@ -47,6 +48,12 @@ def init_settings():
settings.clear() settings.clear()
settings.update(cfg) settings.update(cfg)
if get_setting("server.public_instance"):
# enable image proxy for public instances #6125
settings["server"]["image_proxy"] = True
pref: SettingsPref = get_setting("preferences")
pref.lock.add("image_proxy")
sxng_debug = get_setting("general.debug") sxng_debug = get_setting("general.debug")
if sxng_debug: if sxng_debug:
_logging_config_debug() _logging_config_debug()
@@ -66,7 +73,7 @@ def init_settings():
if settings['server']['public_instance']: if settings['server']['public_instance']:
logger.warning( logger.warning(
"Be aware you have activated features intended only for public instances. " "Be aware you have activated features intended only for public instances. "
"This force the usage of the limiter and link_token / " "This force the usage of the limiter, link_token and image proxy / "
"see https://docs.searxng.org/admin/searx.limiter.html" "see https://docs.searxng.org/admin/searx.limiter.html"
) )
+42
View File
@@ -0,0 +1,42 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementation of the :py:obj:`preference <searx.preference>` settings."""
# pylint: disable = too-few-public-methods
import typing as t
import msgspec
class SettingsPref(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
"""Options for configuring the preferences
.. code:: yaml
preferences:
lock:
- favicon_resolver
- image_proxy
- method
# ...
"""
lock: set[
t.Literal[
"categories",
"language",
"locale",
"autocomplete",
"favicon_resolver",
"image_proxy",
"method",
"safesearch",
"theme",
"results_on_new_tab",
"doi_resolver",
"simple_style",
"center_alignment",
"query_in_title",
"search_on_category_select",
]
] = set()
"""Lock arbitrary settings on the preferences page."""
+52 -63
View File
@@ -17,13 +17,14 @@ import babel.core
import searx.plugins import searx.plugins
from searx import settings, autocomplete, favicons from searx import get_setting, settings, autocomplete, favicons
from searx.enginelib import Engine from searx.enginelib import Engine
from searx.engines import DEFAULT_CATEGORY from searx.engines import DEFAULT_CATEGORY
from searx.extended_types import SXNG_Request from searx.extended_types import SXNG_Request
from searx.locales import LOCALE_NAMES from searx.locales import LOCALE_NAMES
from searx.webutils import VALID_LANGUAGE_CODE from searx.webutils import VALID_LANGUAGE_CODE
from ._settings import SettingsPref
COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5 # 5 years COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5 # 5 years
DOI_RESOLVERS = list(settings['doi_resolvers']) DOI_RESOLVERS = list(settings['doi_resolvers'])
@@ -386,6 +387,7 @@ class ClientPref:
return cls(locale=locale) return cls(locale=locale)
@t.final
class Preferences: class Preferences:
"""Validates and saves preferences to cookies""" """Validates and saves preferences to cookies"""
@@ -400,95 +402,91 @@ class Preferences:
super().__init__() super().__init__()
self.cfg: SettingsPref = get_setting("preferences")
self.key_value_settings: dict[str, Setting] = { self.key_value_settings: dict[str, Setting] = {
# fmt: off
'categories': MultipleChoiceSetting( 'categories': MultipleChoiceSetting(
['general'], ["general"],
locked=is_locked('categories'), locked="categories" in self.cfg.lock,
choices=categories + ['none'] choices=categories + ["none"],
), ),
'language': SearchLanguageSetting( 'language': SearchLanguageSetting(
settings['search']['default_lang'], get_setting("search.default_lang"),
locked=is_locked('language'), locked="language" in self.cfg.lock,
choices=settings['search']['languages'] + [''] choices=get_setting("search.languages") + [""],
), ),
'locale': EnumStringSetting( 'locale': EnumStringSetting(
settings['ui']['default_locale'], get_setting("ui.default_locale"),
locked=is_locked('locale'), locked="locale" in self.cfg.lock,
choices=list(LOCALE_NAMES.keys()) + [''] choices=list(LOCALE_NAMES.keys()) + [""],
), ),
'autocomplete': EnumStringSetting( 'autocomplete': EnumStringSetting(
settings['search']['autocomplete'], get_setting("search.autocomplete"),
locked=is_locked('autocomplete'), locked="autocomplete" in self.cfg.lock,
choices=list(autocomplete.backends.keys()) + [''] choices=list(autocomplete.backends.keys()) + [""],
), ),
'favicon_resolver': EnumStringSetting( 'favicon_resolver': EnumStringSetting(
settings['search']['favicon_resolver'], get_setting("search.favicon_resolver"),
locked=is_locked('favicon_resolver'), locked="favicon_resolver" in self.cfg.lock,
choices=list(favicons.proxy.CFG.resolver_map.keys()) + [''] choices=list(favicons.proxy.CFG.resolver_map.keys()) + [''],
), ),
'image_proxy': BooleanSetting( 'image_proxy': BooleanSetting(
settings['server']['image_proxy'], get_setting("server.image_proxy"),
locked=is_locked('image_proxy') locked="image_proxy" in self.cfg.lock,
), ),
'method': EnumStringSetting( 'method': EnumStringSetting(
settings['server']['method'], get_setting("server.method"),
locked=is_locked('method'), locked="method" in self.cfg.lock,
choices=('GET', 'POST') choices=("GET", "POST"),
), ),
'safesearch': MapSetting( 'safesearch': MapSetting(
settings['search']['safe_search'], get_setting("search.safe_search"),
locked=is_locked('safesearch'), locked="safesearch" in self.cfg.lock,
map={ map={
'0': 0, "0": 0,
'1': 1, "1": 1,
'2': 2 "2": 2,
} },
), ),
'theme': EnumStringSetting( 'theme': EnumStringSetting(
settings['ui']['default_theme'], get_setting("ui.default_theme"),
locked=is_locked('theme'), locked="theme" in self.cfg.lock,
choices=themes choices=themes,
), ),
'results_on_new_tab': BooleanSetting( 'results_on_new_tab': BooleanSetting(
settings['ui']['results_on_new_tab'], get_setting("ui.results_on_new_tab"),
locked=is_locked('results_on_new_tab') locked="results_on_new_tab" in self.cfg.lock,
), ),
'doi_resolver': MultipleChoiceSetting( 'doi_resolver': MultipleChoiceSetting(
[settings['default_doi_resolver'], ], [get_setting("default_doi_resolver")],
locked=is_locked('doi_resolver'), locked="doi_resolver" in self.cfg.lock,
choices=DOI_RESOLVERS choices=DOI_RESOLVERS,
), ),
'simple_style': EnumStringSetting( 'simple_style': EnumStringSetting(
settings['ui']['theme_args']['simple_style'], get_setting("ui.theme_args.simple_style"),
locked=is_locked('simple_style'), locked="simple_style" in self.cfg.lock,
choices=['', 'auto', 'light', 'dark', 'black'] choices=["", "auto", "light", "dark", "black"],
), ),
'center_alignment': BooleanSetting( 'center_alignment': BooleanSetting(
settings['ui']['center_alignment'], get_setting("ui.center_alignment"),
locked=is_locked('center_alignment') locked="center_alignment" in self.cfg.lock,
),
'advanced_search': BooleanSetting(
settings['ui']['advanced_search'],
locked=is_locked('advanced_search')
), ),
'query_in_title': BooleanSetting( 'query_in_title': BooleanSetting(
settings['ui']['query_in_title'], get_setting("ui.query_in_title"),
locked=is_locked('query_in_title') locked="query_in_title" in self.cfg.lock,
), ),
'search_on_category_select': BooleanSetting( 'search_on_category_select': BooleanSetting(
settings['ui']['search_on_category_select'], get_setting("ui.search_on_category_select"),
locked=is_locked('search_on_category_select') locked="search_on_category_select" in self.cfg.lock,
), ),
'hotkeys': EnumStringSetting( 'hotkeys': EnumStringSetting(
settings['ui']['hotkeys'], get_setting("ui.hotkeys"),
choices=['default', 'vim'] choices=["default", "vim"],
), ),
'url_formatting': EnumStringSetting( 'url_formatting': EnumStringSetting(
settings['ui']['url_formatting'], get_setting("ui.url_formatting"),
choices=['pretty', 'full', 'host'] choices=["pretty", "full", "host"],
), ),
# fmt: on
} }
self.engines = EnginesSetting('engines', engines=engines.values()) self.engines = EnginesSetting('engines', engines=engines.values())
@@ -597,12 +595,3 @@ class Preferences:
break break
return valid return valid
def is_locked(setting_name: str):
"""Checks if a given setting name is locked by settings.yml"""
if 'preferences' not in settings:
return False
if 'lock' not in settings['preferences']:
return False
return setting_name in settings['preferences']['lock']
+4 -5
View File
@@ -154,14 +154,13 @@ ui:
# URL formatting: pretty, full or host # URL formatting: pretty, full or host
url_formatting: pretty url_formatting: pretty
# Lock arbitrary settings on the preferences page. preferences:
# # Lock arbitrary settings on the preferences page.
# preferences: lock: []
# lock:
# - categories # - categories
# - language # - language
# - autocomplete # - autocomplete
# - favicon # - favicon_resolver
# - safesearch # - safesearch
# - method # - method
# - doi_resolver # - doi_resolver
+4 -4
View File
@@ -15,6 +15,7 @@ import msgspec
from typing_extensions import override from typing_extensions import override
from .brand import SettingsBrand from .brand import SettingsBrand
from .sxng_locales import sxng_locales from .sxng_locales import sxng_locales
from ._settings import SettingsPref
searx_dir = abspath(dirname(__file__)) searx_dir = abspath(dirname(__file__))
@@ -146,6 +147,8 @@ def apply_schema(settings: dict[str, t.Any], schema: dict[str, t.Any], path_list
# Type Validation at runtime: # Type Validation at runtime:
# https://jcristharif.com/msgspec/structs.html#type-validation # https://jcristharif.com/msgspec/structs.html#type-validation
cfg_dict = settings.get(key) cfg_dict = settings.get(key)
if cfg_dict is None:
cfg_dict = {}
cfg_json = msgspec.json.encode(cfg_dict) cfg_json = msgspec.json.encode(cfg_dict)
settings[key] = msgspec.json.decode(cfg_json, type=value) settings[key] = msgspec.json.decode(cfg_json, type=value)
except msgspec.ValidationError as e: except msgspec.ValidationError as e:
@@ -236,16 +239,13 @@ SCHEMA: dict[str, t.Any] = {
}, },
'center_alignment': SettingsValue(bool, False), 'center_alignment': SettingsValue(bool, False),
'results_on_new_tab': SettingsValue(bool, False), 'results_on_new_tab': SettingsValue(bool, False),
'advanced_search': SettingsValue(bool, False),
'query_in_title': SettingsValue(bool, False), 'query_in_title': SettingsValue(bool, False),
'cache_url': SettingsValue(str, 'https://web.archive.org/web/'), 'cache_url': SettingsValue(str, 'https://web.archive.org/web/'),
'search_on_category_select': SettingsValue(bool, True), 'search_on_category_select': SettingsValue(bool, True),
'hotkeys': SettingsValue(('default', 'vim'), 'default'), 'hotkeys': SettingsValue(('default', 'vim'), 'default'),
'url_formatting': SettingsValue(('pretty', 'full', 'host'), 'pretty'), 'url_formatting': SettingsValue(('pretty', 'full', 'host'), 'pretty'),
}, },
'preferences': { "preferences": SettingsPref,
'lock': SettingsValue(list, []),
},
'outgoing': { 'outgoing': {
'useragent_suffix': SettingsValue(str, ''), 'useragent_suffix': SettingsValue(str, ''),
'request_timeout': SettingsValue(numbers.Real, 3.0), 'request_timeout': SettingsValue(numbers.Real, 3.0),
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -180,7 +180,7 @@
{%- if 'autocomplete' not in locked_preferences -%} {%- if 'autocomplete' not in locked_preferences -%}
{%- include 'simple/preferences/autocomplete.html' -%} {%- include 'simple/preferences/autocomplete.html' -%}
{%- endif -%} {%- endif -%}
{%- if 'favicon' not in locked_preferences -%} {%- if 'favicon_resolver' not in locked_preferences -%}
{%- include 'simple/preferences/favicon.html' -%} {%- include 'simple/preferences/favicon.html' -%}
{%- endif -%} {%- endif -%}
{% if 'safesearch' not in locked_preferences %} {% if 'safesearch' not in locked_preferences %}
+6 -6
View File
@@ -8,7 +8,7 @@ from searx.webutils import VALID_LANGUAGE_CODE
from searx.query import RawTextQuery from searx.query import RawTextQuery
from searx.engines import categories, engines from searx.engines import categories, engines
from searx.search.models import SearchQuery, EngineRef from searx.search.models import SearchQuery, EngineRef
from searx.preferences import Preferences, is_locked from searx.preferences import Preferences
# remove duplicate queries. # remove duplicate queries.
@@ -53,7 +53,7 @@ def parse_pageno(form: Dict[str, str]) -> int:
def parse_lang(preferences: Preferences, form: Dict[str, str], raw_text_query: RawTextQuery) -> str: def parse_lang(preferences: Preferences, form: Dict[str, str], raw_text_query: RawTextQuery) -> str:
if is_locked('language'): if "language" in preferences.cfg.lock:
return preferences.get_value('language') return preferences.get_value('language')
# get language # get language
# set specific language if set on request, query or preferences # set specific language if set on request, query or preferences
@@ -73,7 +73,7 @@ def parse_lang(preferences: Preferences, form: Dict[str, str], raw_text_query: R
def parse_safesearch(preferences: Preferences, form: Dict[str, str]) -> int: def parse_safesearch(preferences: Preferences, form: Dict[str, str]) -> int:
if is_locked('safesearch'): if "safesearch" in preferences.cfg.lock:
return preferences.get_value('safesearch') return preferences.get_value('safesearch')
if 'safesearch' in form: if 'safesearch' in form:
@@ -135,7 +135,7 @@ def parse_category_form(query_categories: List[str], name: str, value: str) -> N
def get_selected_categories(preferences: Preferences, form: Optional[Dict[str, str]]) -> List[str]: def get_selected_categories(preferences: Preferences, form: Optional[Dict[str, str]]) -> List[str]:
selected_categories = [] selected_categories = []
if not is_locked('categories') and form is not None: if not "categories" in preferences.cfg.lock and form is not None:
for name, value in form.items(): for name, value in form.items():
parse_category_form(selected_categories, name, value) parse_category_form(selected_categories, name, value)
@@ -175,7 +175,7 @@ def parse_generic(preferences: Preferences, form: Dict[str, str], disabled_engin
# set categories/engines # set categories/engines
explicit_engine_list = False explicit_engine_list = False
if not is_locked('categories'): if not "categories" in preferences.cfg.lock:
# parse the form only if the categories are not locked # parse the form only if the categories are not locked
for pd_name, pd in form.items(): # pylint: disable=invalid-name for pd_name, pd in form.items(): # pylint: disable=invalid-name
if pd_name == 'engines': if pd_name == 'engines':
@@ -266,7 +266,7 @@ def get_search_query_from_webapp(
if query_lang == 'auto': if query_lang == 'auto':
query_lang = preferences.client.locale_tag or 'all' query_lang = preferences.client.locale_tag or 'all'
if not is_locked('categories') and raw_text_query.specific: if not "categories" in preferences.cfg.lock and raw_text_query.specific:
# if engines are calculated from query, # if engines are calculated from query,
# set categories by using that information # set categories by using that information
query_engineref_list = raw_text_query.enginerefs query_engineref_list = raw_text_query.enginerefs
+1 -2
View File
@@ -377,7 +377,6 @@ def get_client_settings():
'theme_static_path': custom_url_for('static', filename='themes/simple'), 'theme_static_path': custom_url_for('static', filename='themes/simple'),
'results_on_new_tab': req_pref.get_value('results_on_new_tab'), 'results_on_new_tab': req_pref.get_value('results_on_new_tab'),
'favicon_resolver': req_pref.get_value('favicon_resolver'), 'favicon_resolver': req_pref.get_value('favicon_resolver'),
'advanced_search': req_pref.get_value('advanced_search'),
'query_in_title': req_pref.get_value('query_in_title'), 'query_in_title': req_pref.get_value('query_in_title'),
'safesearch': req_pref.get_value('safesearch'), 'safesearch': req_pref.get_value('safesearch'),
'theme': req_pref.get_value('theme'), 'theme': req_pref.get_value('theme'),
@@ -977,7 +976,7 @@ def preferences():
current_doi_resolver = get_doi_resolver(), current_doi_resolver = get_doi_resolver(),
allowed_plugins = allowed_plugins, allowed_plugins = allowed_plugins,
preferences_url_params = sxng_request.preferences.get_as_url_params(), preferences_url_params = sxng_request.preferences.get_as_url_params(),
locked_preferences = get_setting("preferences.lock", []), locked_preferences = get_setting("preferences").lock,
doi_resolvers = get_setting("doi_resolvers", {}), doi_resolvers = get_setting("doi_resolvers", {}),
# fmt: on # fmt: on
) )