preferences.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. from base64 import urlsafe_b64encode, urlsafe_b64decode
  2. from zlib import compress, decompress
  3. from sys import version
  4. from searx import settings, autocomplete
  5. from searx.languages import language_codes as languages
  6. from searx.utils import match_language
  7. from searx.url_utils import parse_qs, urlencode
  8. if version[0] == '3':
  9. unicode = str
  10. COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5 # 5 years
  11. LANGUAGE_CODES = [l[0] for l in languages]
  12. LANGUAGE_CODES.append('all')
  13. DISABLED = 0
  14. ENABLED = 1
  15. DOI_RESOLVERS = list(settings['doi_resolvers'])
  16. class MissingArgumentException(Exception):
  17. pass
  18. class ValidationException(Exception):
  19. pass
  20. class Setting(object):
  21. """Base class of user settings"""
  22. def __init__(self, default_value, **kwargs):
  23. super(Setting, self).__init__()
  24. self.value = default_value
  25. for key, value in kwargs.items():
  26. setattr(self, key, value)
  27. self._post_init()
  28. def _post_init(self):
  29. pass
  30. def parse(self, data):
  31. self.value = data
  32. def get_value(self):
  33. return self.value
  34. def save(self, name, resp):
  35. resp.set_cookie(name, self.value, max_age=COOKIE_MAX_AGE)
  36. class StringSetting(Setting):
  37. """Setting of plain string values"""
  38. pass
  39. class EnumStringSetting(Setting):
  40. """Setting of a value which can only come from the given choices"""
  41. def _validate_selection(self, selection):
  42. if selection not in self.choices:
  43. raise ValidationException('Invalid value: "{0}"'.format(selection))
  44. def _post_init(self):
  45. if not hasattr(self, 'choices'):
  46. raise MissingArgumentException('Missing argument: choices')
  47. self._validate_selection(self.value)
  48. def parse(self, data):
  49. self._validate_selection(data)
  50. self.value = data
  51. class MultipleChoiceSetting(EnumStringSetting):
  52. """Setting of values which can only come from the given choices"""
  53. def _validate_selections(self, selections):
  54. for item in selections:
  55. if item not in self.choices:
  56. raise ValidationException('Invalid value: "{0}"'.format(selections))
  57. def _post_init(self):
  58. if not hasattr(self, 'choices'):
  59. raise MissingArgumentException('Missing argument: choices')
  60. self._validate_selections(self.value)
  61. def parse(self, data):
  62. if data == '':
  63. self.value = []
  64. return
  65. elements = data.split(',')
  66. self._validate_selections(elements)
  67. self.value = elements
  68. def parse_form(self, data):
  69. self.value = []
  70. for choice in data:
  71. if choice in self.choices and choice not in self.value:
  72. self.value.append(choice)
  73. def save(self, name, resp):
  74. resp.set_cookie(name, ','.join(self.value), max_age=COOKIE_MAX_AGE)
  75. class SetSetting(Setting):
  76. def _post_init(self):
  77. if not hasattr(self, 'values'):
  78. self.values = set()
  79. def get_value(self):
  80. return ','.join(self.values)
  81. def parse(self, data):
  82. if data == '':
  83. self.values = set()
  84. return
  85. elements = data.split(',')
  86. for element in elements:
  87. self.values.add(element)
  88. def parse_form(self, data):
  89. elements = data.split(',')
  90. self.values = set(elements)
  91. def save(self, name, resp):
  92. resp.set_cookie(name, ','.join(self.values), max_age=COOKIE_MAX_AGE)
  93. class SearchLanguageSetting(EnumStringSetting):
  94. """Available choices may change, so user's value may not be in choices anymore"""
  95. def _validate_selection(self, selection):
  96. if not match_language(selection, self.choices, fallback=None) and selection != "":
  97. raise ValidationException('Invalid language code: "{0}"'.format(selection))
  98. def parse(self, data):
  99. if data not in self.choices and data != self.value:
  100. # hack to give some backwards compatibility with old language cookies
  101. data = str(data).replace('_', '-')
  102. lang = data.split('-')[0]
  103. if data in self.choices:
  104. pass
  105. elif lang in self.choices:
  106. data = lang
  107. else:
  108. data = self.value
  109. self.value = data
  110. class MapSetting(Setting):
  111. """Setting of a value that has to be translated in order to be storable"""
  112. def _post_init(self):
  113. if not hasattr(self, 'map'):
  114. raise MissingArgumentException('missing argument: map')
  115. if self.value not in self.map.values():
  116. raise ValidationException('Invalid default value')
  117. def parse(self, data):
  118. if data not in self.map:
  119. raise ValidationException('Invalid choice: {0}'.format(data))
  120. self.value = self.map[data]
  121. self.key = data
  122. def save(self, name, resp):
  123. if hasattr(self, 'key'):
  124. resp.set_cookie(name, self.key, max_age=COOKIE_MAX_AGE)
  125. class SwitchableSetting(Setting):
  126. """ Base class for settings that can be turned on && off"""
  127. def _post_init(self):
  128. self.disabled = set()
  129. self.enabled = set()
  130. if not hasattr(self, 'choices'):
  131. raise MissingArgumentException('missing argument: choices')
  132. def transform_form_items(self, items):
  133. return items
  134. def transform_values(self, values):
  135. return values
  136. def parse_cookie(self, data):
  137. if data[DISABLED] != '':
  138. self.disabled = set(data[DISABLED].split(','))
  139. if data[ENABLED] != '':
  140. self.enabled = set(data[ENABLED].split(','))
  141. def parse_form(self, items):
  142. items = self.transform_form_items(items)
  143. self.disabled = set()
  144. self.enabled = set()
  145. for choice in self.choices:
  146. if choice['default_on']:
  147. if choice['id'] in items:
  148. self.disabled.add(choice['id'])
  149. else:
  150. if choice['id'] not in items:
  151. self.enabled.add(choice['id'])
  152. def save(self, resp):
  153. resp.set_cookie('disabled_{0}'.format(self.value), ','.join(self.disabled), max_age=COOKIE_MAX_AGE)
  154. resp.set_cookie('enabled_{0}'.format(self.value), ','.join(self.enabled), max_age=COOKIE_MAX_AGE)
  155. def get_disabled(self):
  156. disabled = self.disabled
  157. for choice in self.choices:
  158. if not choice['default_on'] and choice['id'] not in self.enabled:
  159. disabled.add(choice['id'])
  160. return self.transform_values(disabled)
  161. def get_enabled(self):
  162. enabled = self.enabled
  163. for choice in self.choices:
  164. if choice['default_on'] and choice['id'] not in self.disabled:
  165. enabled.add(choice['id'])
  166. return self.transform_values(enabled)
  167. class EnginesSetting(SwitchableSetting):
  168. def _post_init(self):
  169. super(EnginesSetting, self)._post_init()
  170. transformed_choices = []
  171. for engine_name, engine in self.choices.items():
  172. for category in engine.categories:
  173. transformed_choice = dict()
  174. transformed_choice['default_on'] = not engine.disabled
  175. transformed_choice['id'] = '{}__{}'.format(engine_name, category)
  176. transformed_choices.append(transformed_choice)
  177. self.choices = transformed_choices
  178. def transform_form_items(self, items):
  179. return [item[len('engine_'):].replace('_', ' ').replace(' ', '__') for item in items]
  180. def transform_values(self, values):
  181. if len(values) == 1 and next(iter(values)) == '':
  182. return list()
  183. transformed_values = []
  184. for value in values:
  185. engine, category = value.split('__')
  186. transformed_values.append((engine, category))
  187. return transformed_values
  188. class PluginsSetting(SwitchableSetting):
  189. def _post_init(self):
  190. super(PluginsSetting, self)._post_init()
  191. transformed_choices = []
  192. for plugin in self.choices:
  193. transformed_choice = dict()
  194. transformed_choice['default_on'] = plugin.default_on
  195. transformed_choice['id'] = plugin.id
  196. transformed_choices.append(transformed_choice)
  197. self.choices = transformed_choices
  198. def transform_form_items(self, items):
  199. return [item[len('plugin_'):] for item in items]
  200. class Preferences(object):
  201. """Validates and saves preferences to cookies"""
  202. def __init__(self, themes, categories, engines, plugins):
  203. super(Preferences, self).__init__()
  204. self.key_value_settings = {'categories': MultipleChoiceSetting(['general'], choices=categories + ['none']),
  205. 'language': SearchLanguageSetting(settings['search']['default_lang'],
  206. choices=list(LANGUAGE_CODES) + ['']),
  207. 'locale': EnumStringSetting(settings['ui']['default_locale'],
  208. choices=list(settings['locales'].keys()) + ['']),
  209. 'autocomplete': EnumStringSetting(settings['search']['autocomplete'],
  210. choices=list(autocomplete.backends.keys()) + ['']),
  211. 'image_proxy': MapSetting(settings['server']['image_proxy'],
  212. map={'': settings['server']['image_proxy'],
  213. '0': False,
  214. '1': True,
  215. 'True': True,
  216. 'False': False}),
  217. 'method': EnumStringSetting('POST', choices=('GET', 'POST')),
  218. 'safesearch': MapSetting(settings['search']['safe_search'], map={'0': 0,
  219. '1': 1,
  220. '2': 2}),
  221. 'theme': EnumStringSetting(settings['ui']['default_theme'], choices=themes),
  222. 'results_on_new_tab': MapSetting(False, map={'0': False,
  223. '1': True,
  224. 'False': False,
  225. 'True': True}),
  226. 'doi_resolver': MultipleChoiceSetting(['oadoi.org'], choices=DOI_RESOLVERS),
  227. 'oscar-style': EnumStringSetting(
  228. settings['ui'].get('theme_args', {}).get('oscar_style', 'logicodev'),
  229. choices=['', 'logicodev', 'logicodev-dark', 'pointhi']),
  230. }
  231. self.engines = EnginesSetting('engines', choices=engines)
  232. self.plugins = PluginsSetting('plugins', choices=plugins)
  233. self.tokens = SetSetting('tokens')
  234. self.unknown_params = {}
  235. def get_as_url_params(self):
  236. settings_kv = {}
  237. for k, v in self.key_value_settings.items():
  238. if isinstance(v, MultipleChoiceSetting):
  239. settings_kv[k] = ','.join(v.get_value())
  240. else:
  241. settings_kv[k] = v.get_value()
  242. settings_kv['disabled_engines'] = ','.join(self.engines.disabled)
  243. settings_kv['enabled_engines'] = ','.join(self.engines.enabled)
  244. settings_kv['disabled_plugins'] = ','.join(self.plugins.disabled)
  245. settings_kv['enabled_plugins'] = ','.join(self.plugins.enabled)
  246. settings_kv['tokens'] = ','.join(self.tokens.values)
  247. return urlsafe_b64encode(compress(urlencode(settings_kv).encode('utf-8'))).decode('utf-8')
  248. def parse_encoded_data(self, input_data):
  249. decoded_data = decompress(urlsafe_b64decode(input_data.encode('utf-8')))
  250. dict_data = {}
  251. for x, y in parse_qs(decoded_data).items():
  252. dict_data[x.decode('utf8')] = y[0].decode('utf8')
  253. self.parse_dict(dict_data)
  254. def parse_dict(self, input_data):
  255. for user_setting_name, user_setting in input_data.items():
  256. if user_setting_name in self.key_value_settings:
  257. self.key_value_settings[user_setting_name].parse(user_setting)
  258. elif user_setting_name == 'disabled_engines':
  259. self.engines.parse_cookie((input_data.get('disabled_engines', ''),
  260. input_data.get('enabled_engines', '')))
  261. elif user_setting_name == 'disabled_plugins':
  262. self.plugins.parse_cookie((input_data.get('disabled_plugins', ''),
  263. input_data.get('enabled_plugins', '')))
  264. elif user_setting_name == 'tokens':
  265. self.tokens.parse(user_setting)
  266. elif not any(user_setting_name.startswith(x) for x in [
  267. 'enabled_',
  268. 'disabled_',
  269. 'engine_',
  270. 'category_',
  271. 'plugin_']):
  272. self.unknown_params[user_setting_name] = user_setting
  273. def parse_form(self, input_data):
  274. disabled_engines = []
  275. enabled_categories = []
  276. disabled_plugins = []
  277. for user_setting_name, user_setting in input_data.items():
  278. if user_setting_name in self.key_value_settings:
  279. self.key_value_settings[user_setting_name].parse(user_setting)
  280. elif user_setting_name.startswith('engine_'):
  281. disabled_engines.append(user_setting_name)
  282. elif user_setting_name.startswith('category_'):
  283. enabled_categories.append(user_setting_name[len('category_'):])
  284. elif user_setting_name.startswith('plugin_'):
  285. disabled_plugins.append(user_setting_name)
  286. elif user_setting_name == 'tokens':
  287. self.tokens.parse_form(user_setting)
  288. else:
  289. self.unknown_params[user_setting_name] = user_setting
  290. self.key_value_settings['categories'].parse_form(enabled_categories)
  291. self.engines.parse_form(disabled_engines)
  292. self.plugins.parse_form(disabled_plugins)
  293. # cannot be used in case of engines or plugins
  294. def get_value(self, user_setting_name):
  295. if user_setting_name in self.key_value_settings:
  296. return self.key_value_settings[user_setting_name].get_value()
  297. if user_setting_name in self.unknown_params:
  298. return self.unknown_params[user_setting_name]
  299. def save(self, resp):
  300. for user_setting_name, user_setting in self.key_value_settings.items():
  301. user_setting.save(user_setting_name, resp)
  302. self.engines.save(resp)
  303. self.plugins.save(resp)
  304. self.tokens.save('tokens', resp)
  305. for k, v in self.unknown_params.items():
  306. resp.set_cookie(k, v, max_age=COOKIE_MAX_AGE)
  307. return resp
  308. def validate_token(self, engine):
  309. valid = True
  310. if hasattr(engine, 'tokens') and engine.tokens:
  311. valid = False
  312. for token in self.tokens.values:
  313. if token in engine.tokens:
  314. valid = True
  315. break
  316. return valid