preferences.py 13.7 KB
Newer Older
1 2 3 4
from base64 import urlsafe_b64encode, urlsafe_b64decode
from zlib import compress, decompress
from sys import version

Noemi Vanyi's avatar
Noemi Vanyi committed
5 6
from searx import settings, autocomplete
from searx.languages import language_codes as languages
7 8 9 10
from searx.url_utils import parse_qs, urlencode

if version[0] == '3':
    unicode = str
Noemi Vanyi's avatar
Noemi Vanyi committed
11 12 13 14


COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5  # 5 years
LANGUAGE_CODES = [l[0] for l in languages]
15
LANGUAGE_CODES.append('all')
Noemi Vanyi's avatar
Noemi Vanyi committed
16 17
DISABLED = 0
ENABLED = 1
18
DOI_RESOLVERS = list(settings['doi_resolvers'])
Noemi Vanyi's avatar
Noemi Vanyi committed
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34


class MissingArgumentException(Exception):
    pass


class ValidationException(Exception):
    pass


class Setting(object):
    """Base class of user settings"""

    def __init__(self, default_value, **kwargs):
        super(Setting, self).__init__()
        self.value = default_value
Adam Tauber's avatar
Adam Tauber committed
35
        for key, value in kwargs.items():
Noemi Vanyi's avatar
Noemi Vanyi committed
36 37 38 39 40 41 42 43 44 45 46 47 48 49
            setattr(self, key, value)

        self._post_init()

    def _post_init(self):
        pass

    def parse(self, data):
        self.value = data

    def get_value(self):
        return self.value

    def save(self, name, resp):
Adam Tauber's avatar
Adam Tauber committed
50
        resp.set_cookie(name, self.value, max_age=COOKIE_MAX_AGE)
Noemi Vanyi's avatar
Noemi Vanyi committed
51 52 53 54 55 56 57 58 59 60


class StringSetting(Setting):
    """Setting of plain string values"""
    pass


class EnumStringSetting(Setting):
    """Setting of a value which can only come from the given choices"""

61 62 63 64
    def _validate_selection(self, selection):
        if selection not in self.choices:
            raise ValidationException('Invalid value: "{0}"'.format(selection))

Noemi Vanyi's avatar
Noemi Vanyi committed
65 66 67
    def _post_init(self):
        if not hasattr(self, 'choices'):
            raise MissingArgumentException('Missing argument: choices')
68
        self._validate_selection(self.value)
Noemi Vanyi's avatar
Noemi Vanyi committed
69 70

    def parse(self, data):
71
        self._validate_selection(data)
Noemi Vanyi's avatar
Noemi Vanyi committed
72 73 74 75 76 77
        self.value = data


class MultipleChoiceSetting(EnumStringSetting):
    """Setting of values which can only come from the given choices"""

78 79 80 81 82
    def _validate_selections(self, selections):
        for item in selections:
            if item not in self.choices:
                raise ValidationException('Invalid value: "{0}"'.format(selections))

Noemi Vanyi's avatar
Noemi Vanyi committed
83 84 85
    def _post_init(self):
        if not hasattr(self, 'choices'):
            raise MissingArgumentException('Missing argument: choices')
86
        self._validate_selections(self.value)
Noemi Vanyi's avatar
Noemi Vanyi committed
87 88 89 90 91 92 93

    def parse(self, data):
        if data == '':
            self.value = []
            return

        elements = data.split(',')
94
        self._validate_selections(elements)
Noemi Vanyi's avatar
Noemi Vanyi committed
95 96 97 98 99 100 101 102 103 104 105 106
        self.value = elements

    def parse_form(self, data):
        self.value = []
        for choice in data:
            if choice in self.choices and choice not in self.value:
                self.value.append(choice)

    def save(self, name, resp):
        resp.set_cookie(name, ','.join(self.value), max_age=COOKIE_MAX_AGE)


