| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 | # SPDX-License-Identifier: AGPL-3.0-or-later# lint: pylint"""Implementation of the default settings."""import typingimport numbersimport errnoimport osimport loggingfrom os.path import dirname, abspathfrom searx.languages import language_codes as languagessearx_dir = abspath(dirname(__file__))logger = logging.getLogger('searx')OUTPUT_FORMATS = ['html', 'csv', 'json', 'rss']LANGUAGE_CODES = ('', 'all') + tuple(l[0] for l in languages)OSCAR_STYLE = ('logicodev', 'logicodev-dark', 'pointhi')CATEGORY_ORDER = [    'general',    'images',    'videos',    'news',    'map',    'music',    'it',    'science',    'files',    'social media',]STR_TO_BOOL = {    '0': False,    'false': False,    'off': False,    '1': True,    'true': True,    'on': True,}_UNDEFINED = object()# compatibilitySEARX_ENVIRON_VARIABLES = {    'SEARX_DISABLE_ETC_SETTINGS': 'SEARXNG_DISABLE_ETC_SETTINGS',    'SEARX_SETTINGS_PATH': 'SEARXNG_SETTINGS_PATH',    'SEARX_DEBUG': 'SEARXNG_DEBUG',    'SEARX_PORT': 'SEARXNG_PORT',    'SEARX_BIND_ADDRESS': 'SEARXNG_BIND_ADDRESS',    'SEARX_SECRET': 'SEARXNG_SECRET',}class SettingsValue:    """Check and update a setting value    """    def __init__(self,                 type_definition: typing.Union[None, typing.Any, typing.Tuple[typing.Any]]=None,                 default: typing.Any=None,                 environ_name: str=None):        self.type_definition = (            type_definition            if type_definition is None or isinstance(type_definition, tuple)            else (type_definition,)        )        self.default = default        self.environ_name = environ_name    @property    def type_definition_repr(self):        types_str = [            t.__name__ if isinstance(t, type) else repr(t)            for t in self.type_definition        ]        return ', '.join(types_str)    def check_type_definition(self, value: typing.Any) -> None:        if value in self.type_definition:            return        type_list = tuple(t for t in self.type_definition if isinstance(t, type))        if not isinstance(value, type_list):            raise ValueError(                'The value has to be one of these types/values: {}'.format(                    self.type_definition_repr))    def __call__(self, value: typing.Any) -> typing.Any:        if value == _UNDEFINED:            value = self.default        # override existing value with environ        if self.environ_name and self.environ_name in os.environ:            value = os.environ[self.environ_name]            if self.type_definition == (bool,):                value = STR_TO_BOOL[value.lower()]        self.check_type_definition(value)        return valueclass SettingsDirectoryValue(SettingsValue):    """Check and update a setting value that is a directory path    """    def check_type_definition(self, value: typing.Any) -> typing.Any:        super().check_type_definition(value)        if not os.path.isdir(value):            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), value)    def __call__(self, value: typing.Any) -> typing.Any:        if value == '':            value = self.default        return super().__call__(value)def apply_schema(settings, schema, path_list):    error = False    for key, value in schema.items():        if isinstance(value, SettingsValue):            try:                settings[key] = value(settings.get(key, _UNDEFINED))            except Exception as e:  # pylint: disable=broad-except                # don't stop now: check other values                logger.error('%s: %s', '.'.join([*path_list, key]), e)                error = True        elif isinstance(value, dict):            error = error or apply_schema(settings.setdefault(key, {}), schema[key], [*path_list, key])        else:            settings.setdefault(key, value)    if len(path_list) == 0 and error:        raise ValueError('Invalid settings.yml')    return errorSCHEMA = {    'general': {        'debug': SettingsValue(bool, False, 'SEARXNG_DEBUG'),        'instance_name': SettingsValue(str, 'SearXNG'),        'contact_url': SettingsValue((None, False, str), None),    },    'brand': {        'issue_url': SettingsValue(str, None),        'new_issue_url': SettingsValue(str, None),        'docs_url': SettingsValue(str, None),        'public_instances': SettingsValue(str, None),        'wiki_url': SettingsValue(str, None),    },    'search': {        'safe_search': SettingsValue((0,1,2), 0),        'autocomplete': SettingsValue(str, ''),        'default_lang': SettingsValue(LANGUAGE_CODES, ''),        'ban_time_on_fail': SettingsValue(numbers.Real, 5),        'max_ban_time_on_fail': SettingsValue(numbers.Real, 120),        'formats': SettingsValue(list, OUTPUT_FORMATS),    },    'server': {        'port': SettingsValue((int,str), 8888, 'SEARXNG_PORT'),        'bind_address': SettingsValue(str, '127.0.0.1', 'SEARXNG_BIND_ADDRESS'),        'secret_key': SettingsValue(str, environ_name='SEARXNG_SECRET'),        'base_url': SettingsValue((False, str), False),        'image_proxy': SettingsValue(bool, False),        'http_protocol_version': SettingsValue(('1.0', '1.1'), '1.0'),        'method': SettingsValue(('POST', 'GET'), 'POST'),        'default_http_headers': SettingsValue(dict, {}),    },    'ui': {        'static_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'static')),        'templates_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'templates')),        'default_theme': SettingsValue(str, 'oscar'),        'default_locale': SettingsValue(str, ''),        'theme_args': {            'oscar_style': SettingsValue(OSCAR_STYLE, 'logicodev'),        },        'results_on_new_tab': SettingsValue(bool, False),        'advanced_search': SettingsValue(bool, False),        'categories_order': SettingsValue(list, CATEGORY_ORDER),    },    'preferences': {        'lock': SettingsValue(list, []),    },    'outgoing': {        'useragent_suffix': SettingsValue(str, ''),        'request_timeout': SettingsValue(numbers.Real, 3.0),        'enable_http2': SettingsValue(bool, True),        'max_request_timeout': SettingsValue((None, numbers.Real), None),        # Magic number kept from previous code        'pool_connections': SettingsValue(int, 100),        # Picked from constructor        'pool_maxsize': SettingsValue(int, 10),        'keepalive_expiry': SettingsValue(numbers.Real, 5.0),        # default maximum redirect        # from https://github.com/psf/requests/blob/8c211a96cdbe9fe320d63d9e1ae15c5c07e179f8/requests/models.py#L55        'max_redirects': SettingsValue(int, 30),        'retries': SettingsValue(int, 0),        'proxies': SettingsValue((None, str, dict), None),        'source_ips': SettingsValue((None, str, list), None),        # Tor configuration        'using_tor_proxy': SettingsValue(bool, False),        'extra_proxy_timeout': SettingsValue(int, 0),        'networks': {        },    },    'plugins': SettingsValue((None, list), None),    'enabled_plugins': SettingsValue(list, []),    'checker': {        'off_when_debug': SettingsValue(bool, True),    },    'engines': SettingsValue(list, []),    'doi_resolvers': {    },}def settings_set_defaults(settings):    # compatibility with searx variables    for searx, searxng in SEARX_ENVIRON_VARIABLES.items():        if searx in os.environ and searxng not in os.environ:            os.environ[searxng] = os.environ[searx]            logger.warning('%s uses value from %s', searxng, searx)    apply_schema(settings, SCHEMA, [])    return settings
 |