@@ -1,3 +1,9 @@
1+# SPDX-License-Identifier: AGPL-3.0-or-later
2+"""Searx preferences implementation.
3+"""
4+
5+# pylint: disable=useless-object-inheritance
6+
17from base64 import urlsafe_b64encode, urlsafe_b64decode
28from zlib import compress, decompress
39from sys import version
@@ -8,6 +14,7 @@ from searx.utils import match_language
814from searx.url_utils import parse_qs, urlencode
915
1016if version[0] == '3':
17+ # pylint: disable=invalid-name
1118 unicode = str
1219
1320
@@ -20,11 +27,14 @@ DOI_RESOLVERS = list(settings['doi_resolvers'])
2027
2128
2229class MissingArgumentException(Exception):
23− pass
30+ """Exption from ``cls._post_init`` when a argument is missed.
31+ """
2432
2533
2634class ValidationException(Exception):
27− pass
35+
36+ """Exption from ``cls._post_init`` when configuration value is invalid.
37+ """
2838
2939
3040class Setting(object):
@@ -42,33 +52,45 @@ class Setting(object):
4252 pass
4353
4454 def parse(self, data):
55+ """Parse ``data`` and store the result at ``self.value``
56+
57+ If needed, its overwritten in the inheritance.
58+ """
4559 self.value = data
4660
4761 def get_value(self):
62+ """Returns the value of the setting
63+
64+ If needed, its overwritten in the inheritance.
65+ """
4866 return self.value
4967
5068 def save(self, name, resp):
69+ """Save cookie ``name`` in the HTTP reponse obect
70+
71+ If needed, its overwritten in the inheritance."""
5172 resp.set_cookie(name, self.value, max_age=COOKIE_MAX_AGE)
5273
5374
5475class StringSetting(Setting):
5576 """Setting of plain string values"""
56− pass
5777
5878
5979class EnumStringSetting(Setting):
6080 """Setting of a value which can only come from the given choices"""
6181
62− def _validate_selection(self, selection):
63− if selection not in self.choices:
64− raise ValidationException('Invalid value: "{0}"'.format(selection))
65−
6682 def _post_init(self):
6783 if not hasattr(self, 'choices'):
6884 raise MissingArgumentException('Missing argument: choices')
6985 self._validate_selection(self.value)
7086
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))
90+
7191 def parse(self, data):
92+ """Parse and validate ``data`` and store the result at ``self.value``
93+ """
7294 self._validate_selection(data)
7395 self.value = data
7496
@@ -78,7 +100,7 @@ class MultipleChoiceSetting(EnumStringSetting):
78100
79101 def _validate_selections(self, selections):
80102 for item in selections:
81− if item not in self.choices:
103+ if item not in self.choices: # pylint: disable=no-member
82104 raise ValidationException('Invalid value: "{0}"'.format(selections))
83105
84106 def _post_init(self):
@@ -87,6 +109,8 @@ class MultipleChoiceSetting(EnumStringSetting):
87109 self._validate_selections(self.value)
88110
89111 def parse(self, data):
112+ """Parse and validate ``data`` and store the result at ``self.value``
113+ """
90114 if data == '':
91115 self.value = []
92116 return
@@ -95,38 +119,47 @@ class MultipleChoiceSetting(EnumStringSetting):
95119 self._validate_selections(elements)
96120 self.value = elements
97121
98− def parse_form(self, data):
122+ def parse_form(self, data): # pylint: disable=missing-function-docstring
99123 self.value = []
100124 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
102126 self.value.append(choice)
103127
104128 def save(self, name, resp):
129+ """Save cookie ``name`` in the HTTP reponse obect
130+ """
105131 resp.set_cookie(name, ','.join(self.value), max_age=COOKIE_MAX_AGE)
106132
107133
108134class SetSetting(Setting):
135+ """Setting of values of type ``set`` (comma separated string) """
109136 def _post_init(self):
110137 if not hasattr(self, 'values'):
111138 self.values = set()
112139
113140 def get_value(self):
141+ """Returns a string with comma separated values.
142+ """
114143 return ','.join(self.values)
115144
116145 def parse(self, data):
146+ """Parse and validate ``data`` and store the result at ``self.value``
147+ """
117148 if data == '':
118− self.values = set()
149+ self.values = set() # pylint: disable=attribute-defined-outside-init
119150 return
120151
121152 elements = data.split(',')
122153 for element in elements:
123154 self.values.add(element)
124155
125− def parse_form(self, data):
156+ def parse_form(self, data): # pylint: disable=missing-function-docstring
126157 elements = data.split(',')
127− self.values = set(elements)
158+ self.values = set(elements) # pylint: disable=attribute-defined-outside-init
128159
129160 def save(self, name, resp):
161+ """Save cookie ``name`` in the HTTP reponse obect
162+ """
130163 resp.set_cookie(name, ','.join(self.values), max_age=COOKIE_MAX_AGE)
131164
132165
@@ -134,14 +167,19 @@ class SearchLanguageSetting(EnumStringSetting):
134167 """Available choices may change, so user's value may not be in choices anymore"""
135168
136169 def _validate_selection(self, selection):
137− if not match_language(selection, self.choices, fallback=None) and selection != "":
170+ if selection != "" and not match_language(
171+ # pylint: disable=no-member
172+ selection, self.choices, fallback=None):
138173 raise ValidationException('Invalid language code: "{0}"'.format(selection))
139174
140175 def parse(self, data):
141− if data not in self.choices and data != self.value:
176+ """Parse and validate ``data`` and store the result at ``self.value``
177+ """
178+ if data not in self.choices and data != self.value: # pylint: disable=no-member
142179 # hack to give some backwards compatibility with old language cookies
143180 data = str(data).replace('_', '-')
144181 lang = data.split('-')[0]
182+ # pylint: disable=no-member
145183 if data in self.choices:
146184 pass
147185 elif lang in self.choices:
@@ -157,16 +195,21 @@ class MapSetting(Setting):
157195 def _post_init(self):
158196 if not hasattr(self, 'map'):
159197 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
161199 raise ValidationException('Invalid default value')
162200
163201 def parse(self, data):
202+ """Parse and validate ``data`` and store the result at ``self.value``
203+ """
204+ # pylint: disable=no-member
164205 if data not in self.map:
165206 raise ValidationException('Invalid choice: {0}'.format(data))
166207 self.value = self.map[data]
167− self.key = data
208+ self.key = data # pylint: disable=attribute-defined-outside-init
168209
169210 def save(self, name, resp):
211+ """Save cookie ``name`` in the HTTP reponse obect
212+ """
170213 if hasattr(self, 'key'):
171214 resp.set_cookie(name, self.key, max_age=COOKIE_MAX_AGE)
172215
@@ -180,24 +223,26 @@ class SwitchableSetting(Setting):
180223 if not hasattr(self, 'choices'):
181224 raise MissingArgumentException('missing argument: choices')
182225
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
184228 return items
185229
186− def transform_values(self, values):
230+ def transform_values(self, values): # pylint: disable=missing-function-docstring
231+ # pylint: disable=no-self-use
187232 return values
188233
189− def parse_cookie(self, data):
234+ def parse_cookie(self, data): # pylint: disable=missing-function-docstring
235+ # pylint: disable=attribute-defined-outside-init
190236 if data[DISABLED] != '':
191237 self.disabled = set(data[DISABLED].split(','))
192238 if data[ENABLED] != '':
193239 self.enabled = set(data[ENABLED].split(','))
194240
195− def parse_form(self, items):
241+ def parse_form(self, items): # pylint: disable=missing-function-docstring
196242 items = self.transform_form_items(items)
197−
198− self.disabled = set()
199− self.enabled = set()
200− for choice in self.choices:
243+ self.disabled = set() # pylint: disable=attribute-defined-outside-init
244+ self.enabled = set() # pylint: disable=attribute-defined-outside-init
245+ for choice in self.choices: # pylint: disable=no-member
201246 if choice['default_on']:
202247 if choice['id'] in items:
203248 self.disabled.add(choice['id'])
@@ -205,31 +250,34 @@ class SwitchableSetting(Setting):
205250 if choice['id'] not in items:
206251 self.enabled.add(choice['id'])
207252
208− def save(self, resp):
253+ def save(self, resp): # pylint: disable=arguments-differ
254+ """Save cookie in the HTTP reponse obect
255+ """
209256 resp.set_cookie('disabled_{0}'.format(self.value), ','.join(self.disabled), max_age=COOKIE_MAX_AGE)
210257 resp.set_cookie('enabled_{0}'.format(self.value), ','.join(self.enabled), max_age=COOKIE_MAX_AGE)
211258
212− def get_disabled(self):
259+ def get_disabled(self): # pylint: disable=missing-function-docstring
213260 disabled = self.disabled
214− for choice in self.choices:
261+ for choice in self.choices: # pylint: disable=no-member
215262 if not choice['default_on'] and choice['id'] not in self.enabled:
216263 disabled.add(choice['id'])
217264 return self.transform_values(disabled)
218265
219− def get_enabled(self):
266+ def get_enabled(self): # pylint: disable=missing-function-docstring
220267 enabled = self.enabled
221− for choice in self.choices:
268+ for choice in self.choices: # pylint: disable=no-member
222269 if choice['default_on'] and choice['id'] not in self.disabled:
223270 enabled.add(choice['id'])
224271 return self.transform_values(enabled)
225272
226273
227274class EnginesSetting(SwitchableSetting):
275+ """Engine settings"""
228276
229277 def _post_init(self):
230278 super(EnginesSetting, self)._post_init()
231279 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
233281 for category in engine.categories:
234282 transformed_choice = dict()
235283 transformed_choice['default_on'] = not engine.disabled
@@ -251,11 +299,12 @@ class EnginesSetting(SwitchableSetting):
251299
252300
253301class PluginsSetting(SwitchableSetting):
302+ """Plugin settings"""
254303
255304 def _post_init(self):
256305 super(PluginsSetting, self)._post_init()
257306 transformed_choices = []
258− for plugin in self.choices:
307+ for plugin in self.choices: # pylint: disable=access-member-before-definition
259308 transformed_choice = dict()
260309 transformed_choice['default_on'] = plugin.default_on
261310 transformed_choice['id'] = plugin.id
@@ -272,33 +321,64 @@ class Preferences(object):
272321 def __init__(self, themes, categories, engines, plugins):
273322 super(Preferences, self).__init__()
274323
275− self.key_value_settings = {'categories': MultipleChoiceSetting(['general'], choices=categories + ['none']),
276− 'language': SearchLanguageSetting(settings['search']['default_lang'],
277− choices=list(LANGUAGE_CODES) + ['']),
278− 'locale': EnumStringSetting(settings['ui']['default_locale'],
279− choices=list(settings['locales'].keys()) + ['']),
280− 'autocomplete': EnumStringSetting(settings['search']['autocomplete'],
281− choices=list(autocomplete.backends.keys()) + ['']),
282− 'image_proxy': MapSetting(settings['server']['image_proxy'],
283− map={'': settings['server']['image_proxy'],
284− '0': False,
285− '1': True,
286− 'True': True,
287− 'False': False}),
288− 'method': EnumStringSetting('POST', choices=('GET', 'POST')),
289− 'safesearch': MapSetting(settings['search']['safe_search'], map={'0': 0,
290− '1': 1,
291− '2': 2}),
292− 'theme': EnumStringSetting(settings['ui']['default_theme'], choices=themes),
293− 'results_on_new_tab': MapSetting(False, map={'0': False,
294− '1': True,
295− 'False': False,
296− 'True': True}),
297− 'doi_resolver': MultipleChoiceSetting(['oadoi.org'], choices=DOI_RESOLVERS),
298− 'oscar-style': EnumStringSetting(
299− settings['ui'].get('theme_args', {}).get('oscar_style', 'logicodev'),
300− choices=['', 'logicodev', 'logicodev-dark', 'pointhi']),
301− }
324+ self.key_value_settings = {
325+ 'categories': MultipleChoiceSetting(
326+ ['general'], choices=categories + ['none']
327+ ),
328+ 'language': SearchLanguageSetting(
329+ settings['search'].get('default_lang', ''),
330+ choices=list(LANGUAGE_CODES) + ['']
331+ ),
332+ 'locale': EnumStringSetting(
333+ settings['ui'].get('default_locale', ''),
334+ choices=list(settings['locales'].keys()) + ['']
335+ ),
336+ 'autocomplete': EnumStringSetting(
337+ settings['search'].get('autocomplete', ''),
338+ choices=list(autocomplete.backends.keys()) + ['']
339+ ),
340+ 'image_proxy': MapSetting(
341+ settings['server'].get('image_proxy', False),
342+ map={
343+ '': settings['server'].get('image_proxy', 0),
344+ '0': False,
345+ '1': True,
346+ 'True': True,
347+ 'False': False
348+ }
349+ ),
350+ 'method': EnumStringSetting(
351+ 'POST',
352+ choices=('GET', 'POST')
353+ ),
354+ 'safesearch': MapSetting(
355+ settings['search'].get('safe_search', 0),
356+ map={
357+ '0': 0,
358+ '1': 1,
359+ '2': 2
360+ }
361+ ),
362+ 'theme': EnumStringSetting(
363+ settings['ui'].get('default_theme', 'oscar'),
364+ choices=themes
365+ ),
366+ 'results_on_new_tab': MapSetting(
367+ False,
368+ map={
369+ '0': False,
370+ '1': True,
371+ 'False': False,
372+ 'True': True
373+ }
374+ ),
375+ 'doi_resolver': MultipleChoiceSetting(
376+ ['oadoi.org'], choices=DOI_RESOLVERS
377+ ),
378+ 'oscar-style': EnumStringSetting(
379+ settings['ui'].get('theme_args', {}).get('oscar_style', 'logicodev'),
380+ choices=['', 'logicodev', 'logicodev-dark', 'pointhi']),
381+ }
302382
303383 self.engines = EnginesSetting('engines', choices=engines)
304384 self.plugins = PluginsSetting('plugins', choices=plugins)
@@ -306,6 +386,7 @@ class Preferences(object):
306386 self.unknown_params = {}
307387
308388 def get_as_url_params(self):
389+ """Return preferences as URL parameters"""
309390 settings_kv = {}
310391 for k, v in self.key_value_settings.items():
311392 if isinstance(v, MultipleChoiceSetting):
@@ -324,6 +405,7 @@ class Preferences(object):
324405 return urlsafe_b64encode(compress(urlencode(settings_kv).encode('utf-8'))).decode('utf-8')
325406
326407 def parse_encoded_data(self, input_data):
408+ """parse (base64) preferences from request (``flask.request.form['preferences']``)"""
327409 decoded_data = decompress(urlsafe_b64decode(input_data.encode('utf-8')))
328410 dict_data = {}
329411 for x, y in parse_qs(decoded_data).items():
@@ -331,6 +413,7 @@ class Preferences(object):
331413 self.parse_dict(dict_data)
332414
333415 def parse_dict(self, input_data):
416+ """parse preferences from request (``flask.request.form``)"""
334417 for user_setting_name, user_setting in input_data.items():
335418 if user_setting_name in self.key_value_settings:
336419 self.key_value_settings[user_setting_name].parse(user_setting)
@@ -351,6 +434,7 @@ class Preferences(object):
351434 self.unknown_params[user_setting_name] = user_setting
352435
353436 def parse_form(self, input_data):
437+ """Parse formular (``<input>``) data from a ``flask.request.form``"""
354438 disabled_engines = []
355439 enabled_categories = []
356440 disabled_plugins = []
@@ -373,12 +457,18 @@ class Preferences(object):
373457
374458 # cannot be used in case of engines or plugins
375459 def get_value(self, user_setting_name):
460+ """Returns the value for ``user_setting_name``
461+ """
462+ ret_val = None
376463 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()
378465 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]
467+ return ret_val
380468
381469 def save(self, resp):
470+ """Save cookie in the HTTP reponse obect
471+ """
382472 for user_setting_name, user_setting in self.key_value_settings.items():
383473 user_setting.save(user_setting_name, resp)
384474 self.engines.save(resp)
@@ -388,7 +478,7 @@ class Preferences(object):
388478 resp.set_cookie(k, v, max_age=COOKIE_MAX_AGE)
389479 return resp
390480
391− def validate_token(self, engine):
481+ def validate_token(self, engine): # pylint: disable=missing-function-docstring
392482 valid = True
393483 if hasattr(engine, 'tokens') and engine.tokens:
394484 valid = False