107 108 109 110 111 112 113 114 115 116 117 118 119
class SearchLanguageSetting(EnumStringSetting):
    """Available choices may change, so user's value may not be in choices anymore"""

    def parse(self, data):
        if data not in self.choices and data != self.value:
            # hack to give some backwards compatibility with old language cookies
            data = str(data).replace('_', '-')
            lang = data.split('-')[0]
            if data in self.choices:
                pass
            elif lang in self.choices:
                data = lang
            else:
120
                data = self.value
121 122 123
        self.value = data


Noemi Vanyi's avatar
Noemi Vanyi committed
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
class MapSetting(Setting):
    """Setting of a value that has to be translated in order to be storable"""

    def _post_init(self):
        if not hasattr(self, 'map'):
            raise MissingArgumentException('missing argument: map')
        if self.value not in self.map.values():
            raise ValidationException('Invalid default value')

    def parse(self, data):
        if data not in self.map:
            raise ValidationException('Invalid choice: {0}'.format(data))
        self.value = self.map[data]
        self.key = data

    def save(self, name, resp):
140
        if hasattr(self, 'key'):
Adam Tauber's avatar
Adam Tauber committed
141
            resp.set_cookie(name, self.key, max_age=COOKIE_MAX_AGE)
Noemi Vanyi's avatar
Noemi Vanyi committed
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197


class SwitchableSetting(Setting):
    """ Base class for settings that can be turned on && off"""

    def _post_init(self):
        self.disabled = set()
        self.enabled = set()
        if not hasattr(self, 'choices'):
            raise MissingArgumentException('missing argument: choices')

    def transform_form_items(self, items):
        return items

    def transform_values(self, values):
        return values

    def parse_cookie(self, data):
        if data[DISABLED] != '':
            self.disabled = set(data[DISABLED].split(','))
        if data[ENABLED] != '':
            self.enabled = set(data[ENABLED].split(','))

    def parse_form(self, items):
        items = self.transform_form_items(items)

        self.disabled = set()
        self.enabled = set()
        for choice in self.choices:
            if choice['default_on']:
                if choice['id'] in items:
                    self.disabled.add(choice['id'])
            else:
                if choice['id'] not in items:
                    self.enabled.add(choice['id'])

    def save(self, resp):
        resp.set_cookie('disabled_{0}'.format(self.value), ','.join(self.disabled), max_age=COOKIE_MAX_AGE)
        resp.set_cookie('enabled_{0}'.format(self.value), ','.join(self.enabled), max_age=COOKIE_MAX_AGE)

    def get_disabled(self):
        disabled = self.disabled
        for choice in self.choices:
            if not choice['default_on'] and choice['id'] not in self.enabled:
                disabled.add(choice['id'])
        return self.transform_values(disabled)

    def get_enabled(self):
        enabled = self.enabled
        for choice in self.choices:
            if choice['default_on'] and choice['id'] not in self.disabled:
                enabled.add(choice['id'])
        return self.transform_values(enabled)


class EnginesSetting(SwitchableSetting):
198

Noemi Vanyi's avatar
Noemi Vanyi committed
199 200 201
    def _post_init(self):
        super(EnginesSetting, self)._post_init()
        transformed_choices = []
Adam Tauber's avatar
Adam Tauber committed
202
        for engine_name, engine in self.choices.items():
Noemi Vanyi's avatar
Noemi Vanyi committed
203 204 205 206 207 208 209 210 211 212 213
            for category in engine.categories:
                transformed_choice = dict()
                transformed_choice['default_on'] = not engine.disabled
                transformed_choice['id'] = '{}__{}'.format(engine_name, category)
                transformed_choices.append(transformed_choice)
        self.choices = transformed_choices

    def transform_form_items(self, items):
        return [item[len('engine_'):].replace('_', ' ').replace('  ', '__') for item in items]

    def transform_values(self, values):
214
        if len(values) == 1 and next(iter(values)) == '':
