.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
settings_defaults.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2"""Implementation of the default settings.
3
4"""
5from __future__ import annotations
6
7import typing as t
8import numbers
9import errno
10import os
11import logging
12from base64 import b64decode
13from os.path import dirname, abspath
14
15from typing_extensions import override
16from .sxng_locales import sxng_locales
17
18searx_dir = abspath(dirname(__file__))
19
20logger = logging.getLogger('searx')
21OUTPUT_FORMATS = ['html', 'csv', 'json', 'rss']
22SXNG_LOCALE_TAGS = ['all', 'auto'] + list(l[0] for l in sxng_locales)
23SIMPLE_STYLE = ('auto', 'light', 'dark', 'black')
24CATEGORIES_AS_TABS: dict[str, dict[str, t.Any]] = {
25 'general': {},
26 'images': {},
27 'videos': {},
28 'news': {},
29 'map': {},
30 'music': {},
31 'it': {},
32 'science': {},
33 'files': {},
34 'social media': {},
35}
36STR_TO_BOOL = {
37 '0': False,
38 'false': False,
39 'off': False,
40 '1': True,
41 'true': True,
42 'on': True,
43}
44_UNDEFINED = object()
45
46# This type definition for SettingsValue.type_definition is incomplete, but it
47# helps to significantly reduce the most common error messages regarding type
48# annotations.
49TypeDefinition: t.TypeAlias = ( # pylint: disable=invalid-name
50 tuple[None, bool, type]
51 | tuple[None, type, type]
52 | tuple[None, type]
53 | tuple[bool, type]
54 | tuple[type, type]
55 | tuple[type]
56 | tuple[str | int, ...]
57)
58
59TypeDefinitionArg: t.TypeAlias = type | TypeDefinition # pylint: disable=invalid-name
60
61
63 """Check and update a setting value"""
64
66 self,
67 type_definition_arg: TypeDefinitionArg,
68 default: t.Any = None,
69 environ_name: str | None = None,
70 ):
71 self.type_definition: TypeDefinition = (
72 type_definition_arg if isinstance(type_definition_arg, tuple) else (type_definition_arg,)
73 )
74 self.default: t.Any = default
75 self.environ_name: str | None = environ_name
76
77 @property
79 types_str = [td.__name__ if isinstance(td, type) else repr(td) for td in self.type_definition]
80 return ', '.join(types_str)
81
82 def check_type_definition(self, value: t.Any) -> None:
83 if value in self.type_definition:
84 return
85 type_list = tuple(t for t in self.type_definition if isinstance(t, type))
86 if not isinstance(value, type_list):
87 raise ValueError('The value has to be one of these types/values: {}'.format(self.type_definition_repr))
88
89 def __call__(self, value: t.Any) -> t.Any:
90 if value == _UNDEFINED:
91 value = self.default
92 # override existing value with environ
93 if self.environ_name and self.environ_name in os.environ:
94 value = os.environ[self.environ_name]
95 if self.type_definition == (bool,):
96 value = STR_TO_BOOL[value.lower()]
97
98 self.check_type_definition(value)
99 return value
100
101
103 """Check the value is a sublist of type definition."""
104
105 @override
106 def check_type_definition(self, value: list[t.Any]) -> None:
107 if not isinstance(value, list):
108 raise ValueError('The value has to a list')
109 for item in value:
110 if not item in self.type_definition[0]:
111 raise ValueError('{} not in {}'.format(item, self.type_definition))
112
113
115 """Check and update a setting value that is a directory path"""
116
117 @override
118 def check_type_definition(self, value: t.Any) -> t.Any:
119 super().check_type_definition(value)
120 if not os.path.isdir(value):
121 raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), value)
122
123 @override
124 def __call__(self, value: t.Any) -> t.Any:
125 if value == '':
126 value = self.default
127 return super().__call__(value)
128
129
131 """str are base64 decoded"""
132
133 @override
134 def __call__(self, value: t.Any) -> t.Any:
135 if isinstance(value, str):
136 value = b64decode(value)
137 return super().__call__(value)
138
139
140def apply_schema(settings: dict[str, t.Any], schema: dict[str, t.Any], path_list: list[str]):
141 error = False
142 for key, value in schema.items():
143 if isinstance(value, SettingsValue):
144 try:
145 settings[key] = value(settings.get(key, _UNDEFINED))
146 except Exception as e: # pylint: disable=broad-except
147 # don't stop now: check other values
148 logger.error('%s: %s', '.'.join([*path_list, key]), e)
149 error = True
150 elif isinstance(value, dict):
151 error = error or apply_schema(settings.setdefault(key, {}), schema[key], [*path_list, key])
152 else:
153 settings.setdefault(key, value)
154 if len(path_list) == 0 and error:
155 raise ValueError('Invalid settings.yml')
156 return error
157
158
159SCHEMA: dict[str, t.Any] = {
160 'general': {
161 'debug': SettingsValue(bool, False, 'SEARXNG_DEBUG'),
162 'instance_name': SettingsValue(str, 'SearXNG'),
163 'privacypolicy_url': SettingsValue((None, False, str), None),
164 'contact_url': SettingsValue((None, False, str), None),
165 'donation_url': SettingsValue((bool, str), "https://docs.searxng.org/donate.html"),
166 'enable_metrics': SettingsValue(bool, True),
167 'open_metrics': SettingsValue(str, ''),
168 },
169 'brand': {
170 'issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues'),
171 'new_issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues/new'),
172 'docs_url': SettingsValue(str, 'https://docs.searxng.org'),
173 'public_instances': SettingsValue((False, str), 'https://searx.space'),
174 'wiki_url': SettingsValue((False, str), 'https://github.com/searxng/searxng/wiki'),
175 'custom': SettingsValue(dict, {'links': {}}),
176 },
177 'search': {
178 'safe_search': SettingsValue((0, 1, 2), 0),
179 'autocomplete': SettingsValue(str, ''),
180 'autocomplete_min': SettingsValue(int, 4),
181 'favicon_resolver': SettingsValue(str, ''),
182 'default_lang': SettingsValue(tuple(SXNG_LOCALE_TAGS + ['']), ''),
183 'languages': SettingSublistValue(SXNG_LOCALE_TAGS, SXNG_LOCALE_TAGS), # type: ignore
184 'ban_time_on_fail': SettingsValue(numbers.Real, 5),
185 'max_ban_time_on_fail': SettingsValue(numbers.Real, 120),
186 'suspended_times': {
187 'SearxEngineAccessDenied': SettingsValue(numbers.Real, 86400),
188 'SearxEngineCaptcha': SettingsValue(numbers.Real, 86400),
189 'SearxEngineTooManyRequests': SettingsValue(numbers.Real, 3600),
190 'cf_SearxEngineCaptcha': SettingsValue(numbers.Real, 1296000),
191 'cf_SearxEngineAccessDenied': SettingsValue(numbers.Real, 86400),
192 'recaptcha_SearxEngineCaptcha': SettingsValue(numbers.Real, 604800),
193 },
194 'formats': SettingsValue(list, OUTPUT_FORMATS),
195 'max_page': SettingsValue(int, 0),
196 },
197 'server': {
198 'port': SettingsValue((int, str), 8888, 'SEARXNG_PORT'),
199 'bind_address': SettingsValue(str, '127.0.0.1', 'SEARXNG_BIND_ADDRESS'),
200 'limiter': SettingsValue(bool, False, 'SEARXNG_LIMITER'),
201 'public_instance': SettingsValue(bool, False, 'SEARXNG_PUBLIC_INSTANCE'),
202 'secret_key': SettingsValue(str, environ_name='SEARXNG_SECRET'),
203 'base_url': SettingsValue((False, str), False, 'SEARXNG_BASE_URL'),
204 'image_proxy': SettingsValue(bool, False, 'SEARXNG_IMAGE_PROXY'),
205 'http_protocol_version': SettingsValue(('1.0', '1.1'), '1.0'),
206 'method': SettingsValue(('POST', 'GET'), 'POST', 'SEARXNG_METHOD'),
207 'default_http_headers': SettingsValue(dict, {}),
208 },
209 # redis is deprecated ..
210 'redis': {
211 'url': SettingsValue((None, False, str), False, 'SEARXNG_REDIS_URL'),
212 },
213 'valkey': {
214 'url': SettingsValue((None, False, str), False, 'SEARXNG_VALKEY_URL'),
215 },
216 'ui': {
217 'static_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'static')),
218 'templates_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'templates')),
219 'default_theme': SettingsValue(str, 'simple'),
220 'default_locale': SettingsValue(str, ''),
221 'theme_args': {
222 'simple_style': SettingsValue(SIMPLE_STYLE, 'auto'),
223 },
224 'center_alignment': SettingsValue(bool, False),
225 'results_on_new_tab': SettingsValue(bool, False),
226 'advanced_search': SettingsValue(bool, False),
227 'query_in_title': SettingsValue(bool, False),
228 'infinite_scroll': SettingsValue(bool, False),
229 'cache_url': SettingsValue(str, 'https://web.archive.org/web/'),
230 'search_on_category_select': SettingsValue(bool, True),
231 'hotkeys': SettingsValue(('default', 'vim'), 'default'),
232 'url_formatting': SettingsValue(('pretty', 'full', 'host'), 'pretty'),
233 },
234 'preferences': {
235 'lock': SettingsValue(list, []),
236 },
237 'outgoing': {
238 'useragent_suffix': SettingsValue(str, ''),
239 'request_timeout': SettingsValue(numbers.Real, 3.0),
240 'enable_http2': SettingsValue(bool, True),
241 'verify': SettingsValue((bool, str), True),
242 'max_request_timeout': SettingsValue((None, numbers.Real), None),
243 'pool_connections': SettingsValue(int, 100),
244 'pool_maxsize': SettingsValue(int, 10),
245 'keepalive_expiry': SettingsValue(numbers.Real, 5.0),
246 # default maximum redirect
247 # from https://github.com/psf/requests/blob/8c211a96cdbe9fe320d63d9e1ae15c5c07e179f8/requests/models.py#L55
248 'max_redirects': SettingsValue(int, 30),
249 'retries': SettingsValue(int, 0),
250 'proxies': SettingsValue((None, str, dict), None),
251 'source_ips': SettingsValue((None, str, list), None),
252 # Tor configuration
253 'using_tor_proxy': SettingsValue(bool, False),
254 'extra_proxy_timeout': SettingsValue(int, 0),
255 'networks': {},
256 },
257 'plugins': SettingsValue(dict, {}),
258 'checker': {
259 'off_when_debug': SettingsValue(bool, True, None),
260 'scheduling': SettingsValue((None, dict), None, None),
261 },
262 'categories_as_tabs': SettingsValue(dict, CATEGORIES_AS_TABS),
263 'engines': SettingsValue(list, []),
264 'doi_resolvers': {},
265}
None check_type_definition(self, list[t.Any] value)
None check_type_definition(self, t.Any value)
__init__(self, TypeDefinitionArg type_definition_arg, t.Any default=None, str|None environ_name=None)
apply_schema(dict[str, t.Any] settings, dict[str, t.Any] schema, list[str] path_list)