@@ -1,3 +1,9 @@
1+# SPDX-License-Identifier: AGPL-3.0-or-later
2+"""Searx preferences implementation.
5+# pylint: disable=useless-object-inheritance
1from base64 import urlsafe_b64encode, urlsafe_b64decode
7from base64 import urlsafe_b64encode, urlsafe_b64decode
2from zlib import compress, decompress
8from zlib import compress, decompress
3from sys import version
9from sys import version
@@ -8,6 +14,7 @@ from searx.utils import match_language
8from searx.url_utils import parse_qs, urlencode
14from searx.url_utils import parse_qs, urlencode
10if version[0] == '3':
16if version[0] == '3':
17+ # pylint: disable=invalid-name
11 unicode = str
18 unicode = str
@@ -20,11 +27,14 @@ DOI_RESOLVERS = list(settings['doi_resolvers'])
22class MissingArgumentException(Exception):
29class MissingArgumentException(Exception):
23− pass
30+ """Exption from ``cls._post_init`` when a argument is missed.
26class ValidationException(Exception):
34class ValidationException(Exception):
36+ """Exption from ``cls._post_init`` when configuration value is invalid.
30class Setting(object):
40class Setting(object):
@@ -42,33 +52,45 @@ class Setting(object):
44 def parse(self, data):
54 def parse(self, data):
55+ """Parse ``data`` and store the result at ``self.value``
57+ If needed, its overwritten in the inheritance.
45 self.value = data
59 self.value = data
47 def get_value(self):
61 def get_value(self):
62+ """Returns the value of the setting
64+ If needed, its overwritten in the inheritance.
48 return self.value
66 return self.value
50 def save(self, name, resp):
68 def save(self, name, resp):
69+ """Save cookie ``name`` in the HTTP reponse obect
71+ If needed, its overwritten in the inheritance."""
51 resp.set_cookie(name, self.value, max_age=COOKIE_MAX_AGE)
72 resp.set_cookie(name, self.value, max_age=COOKIE_MAX_AGE)
54class StringSetting(Setting):
75class StringSetting(Setting):
55 """Setting of plain string values"""
76 """Setting of plain string values"""
59class EnumStringSetting(Setting):
79class EnumStringSetting(Setting):
60 """Setting of a value which can only come from the given choices"""
80 """Setting of a value which can only come from the given choices"""
62− def _validate_selection(self, selection):
63− if selection not in self.choices:
64− raise ValidationException('Invalid value: "{0}"'.format(selection))
66 def _post_init(self):
82 def _post_init(self):
67 if not hasattr(self, 'choices'):
83 if not hasattr(self, 'choices'):
68 raise MissingArgumentException('Missing argument: choices')
84 raise MissingArgumentException('Missing argument: choices')
69 self._validate_selection(self.value)
85 self._validate_selection(self.value)
87+ def _validate_selection(self, selection):
88+ if selection not in self.choices: # pylint: disable=no-member
89+ raise ValidationException('Invalid value: "{0}"'.format(selection))
71 def parse(self, data):
91 def parse(self, data):
92+ """Parse and validate ``data`` and store the result at ``self.value``
72 self._validate_selection(data)
94 self._validate_selection(data)
73 self.value = data
95 self.value = data
@@ -78,7 +100,7 @@ class MultipleChoiceSetting(EnumStringSetting):
79 def _validate_selections(self, selections):
101 def _validate_selections(self, selections):
80 for item in selections:
102 for item in selections:
81− if item not in self.choices:
103+ if item not in self.choices: # pylint: disable=no-member
82 raise ValidationException('Invalid value: "{0}"'.format(selections))
104 raise ValidationException('Invalid value: "{0}"'.format(selections))
84 def _post_init(self):
106 def _post_init(self):
@@ -87,6 +109,8 @@ class MultipleChoiceSetting(EnumStringSetting):
87 self._validate_selections(self.value)
109 self._validate_selections(self.value)
89 def parse(self, data):
111 def parse(self, data):
112+ """Parse and validate ``data`` and store the result at ``self.value``
90 if data == '':
114 if data == '':
91 self.value = []
115 self.value = []
@@ -95,38 +119,47 @@ class MultipleChoiceSetting(EnumStringSetting):
95 self._validate_selections(elements)
119 self._validate_selections(elements)
96 self.value = elements
120 self.value = elements
98− def parse_form(self, data):
122+ def parse_form(self, data): # pylint: disable=missing-function-docstring
99 self.value = []
123 self.value = []
100 for choice in data:
124 for choice in data:
101− if choice in self.choices and choice not in self.value:
125+ if choice in self.choices and choice not in self.value: # pylint: disable=no-member
102 self.value.append(choice)
126 self.value.append(choice)
104 def save(self, name, resp):
128 def save(self, name, resp):
129+ """Save cookie ``name`` in the HTTP reponse obect
105 resp.set_cookie(name, ','.join(self.value), max_age=COOKIE_MAX_AGE)
131 resp.set_cookie(name, ','.join(self.value), max_age=COOKIE_MAX_AGE)
108class SetSetting(Setting):
134class SetSetting(Setting):
135+ """Setting of values of type ``set`` (comma separated string) """
109 def _post_init(self):
136 def _post_init(self):
110 if not hasattr(self, 'values'):
137 if not hasattr(self, 'values'):
111 self.values = set()
138 self.values = set()
113 def get_value(self):
140 def get_value(self):
141+ """Returns a string with comma separated values.
114 return ','.join(self.values)
143 return ','.join(self.values)
116 def parse(self, data):
145 def parse(self, data):
146+ """Parse and validate ``data`` and store the result at ``self.value``
117 if data == '':
148 if data == '':
118− self.values = set()
149+ self.values = set() # pylint: disable=attribute-defined-outside-init
121 elements = data.split(',')
152 elements = data.split(',')
122 for element in elements:
153 for element in elements:
123 self.values.add(element)
154 self.values.add(element)
125− def parse_form(self, data):
156+ def parse_form(self, data): # pylint: disable=missing-function-docstring
126 elements = data.split(',')
157 elements = data.split(',')
127− self.values = set(elements)
158+ self.values = set(elements) # pylint: disable=attribute-defined-outside-init
129 def save(self, name, resp):
160 def save(self, name, resp):
161+ """Save cookie ``name`` in the HTTP reponse obect
130 resp.set_cookie(name, ','.join(self.values), max_age=COOKIE_MAX_AGE)
163 resp.set_cookie(name, ','.join(self.values), max_age=COOKIE_MAX_AGE)
@@ -134,14 +167,19 @@ class SearchLanguageSetting(EnumStringSetting):
134 """Available choices may change, so user's value may not be in choices anymore"""
167 """Available choices may change, so user's value may not be in choices anymore"""
136 def _validate_selection(self, selection):
169 def _validate_selection(self, selection):
170+ if selection != "" and not match_language(
171+ # pylint: disable=no-member
137− if not match_language(selection, self.choices, fallback=None) and selection != "":
172+ selection, self.choices, fallback=None):
138 raise ValidationException('Invalid language code: "{0}"'.format(selection))
173 raise ValidationException('Invalid language code: "{0}"'.format(selection))
140 def parse(self, data):
175 def parse(self, data):
176+ """Parse and validate ``data`` and store the result at ``self.value``
141− if data not in self.choices and data != self.value:
178+ if data not in self.choices and data != self.value: # pylint: disable=no-member
142 # hack to give some backwards compatibility with old language cookies
179 # hack to give some backwards compatibility with old language cookies
143 data = str(data).replace('_', '-')
180 data = str(data).replace('_', '-')
144 lang = data.split('-')[0]
181 lang = data.split('-')[0]
182+ # pylint: disable=no-member
145 if data in self.choices:
183 if data in self.choices:
147 elif lang in self.choices:
185 elif lang in self.choices:
@@ -157,16 +195,21 @@ class MapSetting(Setting):
157 def _post_init(self):
195 def _post_init(self):
158 if not hasattr(self, 'map'):
196 if not hasattr(self, 'map'):
159 raise MissingArgumentException('missing argument: map')
197 raise MissingArgumentException('missing argument: map')
160− if self.value not in self.map.values():
198+ if self.value not in self.map.values(): # pylint: disable=no-member
161 raise ValidationException('Invalid default value')
199 raise ValidationException('Invalid default value')
163 def parse(self, data):
201 def parse(self, data):
202+ """Parse and validate ``data`` and store the result at ``self.value``
204+ # pylint: disable=no-member
164 if data not in self.map:
205 if data not in self.map:
165 raise ValidationException('Invalid choice: {0}'.format(data))
206 raise ValidationException('Invalid choice: {0}'.format(data))
166 self.value = self.map[data]
207 self.value = self.map[data]
167− self.key = data
208+ self.key = data # pylint: disable=attribute-defined-outside-init
169 def save(self, name, resp):
210 def save(self, name, resp):
211+ """Save cookie ``name`` in the HTTP reponse obect
170 if hasattr(self, 'key'):
213 if hasattr(self, 'key'):
171 resp.set_cookie(name, self.key, max_age=COOKIE_MAX_AGE)
214 resp.set_cookie(name, self.key, max_age=COOKIE_MAX_AGE)
@@ -180,24 +223,26 @@ class SwitchableSetting(Setting):
180 if not hasattr(self, 'choices'):
223 if not hasattr(self, 'choices'):
181 raise MissingArgumentException('missing argument: choices')
224 raise MissingArgumentException('missing argument: choices')
183− def transform_form_items(self, items):
226+ def transform_form_items(self, items): # pylint: disable=missing-function-docstring
227+ # pylint: disable=no-self-use
184 return items
228 return items
186− def transform_values(self, values):
230+ def transform_values(self, values): # pylint: disable=missing-function-docstring
231+ # pylint: disable=no-self-use
187 return values
232 return values
189− def parse_cookie(self, data):
234+ def parse_cookie(self, data): # pylint: disable=missing-function-docstring
235+ # pylint: disable=attribute-defined-outside-init
190 if data[DISABLED] != '':
236 if data[DISABLED] != '':
191 self.disabled = set(data[DISABLED].split(','))
237 self.disabled = set(data[DISABLED].split(','))
192 if data[ENABLED] != '':
238 if data[ENABLED] != '':
193 self.enabled = set(data[ENABLED].split(','))
239 self.enabled = set(data[ENABLED].split(','))
195− def parse_form(self, items):
241+ def parse_form(self, items): # pylint: disable=missing-function-docstring
196 items = self.transform_form_items(items)
242 items = self.transform_form_items(items)
198− self.disabled = set()
243+ self.disabled = set() # pylint: disable=attribute-defined-outside-init
199− self.enabled = set()
244+ self.enabled = set() # pylint: disable=attribute-defined-outside-init
200− for choice in self.choices:
245+ for choice in self.choices: # pylint: disable=no-member
201 if choice['default_on']:
246 if choice['default_on']:
202 if choice['id'] in items:
247 if choice['id'] in items:
203 self.disabled.add(choice['id'])
248 self.disabled.add(choice['id'])
@@ -205,31 +250,34 @@ class SwitchableSetting(Setting):
205 if choice['id'] not in items:
250 if choice['id'] not in items:
206 self.enabled.add(choice['id'])
251 self.enabled.add(choice['id'])
208− def save(self, resp):
253+ def save(self, resp): # pylint: disable=arguments-differ
254+ """Save cookie in the HTTP reponse obect
209 resp.set_cookie('disabled_{0}'.format(self.value), ','.join(self.disabled), max_age=COOKIE_MAX_AGE)
256 resp.set_cookie('disabled_{0}'.format(self.value), ','.join(self.disabled), max_age=COOKIE_MAX_AGE)
210 resp.set_cookie('enabled_{0}'.format(self.value), ','.join(self.enabled), max_age=COOKIE_MAX_AGE)
257 resp.set_cookie('enabled_{0}'.format(self.value), ','.join(self.enabled), max_age=COOKIE_MAX_AGE)
212− def get_disabled(self):
259+ def get_disabled(self): # pylint: disable=missing-function-docstring
213 disabled = self.disabled
260 disabled = self.disabled
214− for choice in self.choices:
261+ for choice in self.choices: # pylint: disable=no-member
215 if not choice['default_on'] and choice['id'] not in self.enabled:
262 if not choice['default_on'] and choice['id'] not in self.enabled:
216 disabled.add(choice['id'])
263 disabled.add(choice['id'])
217 return self.transform_values(disabled)
264 return self.transform_values(disabled)
219− def get_enabled(self):
266+ def get_enabled(self): # pylint: disable=missing-function-docstring
220 enabled = self.enabled
267 enabled = self.enabled
221− for choice in self.choices:
268+ for choice in self.choices: # pylint: disable=no-member
222 if choice['default_on'] and choice['id'] not in self.disabled:
269 if choice['default_on'] and choice['id'] not in self.disabled:
223 enabled.add(choice['id'])
270 enabled.add(choice['id'])
224 return self.transform_values(enabled)
271 return self.transform_values(enabled)
227class EnginesSetting(SwitchableSetting):
274class EnginesSetting(SwitchableSetting):
275+ """Engine settings"""
229 def _post_init(self):
277 def _post_init(self):
230 super(EnginesSetting, self)._post_init()
278 super(EnginesSetting, self)._post_init()
231 transformed_choices = []
279 transformed_choices = []
232− for engine_name, engine in self.choices.items():
280+ for engine_name, engine in self.choices.items(): # pylint: disable=no-member,access-member-before-definition
233 for category in engine.categories:
281 for category in engine.categories:
234 transformed_choice = dict()
282 transformed_choice = dict()
235 transformed_choice['default_on'] = not engine.disabled
283 transformed_choice['default_on'] = not engine.disabled
@@ -251,11 +299,12 @@ class EnginesSetting(SwitchableSetting):
253class PluginsSetting(SwitchableSetting):
301class PluginsSetting(SwitchableSetting):
302+ """Plugin settings"""
255 def _post_init(self):
304 def _post_init(self):
256 super(PluginsSetting, self)._post_init()
305 super(PluginsSetting, self)._post_init()
257 transformed_choices = []
306 transformed_choices = []
258− for plugin in self.choices:
307+ for plugin in self.choices: # pylint: disable=access-member-before-definition
259 transformed_choice = dict()
308 transformed_choice = dict()
260 transformed_choice['default_on'] = plugin.default_on
309 transformed_choice['default_on'] = plugin.default_on
261 transformed_choice['id'] = plugin.id
310 transformed_choice['id'] = plugin.id
@@ -272,33 +321,64 @@ class Preferences(object):
272 def __init__(self, themes, categories, engines, plugins):
321 def __init__(self, themes, categories, engines, plugins):
273 super(Preferences, self).__init__()
322 super(Preferences, self).__init__()
324+ self.key_value_settings = {
275− self.key_value_settings = {'categories': MultipleChoiceSetting(['general'], choices=categories + ['none']),
325+ 'categories': MultipleChoiceSetting(
326+ ['general'], choices=categories + ['none']
276− 'language': SearchLanguageSetting(settings['search']['default_lang'],
328+ 'language': SearchLanguageSetting(
329+ settings['search'].get('default_lang', ''),
277− choices=list(LANGUAGE_CODES) + ['']),
330+ choices=list(LANGUAGE_CODES) + ['']
278− 'locale': EnumStringSetting(settings['ui']['default_locale'],
332+ 'locale': EnumStringSetting(
333+ settings['ui'].get('default_locale', ''),
279− choices=list(settings['locales'].keys()) + ['']),
334+ choices=list(settings['locales'].keys()) + ['']
280− 'autocomplete': EnumStringSetting(settings['search']['autocomplete'],
336+ 'autocomplete': EnumStringSetting(
337+ settings['search'].get('autocomplete', ''),
281− choices=list(autocomplete.backends.keys()) + ['']),
338+ choices=list(autocomplete.backends.keys()) + ['']
340+ 'image_proxy': MapSetting(
282− 'image_proxy': MapSetting(settings['server']['image_proxy'],
341+ settings['server'].get('image_proxy', False),
283− map={'': settings['server']['image_proxy'],
343+ '': settings['server'].get('image_proxy', 0),
284− '0': False,
344+ '0': False,
285− '1': True,
345+ '1': True,
286− 'True': True,
346+ 'True': True,
287− 'False': False}),
347+ 'False': False
288− 'method': EnumStringSetting('POST', choices=('GET', 'POST')),
350+ 'method': EnumStringSetting(
352+ choices=('GET', 'POST')
354+ 'safesearch': MapSetting(
289− 'safesearch': MapSetting(settings['search']['safe_search'], map={'0': 0,
355+ settings['search'].get('safe_search', 0),
291− '2': 2}),
359+ '2': 2
292− 'theme': EnumStringSetting(settings['ui']['default_theme'], choices=themes),
362+ 'theme': EnumStringSetting(
363+ settings['ui'].get('default_theme', 'oscar'),
293− 'results_on_new_tab': MapSetting(False, map={'0': False,
366+ 'results_on_new_tab': MapSetting(
294− '1': True,
370+ '1': True,
295− 'False': False,
371+ 'False': False,
296− 'True': True}),
372+ 'True': True
297− 'doi_resolver': MultipleChoiceSetting(['oadoi.org'], choices=DOI_RESOLVERS),
375+ 'doi_resolver': MultipleChoiceSetting(
376+ ['oadoi.org'], choices=DOI_RESOLVERS
298− 'oscar-style': EnumStringSetting(
378+ 'oscar-style': EnumStringSetting(
299− settings['ui'].get('theme_args', {}).get('oscar_style', 'logicodev'),
379+ settings['ui'].get('theme_args', {}).get('oscar_style', 'logicodev'),
300− choices=['', 'logicodev', 'logicodev-dark', 'pointhi']),
380+ choices=['', 'logicodev', 'logicodev-dark', 'pointhi']),
303 self.engines = EnginesSetting('engines', choices=engines)
383 self.engines = EnginesSetting('engines', choices=engines)
304 self.plugins = PluginsSetting('plugins', choices=plugins)
384 self.plugins = PluginsSetting('plugins', choices=plugins)
@@ -306,6 +386,7 @@ class Preferences(object):
306 self.unknown_params = {}
386 self.unknown_params = {}
308 def get_as_url_params(self):
388 def get_as_url_params(self):
389+ """Return preferences as URL parameters"""
309 settings_kv = {}
390 settings_kv = {}
310 for k, v in self.key_value_settings.items():
391 for k, v in self.key_value_settings.items():
311 if isinstance(v, MultipleChoiceSetting):
392 if isinstance(v, MultipleChoiceSetting):
@@ -324,6 +405,7 @@ class Preferences(object):
324 return urlsafe_b64encode(compress(urlencode(settings_kv).encode('utf-8'))).decode('utf-8')
405 return urlsafe_b64encode(compress(urlencode(settings_kv).encode('utf-8'))).decode('utf-8')
326 def parse_encoded_data(self, input_data):
407 def parse_encoded_data(self, input_data):
408+ """parse (base64) preferences from request (``flask.request.form['preferences']``)"""
327 decoded_data = decompress(urlsafe_b64decode(input_data.encode('utf-8')))
409 decoded_data = decompress(urlsafe_b64decode(input_data.encode('utf-8')))
328 dict_data = {}
410 dict_data = {}
329 for x, y in parse_qs(decoded_data).items():
411 for x, y in parse_qs(decoded_data).items():
@@ -331,6 +413,7 @@ class Preferences(object):
331 self.parse_dict(dict_data)
413 self.parse_dict(dict_data)
333 def parse_dict(self, input_data):
415 def parse_dict(self, input_data):
416+ """parse preferences from request (``flask.request.form``)"""
334 for user_setting_name, user_setting in input_data.items():
417 for user_setting_name, user_setting in input_data.items():
335 if user_setting_name in self.key_value_settings:
418 if user_setting_name in self.key_value_settings:
336 self.key_value_settings[user_setting_name].parse(user_setting)
419 self.key_value_settings[user_setting_name].parse(user_setting)
@@ -351,6 +434,7 @@ class Preferences(object):
351 self.unknown_params[user_setting_name] = user_setting
434 self.unknown_params[user_setting_name] = user_setting
353 def parse_form(self, input_data):
436 def parse_form(self, input_data):
437+ """Parse formular (``<input>``) data from a ``flask.request.form``"""
354 disabled_engines = []
438 disabled_engines = []
355 enabled_categories = []
439 enabled_categories = []
356 disabled_plugins = []
440 disabled_plugins = []
@@ -373,12 +457,18 @@ class Preferences(object):
374 # cannot be used in case of engines or plugins
458 # cannot be used in case of engines or plugins
375 def get_value(self, user_setting_name):
459 def get_value(self, user_setting_name):
460+ """Returns the value for ``user_setting_name``
376 if user_setting_name in self.key_value_settings:
463 if user_setting_name in self.key_value_settings:
377− return self.key_value_settings[user_setting_name].get_value()
464+ ret_val = self.key_value_settings[user_setting_name].get_value()
378 if user_setting_name in self.unknown_params:
465 if user_setting_name in self.unknown_params:
379− return self.unknown_params[user_setting_name]
466+ ret_val = self.unknown_params[user_setting_name]
381 def save(self, resp):
469 def save(self, resp):
470+ """Save cookie in the HTTP reponse obect
382 for user_setting_name, user_setting in self.key_value_settings.items():
472 for user_setting_name, user_setting in self.key_value_settings.items():
383 user_setting.save(user_setting_name, resp)
473 user_setting.save(user_setting_name, resp)
384 self.engines.save(resp)
474 self.engines.save(resp)
@@ -388,7 +478,7 @@ class Preferences(object):
388 resp.set_cookie(k, v, max_age=COOKIE_MAX_AGE)
478 resp.set_cookie(k, v, max_age=COOKIE_MAX_AGE)
389 return resp
479 return resp
391− def validate_token(self, engine):
481+ def validate_token(self, engine): # pylint: disable=missing-function-docstring
392 valid = True
482 valid = True
393 if hasattr(engine, 'tokens') and engine.tokens:
483 if hasattr(engine, 'tokens') and engine.tokens:
394 valid = False
484 valid = False