2"""Searx preferences implementation.
9from base64
import urlsafe_b64encode, urlsafe_b64decode
10from zlib
import compress, decompress
11from urllib.parse
import parse_qs, urlencode
12from collections
import OrderedDict
13from collections.abc
import Iterable
21from searx
import settings, autocomplete, favicons
29COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5
30DOI_RESOLVERS = list(settings[
'doi_resolvers'])
32MAP_STR2BOOL: dict[str, bool] = OrderedDict(
46 """Exption from ``cls.__init__`` when configuration value is invalid."""
50 """Base class of user settings"""
52 def __init__(self, default_value: t.Any, locked: bool =
False):
54 self.
value: t.Any = default_value
58 """Parse ``data`` and store the result at ``self.value``
60 If needed, its overwritten in the inheritance.
65 """Returns the value of the setting
67 If needed, its overwritten in the inheritance.
71 def save(self, name: str, resp: flask.Response):
72 """Save cookie ``name`` in the HTTP response object
74 If needed, its overwritten in the inheritance."""
75 resp.set_cookie(name, self.
value, max_age=COOKIE_MAX_AGE)
79 """Setting of plain string values"""
82class EnumStringSetting(Setting):
83 """Setting of a value which can only come from the given choices"""
87 def __init__(self, default_value: str, choices: Iterable[str], locked: bool =
False):
88 super().
__init__(default_value, locked)
93 if selection
not in self.
choices:
97 """Parse and validate ``data`` and store the result at ``self.value``"""
103 """Setting of values which can only come from the given choices"""
105 def __init__(self, default_value: list[str], choices: Iterable[str], locked: bool =
False):
106 super().
__init__(default_value, locked)
111 for item
in selections:
116 """Parse and validate ``data`` and store the result at ``self.value``"""
118 self.
value: list[str] = []
121 elements = data.split(
',')
123 self.
value = elements
132 self.
value.append(choice)
134 def save(self, name: str, resp: flask.Response):
135 """Save cookie ``name`` in the HTTP response object"""
136 resp.set_cookie(name,
','.join(self.
value), max_age=COOKIE_MAX_AGE)
140 """Setting of values of type ``set`` (comma separated string)"""
147 """Returns a string with comma separated values."""
148 return ','.join(self.
values)
151 """Parse and validate ``data`` and store the result at ``self.value``"""
156 elements = data.split(
',')
157 for element
in elements:
164 elements = data.split(
',')
165 self.
values = set(elements)
167 def save(self, name: str, resp: flask.Response):
168 """Save cookie ``name`` in the HTTP response object"""
169 resp.set_cookie(name,
','.join(self.
values), max_age=COOKIE_MAX_AGE)
173 """Available choices may change, so user's value may not be in choices anymore"""
178 if selection !=
'' and selection !=
'auto' and not VALID_LANGUAGE_CODE.match(selection):
182 """Parse and validate ``data`` and store the result at ``self.value``"""
185 data = str(data).replace(
'_',
'-')
186 lang = data.split(
'-', maxsplit=1)[0]
199 """Setting of a value that has to be translated in order to be storable"""
205 self, default_value: object, map: dict[str, object], locked: bool =
False
207 super().
__init__(default_value, locked)
208 self.
map: dict[str, object] = map
210 if self.
value not in self.
map.values():
214 """Parse and validate ``data`` and store the result at ``self.value``"""
216 if data
not in self.
map:
221 def save(self, name: str, resp: flask.Response):
222 """Save cookie ``name`` in the HTTP response object"""
223 if hasattr(self,
'key'):
224 resp.set_cookie(name, self.
key, max_age=COOKIE_MAX_AGE)
228 """Setting of a boolean value that has to be translated in order to be storable"""
234 for v_str, v_obj
in MAP_STR2BOOL.items():
237 raise ValueError(
"Invalid value: %s (%s) is not a boolean!" % (repr(val), type(val)))
240 """Parse and validate ``data`` and store the result at ``self.value``"""
244 def save(self, name: str, resp: flask.Response):
245 """Save cookie ``name`` in the HTTP response object"""
246 if hasattr(self,
'key'):
247 resp.set_cookie(name, self.
key, max_age=COOKIE_MAX_AGE)
251 """Maps strings to booleans that are either true or false."""
253 def __init__(self, name: str, choices: dict[str, bool], locked: bool =
False):
266 for disabled
in data_disabled.split(
','):
270 for enabled
in data_enabled.split(
','):
280 self.
choices[setting] = setting
not in disabled
284 return (k
for k, v
in self.
choices.items()
if v)
288 return (k
for k, v
in self.
choices.items()
if not v)
290 def save(self, resp: flask.Response):
291 """Save cookie in the HTTP response object"""
294 resp.set_cookie(
'disabled_{0}'.format(self.
name),
','.join(disabled_changed), max_age=COOKIE_MAX_AGE)
295 resp.set_cookie(
'enabled_{0}'.format(self.
name),
','.join(enabled_changed), max_age=COOKIE_MAX_AGE)
305 """Engine settings"""
307 def __init__(self, default_value, engines: Iterable[Engine]):
309 for engine
in engines:
310 for category
in engine.categories:
311 if not category
in list(settings[
'categories_as_tabs'].keys()) + [DEFAULT_CATEGORY]:
313 choices[
'{}__{}'.format(engine.name, category)] =
not engine.disabled
314 super().
__init__(default_value, choices)
317 return [item[len(
'engine_') :].replace(
'_',
' ').replace(
' ',
'__')
for item
in items]
320 if len(values) == 1
and next(iter(values)) ==
'':
322 transformed_values = []
324 engine, category = value.split(
'__')
325 transformed_values.append((engine, category))
326 return transformed_values
330 """Plugin settings"""
333 super().
__init__(default_value, {plugin.id: plugin.active
for plugin
in plugins})
336 return [item[len(
'plugin_') :]
for item
in items]
340 """Container to assemble client prefferences and settings."""
344 locale: babel.Locale |
None
345 """Locale preferred by the client."""
347 def __init__(self, locale: babel.Locale |
None =
None):
354 tag = self.
locale.language
356 tag +=
'-' + self.
locale.territory
361 """Build ClientPref object from HTTP request.
363 - `Accept-Language used for locale setting
364 <https://www.w3.org/International/questions/qa-accept-lang-locales.en>`__
367 al_header = http_request.headers.get(
"Accept-Language")
369 return cls(locale=
None)
371 pairs: list[tuple[babel.Locale, float]] = []
372 for l
in al_header.split(
','):
374 lang, qvalue = [_.strip()
for _
in (l.split(
';') + [
'q=1',])[:2]]
377 qvalue = float(qvalue.split(
'=')[-1])
378 locale = babel.Locale.parse(lang, sep=
'-')
379 except (ValueError, babel.core.UnknownLocaleError):
381 pairs.append((locale, qvalue))
385 pairs.sort(reverse=
True, key=
lambda x: x[1])
387 return cls(locale=locale)
391 """Validates and saves preferences to cookies"""
396 categories: list[str],
397 engines: dict[str, Engine],
399 client: ClientPref |
None =
None,
409 choices=categories + [
'none']
412 settings[
'search'][
'default_lang'],
414 choices=settings[
'search'][
'languages'] + [
'']
417 settings[
'ui'][
'default_locale'],
419 choices=list(LOCALE_NAMES.keys()) + [
'']
422 settings[
'search'][
'autocomplete'],
424 choices=list(autocomplete.backends.keys()) + [
'']
427 settings[
'search'][
'favicon_resolver'],
429 choices=list(favicons.proxy.CFG.resolver_map.keys()) + [
'']
432 settings[
'server'][
'image_proxy'],
436 settings[
'server'][
'method'],
438 choices=(
'GET',
'POST')
441 settings[
'search'][
'safe_search'],
450 settings[
'ui'][
'default_theme'],
455 settings[
'ui'][
'results_on_new_tab'],
459 [settings[
'default_doi_resolver'], ],
461 choices=DOI_RESOLVERS
464 settings[
'ui'][
'theme_args'][
'simple_style'],
466 choices=[
'',
'auto',
'light',
'dark',
'black']
469 settings[
'ui'][
'center_alignment'],
473 settings[
'ui'][
'advanced_search'],
477 settings[
'ui'][
'query_in_title'],
481 settings[
'ui'][
'infinite_scroll'],
485 settings[
'ui'][
'search_on_category_select'],
486 locked=
is_locked(
'search_on_category_select')
489 settings[
'ui'][
'hotkeys'],
490 choices=[
'default',
'vim']
493 settings[
'ui'][
'url_formatting'],
494 choices=[
'pretty',
'full',
'host']
505 """Return preferences as URL parameters"""
510 if isinstance(v, MultipleChoiceSetting):
511 settings_kv[k] =
','.join(v.get_value())
513 settings_kv[k] = v.get_value()
515 settings_kv[
'disabled_engines'] =
','.join(self.
engines.disabled)
516 settings_kv[
'enabled_engines'] =
','.join(self.
engines.enabled)
518 settings_kv[
'disabled_plugins'] =
','.join(self.
plugins.disabled)
519 settings_kv[
'enabled_plugins'] =
','.join(self.
plugins.enabled)
521 settings_kv[
'tokens'] =
','.join(self.
tokens.values)
523 return urlsafe_b64encode(compress(urlencode(settings_kv).encode())).decode()
526 """parse (base64) preferences from request (``flask.request.form['preferences']``)"""
527 bin_data = decompress(urlsafe_b64decode(input_data))
529 for x, y
in parse_qs(bin_data.decode(
'ascii'), keep_blank_values=
True).items():
534 """parse preferences from request (``flask.request.form``)"""
535 for user_setting_name, user_setting
in input_data.items():
540 elif user_setting_name ==
'disabled_engines':
541 self.
engines.parse_cookie(input_data.get(
'disabled_engines',
''), input_data.get(
'enabled_engines',
''))
542 elif user_setting_name ==
'disabled_plugins':
543 self.
plugins.parse_cookie(input_data.get(
'disabled_plugins',
''), input_data.get(
'enabled_plugins',
''))
544 elif user_setting_name ==
'tokens':
545 self.
tokens.parse(user_setting)
548 """Parse formular (``<input>``) data from a ``flask.request.form``"""
549 disabled_engines = []
550 enabled_categories = []
551 disabled_plugins = []
556 if key
not in input_data.keys()
and isinstance(setting, BooleanSetting):
557 input_data[key] =
'False'
559 for user_setting_name, user_setting
in input_data.items():
562 elif user_setting_name.startswith(
'engine_'):
563 disabled_engines.append(user_setting_name)
564 elif user_setting_name.startswith(
'category_'):
565 enabled_categories.append(user_setting_name[len(
'category_') :])
566 elif user_setting_name.startswith(
'plugin_'):
567 disabled_plugins.append(user_setting_name)
568 elif user_setting_name ==
'tokens':
577 """Returns the value for ``user_setting_name``"""
583 def save(self, resp: flask.Response):
584 """Save cookie in the HTTP response object"""
589 user_setting.save(user_setting_name, resp)
597 if hasattr(engine,
'tokens')
and engine.tokens:
599 for token
in self.
tokens.values:
600 if token
in engine.tokens:
608 """Checks if a given setting name is locked by settings.yml"""
609 if 'preferences' not in settings:
611 if 'lock' not in settings[
'preferences']:
613 return setting_name
in settings[
'preferences'][
'lock']
save(self, flask.Response resp)
transform_values(self, values)
dict[str, bool] default_choices
parse_form(self, list[str] items)
__init__(self, str name, dict[str, bool] choices, bool locked=False)
transform_form_items(self, items)
parse_cookie(self, str data_disabled, str data_enabled)
str normalized_str(self, t.Any val)
save(self, str name, flask.Response resp)
from_http_request(cls, SXNG_Request http_request)
__init__(self, babel.Locale|None locale=None)
__init__(self, default_value, Iterable[Engine] engines)
transform_values(self, values)
transform_form_items(self, items)
_validate_selection(self, str selection)
__init__(self, str default_value, Iterable[str] choices, bool locked=False)
__init__(self, object default_value, dict[str, object] map, bool locked=False)
save(self, str name, flask.Response resp)
_validate_selections(self, list[str] selections)
__init__(self, list[str] default_value, Iterable[str] choices, bool locked=False)
parse_form(self, list[str] data)
save(self, str name, flask.Response resp)
transform_form_items(self, items)
__init__(self, default_value, Iterable[searx.plugins.Plugin] plugins)
parse_form(self, dict[str, str] input_data)
parse_encoded_data(self, str input_data)
t.Any get_value(self, str user_setting_name)
__init__(self, list[str] themes, list[str] categories, dict[str, Engine] engines, searx.plugins.PluginStorage plugins, ClientPref|None client=None)
parse_dict(self, dict[str, str] input_data)
save(self, flask.Response resp)
validate_token(self, engine)
_validate_selection(self, str selection)
save(self, str name, flask.Response resp)
__init__(self, *args, **kwargs)
parse_form(self, str data)
save(self, str name, flask.Response resp)
__init__(self, t.Any default_value, bool locked=False)
is_locked(str setting_name)