Noemi Vanyi's avatar
Noemi Vanyi committed
215 216 217 218 219 220 221 222 223
            return list()
        transformed_values = []
        for value in values:
            engine, category = value.split('__')
            transformed_values.append((engine, category))
        return transformed_values


class PluginsSetting(SwitchableSetting):
224

Noemi Vanyi's avatar
Noemi Vanyi committed
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
    def _post_init(self):
        super(PluginsSetting, self)._post_init()
        transformed_choices = []
        for plugin in self.choices:
            transformed_choice = dict()
            transformed_choice['default_on'] = plugin.default_on
            transformed_choice['id'] = plugin.id
            transformed_choices.append(transformed_choice)
        self.choices = transformed_choices

    def transform_form_items(self, items):
        return [item[len('plugin_'):] for item in items]


class Preferences(object):
240
    """Validates and saves preferences to cookies"""
Noemi Vanyi's avatar
Noemi Vanyi committed
241 242 243 244

    def __init__(self, themes, categories, engines, plugins):
        super(Preferences, self).__init__()

245
        self.key_value_settings = {'categories': MultipleChoiceSetting(['general'], choices=categories + ['none']),
246 247
                                   'language': SearchLanguageSetting(settings['search']['language'],
                                                                     choices=LANGUAGE_CODES),
Noemi Vanyi's avatar
Noemi Vanyi committed
248
                                   'locale': EnumStringSetting(settings['ui']['default_locale'],
Adam Tauber's avatar
Adam Tauber committed
249
                                                               choices=list(settings['locales'].keys()) + ['']),
Noemi Vanyi's avatar
Noemi Vanyi committed
250
                                   'autocomplete': EnumStringSetting(settings['search']['autocomplete'],
Adam Tauber's avatar
Adam Tauber committed
251
                                                                     choices=list(autocomplete.backends.keys()) + ['']),
Noemi Vanyi's avatar
Noemi Vanyi committed
252 253 254
                                   'image_proxy': MapSetting(settings['server']['image_proxy'],
                                                             map={'': settings['server']['image_proxy'],
                                                                  '0': False,
255 256 257
                                                                  '1': True,
                                                                  'True': True,
                                                                  'False': False}),
Noemi Vanyi's avatar
Noemi Vanyi committed
258 259 260 261
                                   'method': EnumStringSetting('POST', choices=('GET', 'POST')),
                                   'safesearch': MapSetting(settings['search']['safe_search'], map={'0': 0,
                                                                                                    '1': 1,
                                                                                                    '2': 2}),
262
                                   'theme': EnumStringSetting(settings['ui']['default_theme'], choices=themes),
263 264 265
                                   'results_on_new_tab': MapSetting(False, map={'0': False,
                                                                                '1': True,
                                                                                'False': False,
266 267
                                                                                'True': True}),
                                   'doi_resolver': MultipleChoiceSetting(['oadoi.org'], choices=DOI_RESOLVERS),
268 269 270
                                   'oscar-style': EnumStringSetting(
                                       settings['ui'].get('theme_args', {}).get('oscar_style', 'logicodev'),
                                       choices=['', 'logicodev', 'logicodev-dark', 'pointhi']),
271
                                   }
Noemi Vanyi's avatar
Noemi Vanyi committed
272 273 274

        self.engines = EnginesSetting('engines', choices=engines)
        self.plugins = PluginsSetting('plugins', choices=plugins)
275
        self.unknown_params = {}
Noemi Vanyi's avatar
Noemi Vanyi committed
276

277 278 279 280 281 282 283 284 285 286 287 288 289 290
    def get_as_url_params(self):
        settings_kv = {}
        for k, v in self.key_value_settings.items():
            if isinstance(v, MultipleChoiceSetting):
                settings_kv[k] = ','.join(v.get_value())
            else:
                settings_kv[k] = v.get_value()

        settings_kv['disabled_engines'] = ','.join(self.engines.disabled)
        settings_kv['enabled_engines'] = ','.join(self.engines.enabled)

        settings_kv['disabled_plugins'] = ','.join(self.plugins.disabled)
        settings_kv['enabled_plugins'] = ','.join(self.plugins.enabled)

