2"""Searx preferences implementation.
4from __future__
import annotations
8from base64
import urlsafe_b64encode, urlsafe_b64decode
9from zlib
import compress, decompress
10from urllib.parse
import parse_qs, urlencode
11from typing
import Iterable, Dict, List, Optional
12from collections
import OrderedDict
19from searx
import settings, autocomplete, favicons
27COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5
28DOI_RESOLVERS = list(settings[
'doi_resolvers'])
30MAP_STR2BOOL: Dict[str, bool] = OrderedDict(
44 """Exption from ``cls.__init__`` when configuration value is invalid."""
48 """Base class of user settings"""
50 def __init__(self, default_value, locked: bool =
False):
56 """Parse ``data`` and store the result at ``self.value``
58 If needed, its overwritten in the inheritance.
63 """Returns the value of the setting
65 If needed, its overwritten in the inheritance.
69 def save(self, name: str, resp: flask.Response):
70 """Save cookie ``name`` in the HTTP response object
72 If needed, its overwritten in the inheritance."""
73 resp.set_cookie(name, self.
value, max_age=COOKIE_MAX_AGE)
77 """Setting of plain string values"""
80class EnumStringSetting(Setting):
81 """Setting of a value which can only come from the given choices"""
83 def __init__(self, default_value: str, choices: Iterable[str], locked=
False):
84 super().
__init__(default_value, locked)
89 if selection
not in self.
choices:
93 """Parse and validate ``data`` and store the result at ``self.value``"""
99 """Setting of values which can only come from the given choices"""
101 def __init__(self, default_value: List[str], choices: Iterable[str], locked=
False):
102 super().
__init__(default_value, locked)
107 for item
in selections:
112 """Parse and validate ``data`` and store the result at ``self.value``"""
117 elements = data.split(
',')
119 self.
value = elements
128 self.
value.append(choice)
130 def save(self, name: str, resp: flask.Response):
131 """Save cookie ``name`` in the HTTP response object"""
132 resp.set_cookie(name,
','.join(self.
value), max_age=COOKIE_MAX_AGE)
136 """Setting of values of type ``set`` (comma separated string)"""
143 """Returns a string with comma separated values."""
144 return ','.join(self.
values)
147 """Parse and validate ``data`` and store the result at ``self.value``"""
152 elements = data.split(
',')
153 for element
in elements:
160 elements = data.split(
',')
161 self.
values = set(elements)
163 def save(self, name: str, resp: flask.Response):
164 """Save cookie ``name`` in the HTTP response object"""
165 resp.set_cookie(name,
','.join(self.
values), max_age=COOKIE_MAX_AGE)
169 """Available choices may change, so user's value may not be in choices anymore"""
172 if selection !=
'' and selection !=
'auto' and not VALID_LANGUAGE_CODE.match(selection):
176 """Parse and validate ``data`` and store the result at ``self.value``"""
179 data = str(data).replace(
'_',
'-')
180 lang = data.split(
'-', maxsplit=1)[0]
193 """Setting of a value that has to be translated in order to be storable"""
195 def __init__(self, default_value, map: Dict[str, object], locked=
False):
196 super().
__init__(default_value, locked)
199 if self.
value not in self.
map.values():
203 """Parse and validate ``data`` and store the result at ``self.value``"""
205 if data
not in self.
map:
210 def save(self, name: str, resp: flask.Response):
211 """Save cookie ``name`` in the HTTP response object"""
212 if hasattr(self,
'key'):
213 resp.set_cookie(name, self.
key, max_age=COOKIE_MAX_AGE)
217 """Setting of a boolean value that has to be translated in order to be storable"""
220 for v_str, v_obj
in MAP_STR2BOOL.items():
223 raise ValueError(
"Invalid value: %s (%s) is not a boolean!" % (repr(val), type(val)))
226 """Parse and validate ``data`` and store the result at ``self.value``"""
230 def save(self, name: str, resp: flask.Response):
231 """Save cookie ``name`` in the HTTP response object"""
232 if hasattr(self,
'key'):
233 resp.set_cookie(name, self.
key, max_age=COOKIE_MAX_AGE)
237 """Maps strings to booleans that are either true or false."""
239 def __init__(self, name: str, choices: Dict[str, bool], locked: bool =
False):
252 for disabled
in data_disabled.split(
','):
256 for enabled
in data_enabled.split(
','):
266 self.
choices[setting] = setting
not in disabled
270 return (k
for k, v
in self.
choices.items()
if v)
274 return (k
for k, v
in self.
choices.items()
if not v)
276 def save(self, resp: flask.Response):
277 """Save cookie in the HTTP response object"""
280 resp.set_cookie(
'disabled_{0}'.format(self.
name),
','.join(disabled_changed), max_age=COOKIE_MAX_AGE)
281 resp.set_cookie(
'enabled_{0}'.format(self.
name),
','.join(enabled_changed), max_age=COOKIE_MAX_AGE)
291 """Engine settings"""
293 def __init__(self, default_value, engines: Iterable[Engine]):
295 for engine
in engines:
296 for category
in engine.categories:
297 if not category
in list(settings[
'categories_as_tabs'].keys()) + [DEFAULT_CATEGORY]:
299 choices[
'{}__{}'.format(engine.name, category)] =
not engine.disabled
300 super().
__init__(default_value, choices)
303 return [item[len(
'engine_') :].replace(
'_',
' ').replace(
' ',
'__')
for item
in items]
306 if len(values) == 1
and next(iter(values)) ==
'':
308 transformed_values = []
310 engine, category = value.split(
'__')
311 transformed_values.append((engine, category))
312 return transformed_values
316 """Plugin settings"""
319 super().
__init__(default_value, {plugin.id: plugin.default_on
for plugin
in plugins})
322 return [item[len(
'plugin_') :]
for item
in items]
326 """Container to assemble client prefferences and settings."""
331 """Locale preferred by the client."""
333 def __init__(self, locale: Optional[babel.Locale] =
None):
340 tag = self.
locale.language
342 tag +=
'-' + self.
locale.territory
347 """Build ClientPref object from HTTP request.
349 - `Accept-Language used for locale setting
350 <https://www.w3.org/International/questions/qa-accept-lang-locales.en>`__
353 al_header = http_request.headers.get(
"Accept-Language")
355 return cls(locale=
None)
358 for l
in al_header.split(
','):
360 lang, qvalue = [_.strip()
for _
in (l.split(
';') + [
'q=1',])[:2]]
363 qvalue = float(qvalue.split(
'=')[-1])
364 locale = babel.Locale.parse(lang, sep=
'-')
365 except (ValueError, babel.core.UnknownLocaleError):
367 pairs.append((locale, qvalue))
371 pairs.sort(reverse=
True, key=
lambda x: x[1])
373 return cls(locale=locale)
377 """Validates and saves preferences to cookies"""
382 categories: list[str],
383 engines: dict[str, Engine],
385 client: ClientPref |
None =
None,
395 choices=categories + [
'none']
398 settings[
'search'][
'default_lang'],
400 choices=settings[
'search'][
'languages'] + [
'']
403 settings[
'ui'][
'default_locale'],
405 choices=list(LOCALE_NAMES.keys()) + [
'']
408 settings[
'search'][
'autocomplete'],
410 choices=list(autocomplete.backends.keys()) + [
'']
413 settings[
'search'][
'favicon_resolver'],
415 choices=list(favicons.proxy.CFG.resolver_map.keys()) + [
'']
418 settings[
'server'][
'image_proxy'],
422 settings[
'server'][
'method'],
424 choices=(
'GET',
'POST')
427 settings[
'search'][
'safe_search'],
436 settings[
'ui'][
'default_theme'],
441 settings[
'ui'][
'results_on_new_tab'],
445 [settings[
'default_doi_resolver'], ],
447 choices=DOI_RESOLVERS
450 settings[
'ui'][
'theme_args'][
'simple_style'],
452 choices=[
'',
'auto',
'light',
'dark',
'black']
455 settings[
'ui'][
'center_alignment'],
459 settings[
'ui'][
'advanced_search'],
463 settings[
'ui'][
'query_in_title'],
467 settings[
'ui'][
'infinite_scroll'],
471 settings[
'ui'][
'search_on_category_select'],
472 locked=
is_locked(
'search_on_category_select')
475 settings[
'ui'][
'hotkeys'],
476 choices=[
'default',
'vim']
479 settings[
'ui'][
'url_formatting'],
480 choices=[
'pretty',
'full',
'host']
491 """Return preferences as URL parameters"""
496 if isinstance(v, MultipleChoiceSetting):
497 settings_kv[k] =
','.join(v.get_value())
499 settings_kv[k] = v.get_value()
501 settings_kv[
'disabled_engines'] =
','.join(self.
engines.disabled)
502 settings_kv[
'enabled_engines'] =
','.join(self.
engines.enabled)
504 settings_kv[
'disabled_plugins'] =
','.join(self.
plugins.disabled)
505 settings_kv[
'enabled_plugins'] =
','.join(self.
plugins.enabled)
507 settings_kv[
'tokens'] =
','.join(self.
tokens.values)
509 return urlsafe_b64encode(compress(urlencode(settings_kv).encode())).decode()
512 """parse (base64) preferences from request (``flask.request.form['preferences']``)"""
513 bin_data = decompress(urlsafe_b64decode(input_data))
515 for x, y
in parse_qs(bin_data.decode(
'ascii'), keep_blank_values=
True).items():
520 """parse preferences from request (``flask.request.form``)"""
521 for user_setting_name, user_setting
in input_data.items():
526 elif user_setting_name ==
'disabled_engines':
527 self.
engines.parse_cookie(input_data.get(
'disabled_engines',
''), input_data.get(
'enabled_engines',
''))
528 elif user_setting_name ==
'disabled_plugins':
529 self.
plugins.parse_cookie(input_data.get(
'disabled_plugins',
''), input_data.get(
'enabled_plugins',
''))
530 elif user_setting_name ==
'tokens':
531 self.
tokens.parse(user_setting)
534 """Parse formular (``<input>``) data from a ``flask.request.form``"""
535 disabled_engines = []
536 enabled_categories = []
537 disabled_plugins = []
542 if key
not in input_data.keys()
and isinstance(setting, BooleanSetting):
543 input_data[key] =
'False'
545 for user_setting_name, user_setting
in input_data.items():
548 elif user_setting_name.startswith(
'engine_'):
549 disabled_engines.append(user_setting_name)
550 elif user_setting_name.startswith(
'category_'):
551 enabled_categories.append(user_setting_name[len(
'category_') :])
552 elif user_setting_name.startswith(
'plugin_'):
553 disabled_plugins.append(user_setting_name)
554 elif user_setting_name ==
'tokens':
563 """Returns the value for ``user_setting_name``"""
569 def save(self, resp: flask.Response):
570 """Save cookie in the HTTP response object"""
575 user_setting.save(user_setting_name, resp)
583 if hasattr(engine,
'tokens')
and engine.tokens:
585 for token
in self.
tokens.values:
586 if token
in engine.tokens:
594 """Checks if a given setting name is locked by settings.yml"""
595 if 'preferences' not in settings:
597 if 'lock' not in settings[
'preferences']:
599 return setting_name
in settings[
'preferences'][
'lock']
save(self, flask.Response resp)
transform_values(self, values)
parse_form(self, List[str] items)
transform_form_items(self, items)
__init__(self, str name, Dict[str, bool] choices, bool locked=False)
parse_cookie(self, str data_disabled, str data_enabled)
normalized_str(self, val)
save(self, str name, flask.Response resp)
from_http_request(cls, SXNG_Request http_request)
__init__(self, Optional[babel.Locale] locale=None)
__init__(self, default_value, Iterable[Engine] engines)
transform_values(self, values)
transform_form_items(self, items)
__init__(self, str default_value, Iterable[str] choices, locked=False)
_validate_selection(self, str selection)
__init__(self, default_value, Dict[str, object] map, locked=False)
save(self, str name, flask.Response resp)
parse_form(self, List[str] data)
__init__(self, List[str] default_value, Iterable[str] choices, locked=False)
_validate_selections(self, List[str] selections)
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)
__init__(self, list[str] themes, list[str] categories, dict[str, Engine] engines, searx.plugins.PluginStorage plugins, ClientPref|None client=None)
save(self, flask.Response resp)
validate_token(self, engine)
parse_dict(self, Dict[str, str] input_data)
get_value(self, str user_setting_name)
_validate_selection(self, 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, default_value, bool locked=False)
is_locked(str setting_name)