2"""Searx preferences implementation.
7from base64
import urlsafe_b64encode, urlsafe_b64decode
8from zlib
import compress, decompress
9from urllib.parse
import parse_qs, urlencode
10from typing
import Iterable, Dict, List, Optional
11from collections
import OrderedDict
16from searx
import settings, autocomplete, favicons
24COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5
25DOI_RESOLVERS = list(settings[
'doi_resolvers'])
27MAP_STR2BOOL: Dict[str, bool] = OrderedDict(
41 """Exption from ``cls.__init__`` when configuration value is invalid."""
45 """Base class of user settings"""
47 def __init__(self, default_value, locked: bool =
False):
53 """Parse ``data`` and store the result at ``self.value``
55 If needed, its overwritten in the inheritance.
60 """Returns the value of the setting
62 If needed, its overwritten in the inheritance.
66 def save(self, name: str, resp: flask.Response):
67 """Save cookie ``name`` in the HTTP response object
69 If needed, its overwritten in the inheritance."""
70 resp.set_cookie(name, self.
value, max_age=COOKIE_MAX_AGE)
74 """Setting of plain string values"""
77class EnumStringSetting(Setting):
78 """Setting of a value which can only come from the given choices"""
80 def __init__(self, default_value: str, choices: Iterable[str], locked=
False):
81 super().
__init__(default_value, locked)
86 if selection
not in self.
choices:
90 """Parse and validate ``data`` and store the result at ``self.value``"""
96 """Setting of values which can only come from the given choices"""
98 def __init__(self, default_value: List[str], choices: Iterable[str], locked=
False):
99 super().
__init__(default_value, locked)
104 for item
in selections:
109 """Parse and validate ``data`` and store the result at ``self.value``"""
114 elements = data.split(
',')
127 def save(self, name: str, resp: flask.Response):
128 """Save cookie ``name`` in the HTTP response object"""
129 resp.set_cookie(name,
','.join(self.
valuevalue), max_age=COOKIE_MAX_AGE)
133 """Setting of values of type ``set`` (comma separated string)"""
140 """Returns a string with comma separated values."""
141 return ','.join(self.
values)
144 """Parse and validate ``data`` and store the result at ``self.value``"""
149 elements = data.split(
',')
150 for element
in elements:
157 elements = data.split(
',')
158 self.
values = set(elements)
160 def save(self, name: str, resp: flask.Response):
161 """Save cookie ``name`` in the HTTP response object"""
162 resp.set_cookie(name,
','.join(self.
values), max_age=COOKIE_MAX_AGE)
166 """Available choices may change, so user's value may not be in choices anymore"""
169 if selection !=
'' and selection !=
'auto' and not VALID_LANGUAGE_CODE.match(selection):
173 """Parse and validate ``data`` and store the result at ``self.value``"""
176 data = str(data).replace(
'_',
'-')
177 lang = data.split(
'-', maxsplit=1)[0]
190 """Setting of a value that has to be translated in order to be storable"""
192 def __init__(self, default_value, map: Dict[str, object], locked=
False):
193 super().
__init__(default_value, locked)
200 """Parse and validate ``data`` and store the result at ``self.value``"""
202 if data
not in self.
map:
207 def save(self, name: str, resp: flask.Response):
208 """Save cookie ``name`` in the HTTP response object"""
209 if hasattr(self,
'key'):
210 resp.set_cookie(name, self.
key, max_age=COOKIE_MAX_AGE)
214 """Setting of a boolean value that has to be translated in order to be storable"""
217 for v_str, v_obj
in MAP_STR2BOOL.items():
220 raise ValueError(
"Invalid value: %s (%s) is not a boolean!" % (repr(val), type(val)))
223 """Parse and validate ``data`` and store the result at ``self.value``"""
227 def save(self, name: str, resp: flask.Response):
228 """Save cookie ``name`` in the HTTP response object"""
229 if hasattr(self,
'key'):
230 resp.set_cookie(name, self.
key, max_age=COOKIE_MAX_AGE)
234 """Maps strings to booleans that are either true or false."""
236 def __init__(self, name: str, choices: Dict[str, bool], locked: bool =
False):
249 for disabled
in data_disabled.split(
','):
253 for enabled
in data_enabled.split(
','):
273 def save(self, resp: flask.Response):
274 """Save cookie in the HTTP response object"""
277 resp.set_cookie(
'disabled_{0}'.
format(self.
name),
','.join(disabled_changed), max_age=COOKIE_MAX_AGE)
278 resp.set_cookie(
'enabled_{0}'.
format(self.
name),
','.join(enabled_changed), max_age=COOKIE_MAX_AGE)
288 """Engine settings"""
290 def __init__(self, default_value, engines: Iterable[Engine]):
292 for engine
in engines:
293 for category
in engine.categories:
294 if not category
in list(settings[
'categories_as_tabs'].keys()) + [DEFAULT_CATEGORY]:
296 choices[
'{}__{}'.
format(engine.name, category)] =
not engine.disabled
297 super().
__init__(default_value, choices)
300 return [item[len(
'engine_') :].replace(
'_',
' ').replace(
' ',
'__')
for item
in items]
303 if len(values) == 1
and next(iter(values)) ==
'':
305 transformed_values = []
307 engine, category = value.split(
'__')
308 transformed_values.append((engine, category))
309 return transformed_values
313 """Plugin settings"""
315 def __init__(self, default_value, plugins: Iterable[Plugin]):
316 super().
__init__(default_value, {plugin.id: plugin.default_on
for plugin
in plugins})
319 return [item[len(
'plugin_') :]
for item
in items]
323 """Container to assemble client prefferences and settings."""
328 """Locale prefered by the client."""
330 def __init__(self, locale: Optional[babel.Locale] =
None):
337 tag = self.
locale.language
339 tag +=
'-' + self.
locale.territory
344 """Build ClientPref object from HTTP request.
346 - `Accept-Language used for locale setting
347 <https://www.w3.org/International/questions/qa-accept-lang-locales.en>`__
350 al_header = http_request.headers.get(
"Accept-Language")
352 return cls(locale=
None)
355 for l
in al_header.split(
','):
357 lang, qvalue = [_.strip()
for _
in (l.split(
';') + [
'q=1',])[:2]]
360 qvalue = float(qvalue.split(
'=')[-1])
361 locale = babel.Locale.parse(lang, sep=
'-')
362 except (ValueError, babel.core.UnknownLocaleError):
364 pairs.append((locale, qvalue))
368 pairs.sort(reverse=
True, key=
lambda x: x[1])
370 return cls(locale=locale)
374 """Validates and saves preferences to cookies"""
379 categories: List[str],
380 engines: Dict[str, Engine],
381 plugins: Iterable[Plugin],
382 client: Optional[ClientPref] =
None,
391 locked=is_locked(
'categories'),
392 choices=categories + [
'none']
395 settings[
'search'][
'default_lang'],
396 locked=is_locked(
'language'),
397 choices=settings[
'search'][
'languages'] + [
'']
400 settings[
'ui'][
'default_locale'],
401 locked=is_locked(
'locale'),
402 choices=list(LOCALE_NAMES.keys()) + [
'']
405 settings[
'search'][
'autocomplete'],
406 locked=is_locked(
'autocomplete'),
407 choices=list(autocomplete.backends.keys()) + [
'']
410 settings[
'search'][
'favicon_resolver'],
411 locked=is_locked(
'favicon_resolver'),
412 choices=list(favicons.proxy.CFG.resolver_map.keys()) + [
'']
415 settings[
'server'][
'image_proxy'],
416 locked=is_locked(
'image_proxy')
419 settings[
'server'][
'method'],
420 locked=is_locked(
'method'),
421 choices=(
'GET',
'POST')
424 settings[
'search'][
'safe_search'],
425 locked=is_locked(
'safesearch'),
433 settings[
'ui'][
'default_theme'],
434 locked=is_locked(
'theme'),
438 settings[
'ui'][
'results_on_new_tab'],
439 locked=is_locked(
'results_on_new_tab')
442 [settings[
'default_doi_resolver'], ],
443 locked=is_locked(
'doi_resolver'),
444 choices=DOI_RESOLVERS
447 settings[
'ui'][
'theme_args'][
'simple_style'],
448 locked=is_locked(
'simple_style'),
449 choices=[
'',
'auto',
'light',
'dark',
'black']
452 settings[
'ui'][
'center_alignment'],
453 locked=is_locked(
'center_alignment')
456 settings[
'ui'][
'advanced_search'],
457 locked=is_locked(
'advanced_search')
460 settings[
'ui'][
'query_in_title'],
461 locked=is_locked(
'query_in_title')
464 settings[
'ui'][
'infinite_scroll'],
465 locked=is_locked(
'infinite_scroll')
468 settings[
'ui'][
'search_on_category_select'],
469 locked=is_locked(
'search_on_category_select')
472 settings[
'ui'][
'hotkeys'],
473 choices=[
'default',
'vim']
485 """Return preferences as URL parameters"""
490 if isinstance(v, MultipleChoiceSetting):
491 settings_kv[k] =
','.join(v.get_value())
493 settings_kv[k] = v.get_value()
495 settings_kv[
'disabled_engines'] =
','.join(self.
engines.disabled)
496 settings_kv[
'enabled_engines'] =
','.join(self.
engines.enabled)
498 settings_kv[
'disabled_plugins'] =
','.join(self.
plugins.disabled)
499 settings_kv[
'enabled_plugins'] =
','.join(self.
plugins.enabled)
501 settings_kv[
'tokens'] =
','.join(self.
tokens.values)
503 return urlsafe_b64encode(compress(urlencode(settings_kv).encode())).decode()
506 """parse (base64) preferences from request (``flask.request.form['preferences']``)"""
507 bin_data = decompress(urlsafe_b64decode(input_data))
509 for x, y
in parse_qs(bin_data.decode(
'ascii'), keep_blank_values=
True).items():
514 """parse preferences from request (``flask.request.form``)"""
515 for user_setting_name, user_setting
in input_data.items():
520 elif user_setting_name ==
'disabled_engines':
521 self.
engines.parse_cookie(input_data.get(
'disabled_engines',
''), input_data.get(
'enabled_engines',
''))
522 elif user_setting_name ==
'disabled_plugins':
523 self.
plugins.parse_cookie(input_data.get(
'disabled_plugins',
''), input_data.get(
'enabled_plugins',
''))
524 elif user_setting_name ==
'tokens':
525 self.
tokens.parse(user_setting)
527 user_setting_name.startswith(x)
for x
in [
'enabled_',
'disabled_',
'engine_',
'category_',
'plugin_']
532 """Parse formular (``<input>``) data from a ``flask.request.form``"""
533 disabled_engines = []
534 enabled_categories = []
535 disabled_plugins = []
540 if key
not in input_data.keys()
and isinstance(setting, BooleanSetting):
541 input_data[key] =
'False'
543 for user_setting_name, user_setting
in input_data.items():
546 elif user_setting_name.startswith(
'engine_'):
547 disabled_engines.append(user_setting_name)
548 elif user_setting_name.startswith(
'category_'):
549 enabled_categories.append(user_setting_name[len(
'category_') :])
550 elif user_setting_name.startswith(
'plugin_'):
551 disabled_plugins.append(user_setting_name)
552 elif user_setting_name ==
'tokens':
562 """Returns the value for ``user_setting_name``"""
570 def save(self, resp: flask.Response):
571 """Save cookie in the HTTP response object"""
576 user_setting.save(user_setting_name, resp)
581 resp.set_cookie(k, v, max_age=COOKIE_MAX_AGE)
586 if hasattr(engine,
'tokens')
and engine.tokens:
588 for token
in self.
tokens.values:
589 if token
in engine.tokens:
597 """Checks if a given setting name is locked by settings.yml"""
598 if 'preferences' not in settings:
600 if 'lock' not in settings[
'preferences']:
602 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, flask.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[Plugin] plugins)
parse_form(self, Dict[str, str] input_data)
__init__(self, List[str] themes, List[str] categories, Dict[str, Engine] engines, Iterable[Plugin] plugins, Optional[ClientPref] client=None)
parse_encoded_data(self, str input_data)
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)