291 292 293 294
        return urlsafe_b64encode(compress(urlencode(settings_kv).encode('utf-8'))).decode('utf-8')

    def parse_encoded_data(self, input_data):
        decoded_data = decompress(urlsafe_b64decode(input_data.encode('utf-8')))
Adam Tauber's avatar
Adam Tauber committed
295
        self.parse_dict({x: y[0] for x, y in parse_qs(unicode(decoded_data)).items()})
296 297

    def parse_dict(self, input_data):
Adam Tauber's avatar
Adam Tauber committed
298
        for user_setting_name, user_setting in input_data.items():
Noemi Vanyi's avatar
Noemi Vanyi committed
299 300 301
            if user_setting_name in self.key_value_settings:
                self.key_value_settings[user_setting_name].parse(user_setting)
            elif user_setting_name == 'disabled_engines':
Adam Tauber's avatar
Adam Tauber committed
302 303
                self.engines.parse_cookie((input_data.get('disabled_engines', ''),
                                           input_data.get('enabled_engines', '')))
Noemi Vanyi's avatar
Noemi Vanyi committed
304
            elif user_setting_name == 'disabled_plugins':
Adam Tauber's avatar
Adam Tauber committed
305 306
                self.plugins.parse_cookie((input_data.get('disabled_plugins', ''),
                                           input_data.get('enabled_plugins', '')))
307 308 309 310 311 312
            elif not any(user_setting_name.startswith(x) for x in [
                    'enabled_',
                    'disabled_',
                    'engine_',
                    'category_',
                    'plugin_']):
313
                self.unknown_params[user_setting_name] = user_setting
Noemi Vanyi's avatar
Noemi Vanyi committed
314 315 316 317 318

    def parse_form(self, input_data):
        disabled_engines = []
        enabled_categories = []
        disabled_plugins = []
Adam Tauber's avatar
Adam Tauber committed
319
        for user_setting_name, user_setting in input_data.items():
Noemi Vanyi's avatar
Noemi Vanyi committed
320 321 322 323 324 325 326 327
            if user_setting_name in self.key_value_settings:
                self.key_value_settings[user_setting_name].parse(user_setting)
            elif user_setting_name.startswith('engine_'):
                disabled_engines.append(user_setting_name)
            elif user_setting_name.startswith('category_'):
                enabled_categories.append(user_setting_name[len('category_'):])
            elif user_setting_name.startswith('plugin_'):
                disabled_plugins.append(user_setting_name)
328 329
            else:
                self.unknown_params[user_setting_name] = user_setting
Noemi Vanyi's avatar
Noemi Vanyi committed
330 331 332 333 334 335 336 337
        self.key_value_settings['categories'].parse_form(enabled_categories)
        self.engines.parse_form(disabled_engines)
        self.plugins.parse_form(disabled_plugins)

    # cannot be used in case of engines or plugins
    def get_value(self, user_setting_name):
        if user_setting_name in self.key_value_settings:
            return self.key_value_settings[user_setting_name].get_value()
338 339
        if user_setting_name in self.unknown_params:
            return self.unknown_params[user_setting_name]
Noemi Vanyi's avatar
Noemi Vanyi committed
340 341

    def save(self, resp):
Adam Tauber's avatar
Adam Tauber committed
342
        for user_setting_name, user_setting in self.key_value_settings.items():
Noemi Vanyi's avatar
Noemi Vanyi committed
343 344 345
            user_setting.save(user_setting_name, resp)
        self.engines.save(resp)
        self.plugins.save(resp)
346 347
        for k, v in self.unknown_params.items():
            resp.set_cookie(k, v, max_age=COOKIE_MAX_AGE)
Noemi Vanyi's avatar
Noemi Vanyi committed
348
        return resp