forked from zaclys/searxng
		
	Merge pull request #110 from searxng/mod-default-settings
[mod] move all default settings into searx.settings_defaults
This commit is contained in:
		
						commit
						e3f4a77311
					
				
					 11 changed files with 269 additions and 190 deletions
				
			
		| 
						 | 
				
			
			@ -1,53 +1,21 @@
 | 
			
		|||
'''
 | 
			
		||||
searx is free software: you can redistribute it and/or modify
 | 
			
		||||
it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
(at your option) any later version.
 | 
			
		||||
 | 
			
		||||
searx is distributed in the hope that it will be useful,
 | 
			
		||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
GNU Affero General Public License for more details.
 | 
			
		||||
 | 
			
		||||
You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
along with searx. If not, see < http://www.gnu.org/licenses/ >.
 | 
			
		||||
 | 
			
		||||
(C) 2013- by Adam Tauber, <asciimoo@gmail.com>
 | 
			
		||||
'''
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-or-later
 | 
			
		||||
# lint: pylint
 | 
			
		||||
# pylint: disable=missing-function-docstring, missing-module-docstring
 | 
			
		||||
 | 
			
		||||
from os.path import dirname, abspath
 | 
			
		||||
import logging
 | 
			
		||||
import searx.settings_loader
 | 
			
		||||
from os import environ
 | 
			
		||||
from os.path import realpath, dirname, join, abspath, isfile
 | 
			
		||||
 | 
			
		||||
import searx.settings_loader
 | 
			
		||||
from searx.settings_defaults import settings_set_defaults
 | 
			
		||||
 | 
			
		||||
searx_dir = abspath(dirname(__file__))
 | 
			
		||||
searx_parent_dir = abspath(dirname(dirname(__file__)))
 | 
			
		||||
engine_dir = dirname(realpath(__file__))
 | 
			
		||||
static_path = abspath(join(dirname(__file__), 'static'))
 | 
			
		||||
settings, settings_load_message = searx.settings_loader.load_settings()
 | 
			
		||||
 | 
			
		||||
if settings['ui']['static_path']:
 | 
			
		||||
    static_path = settings['ui']['static_path']
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
enable debug if
 | 
			
		||||
the environnement variable SEARX_DEBUG is 1 or true
 | 
			
		||||
(whatever the value in settings.yml)
 | 
			
		||||
or general.debug=True in settings.yml
 | 
			
		||||
disable debug if
 | 
			
		||||
the environnement variable SEARX_DEBUG is 0 or false
 | 
			
		||||
(whatever the value in settings.yml)
 | 
			
		||||
or general.debug=False in settings.yml
 | 
			
		||||
'''
 | 
			
		||||
searx_debug_env = environ.get('SEARX_DEBUG', '').lower()
 | 
			
		||||
if searx_debug_env == 'true' or searx_debug_env == '1':
 | 
			
		||||
    searx_debug = True
 | 
			
		||||
elif searx_debug_env == 'false' or searx_debug_env == '0':
 | 
			
		||||
    searx_debug = False
 | 
			
		||||
else:
 | 
			
		||||
    searx_debug = settings.get('general', {}).get('debug')
 | 
			
		||||
if settings is not None:
 | 
			
		||||
    settings = settings_set_defaults(settings)
 | 
			
		||||
 | 
			
		||||
searx_debug = settings['general']['debug']
 | 
			
		||||
if searx_debug:
 | 
			
		||||
    logging.basicConfig(level=logging.DEBUG)
 | 
			
		||||
else:
 | 
			
		||||
| 
						 | 
				
			
			@ -55,15 +23,16 @@ else:
 | 
			
		|||
 | 
			
		||||
logger = logging.getLogger('searx')
 | 
			
		||||
logger.info(settings_load_message)
 | 
			
		||||
logger.info('Initialisation done')
 | 
			
		||||
 | 
			
		||||
if 'SEARX_SECRET' in environ:
 | 
			
		||||
    settings['server']['secret_key'] = environ['SEARX_SECRET']
 | 
			
		||||
if 'SEARX_BIND_ADDRESS' in environ:
 | 
			
		||||
    settings['server']['bind_address'] = environ['SEARX_BIND_ADDRESS']
 | 
			
		||||
# log max_request_timeout
 | 
			
		||||
max_request_timeout = settings['outgoing']['max_request_timeout']
 | 
			
		||||
if max_request_timeout is None:
 | 
			
		||||
    logger.info('max_request_timeout=%s', repr(max_request_timeout))
 | 
			
		||||
else:
 | 
			
		||||
    logger.info('max_request_timeout=%i second(s)', max_request_timeout)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class _brand_namespace:
 | 
			
		||||
class _brand_namespace:  # pylint: disable=invalid-name
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_val(cls, group, name, default=''):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -144,7 +144,7 @@ def load_engine(engine_data):
 | 
			
		|||
        # exclude onion engines if not using tor.
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    engine.timeout += settings['outgoing'].get('extra_proxy_timeout', 0)
 | 
			
		||||
    engine.timeout += settings['outgoing']['extra_proxy_timeout']
 | 
			
		||||
 | 
			
		||||
    for category_name in engine.categories:
 | 
			
		||||
        categories.setdefault(category_name, []).append(engine)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -224,28 +224,22 @@ def initialize(settings_engines=None, settings_outgoing=None):
 | 
			
		|||
 | 
			
		||||
    global NETWORKS
 | 
			
		||||
 | 
			
		||||
    settings_engines = settings_engines or settings.get('engines')
 | 
			
		||||
    settings_outgoing = settings_outgoing or settings.get('outgoing')
 | 
			
		||||
    settings_engines = settings_engines or settings['engines']
 | 
			
		||||
    settings_outgoing = settings_outgoing or settings['outgoing']
 | 
			
		||||
 | 
			
		||||
    # default parameters for AsyncHTTPTransport
 | 
			
		||||
    # see https://github.com/encode/httpx/blob/e05a5372eb6172287458b37447c30f650047e1b8/httpx/_transports/default.py#L108-L121  # pylint: disable=line-too-long
 | 
			
		||||
    default_params = {
 | 
			
		||||
        'enable_http': False,
 | 
			
		||||
        'verify': True,
 | 
			
		||||
        'enable_http2': settings_outgoing.get('enable_http2', True),
 | 
			
		||||
        # Magic number kept from previous code
 | 
			
		||||
        'max_connections': settings_outgoing.get('pool_connections', 100),
 | 
			
		||||
        # Picked from constructor
 | 
			
		||||
        'max_keepalive_connections': settings_outgoing.get('pool_maxsize', 10),
 | 
			
		||||
        #
 | 
			
		||||
        'keepalive_expiry': settings_outgoing.get('keepalive_expiry', 5.0),
 | 
			
		||||
        'local_addresses': settings_outgoing.get('source_ips'),
 | 
			
		||||
        'proxies': settings_outgoing.get('proxies'),
 | 
			
		||||
        # default maximum redirect
 | 
			
		||||
        # from https://github.com/psf/requests/blob/8c211a96cdbe9fe320d63d9e1ae15c5c07e179f8/requests/models.py#L55
 | 
			
		||||
        'max_redirects': settings_outgoing.get('max_redirects', 30),
 | 
			
		||||
        #
 | 
			
		||||
        'retries': settings_outgoing.get('retries', 0),
 | 
			
		||||
        'enable_http2': settings_outgoing['enable_http2'],
 | 
			
		||||
        'max_connections': settings_outgoing['pool_connections'],
 | 
			
		||||
        'max_keepalive_connections': settings_outgoing['pool_maxsize'],
 | 
			
		||||
        'keepalive_expiry': settings_outgoing['keepalive_expiry'],
 | 
			
		||||
        'local_addresses': settings_outgoing['source_ips'],
 | 
			
		||||
        'proxies': settings_outgoing['proxies'],
 | 
			
		||||
        'max_redirects': settings_outgoing['max_redirects'],
 | 
			
		||||
        'retries': settings_outgoing['retries'],
 | 
			
		||||
        'retry_on_http_error': None,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -274,7 +268,7 @@ def initialize(settings_engines=None, settings_outgoing=None):
 | 
			
		|||
    NETWORKS['ipv6'] = new_network({'local_addresses': '::'})
 | 
			
		||||
 | 
			
		||||
    # define networks from outgoing.networks
 | 
			
		||||
    for network_name, network in settings_outgoing.get('networks', {}).items():
 | 
			
		||||
    for network_name, network in settings_outgoing['networks'].items():
 | 
			
		||||
        NETWORKS[network_name] = new_network(network)
 | 
			
		||||
 | 
			
		||||
    # define networks from engines.[i].network (except references)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ from os import listdir, makedirs, remove, stat, utime
 | 
			
		|||
from os.path import abspath, basename, dirname, exists, join
 | 
			
		||||
from shutil import copyfile
 | 
			
		||||
 | 
			
		||||
from searx import logger, settings, static_path
 | 
			
		||||
from searx import logger, settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logger = logger.getChild('plugins')
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +123,7 @@ def sync_resource(base_path, resource_path, name, target_dir, plugin_dir):
 | 
			
		|||
 | 
			
		||||
def prepare_package_resources(pkg, name):
 | 
			
		||||
    plugin_dir = 'plugin_' + name
 | 
			
		||||
    target_dir = join(static_path, 'plugins/external_plugins', plugin_dir)
 | 
			
		||||
    target_dir = join(settings['ui']['static_path'], 'plugins/external_plugins', plugin_dir)
 | 
			
		||||
    try:
 | 
			
		||||
        makedirs(target_dir, exist_ok=True)
 | 
			
		||||
    except:
 | 
			
		||||
| 
						 | 
				
			
			@ -170,10 +170,10 @@ plugins.register(search_on_category_select)
 | 
			
		|||
plugins.register(tracker_url_remover)
 | 
			
		||||
plugins.register(vim_hotkeys)
 | 
			
		||||
# load external plugins
 | 
			
		||||
if 'plugins' in settings:
 | 
			
		||||
if settings['plugins']:
 | 
			
		||||
    plugins.register(*settings['plugins'], external=True)
 | 
			
		||||
 | 
			
		||||
if 'enabled_plugins' in settings:
 | 
			
		||||
if settings['enabled_plugins']:
 | 
			
		||||
    for plugin in plugins:
 | 
			
		||||
        if plugin.name in settings['enabled_plugins']:
 | 
			
		||||
            plugin.default_on = True
 | 
			
		||||
| 
						 | 
				
			
			@ -181,5 +181,5 @@ if 'enabled_plugins' in settings:
 | 
			
		|||
            plugin.default_on = False
 | 
			
		||||
 | 
			
		||||
# load tor specific plugins
 | 
			
		||||
if settings['outgoing'].get('using_tor_proxy'):
 | 
			
		||||
if settings['outgoing']['using_tor_proxy']:
 | 
			
		||||
    plugins.register(ahmia_filter)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -333,25 +333,25 @@ class Preferences:
 | 
			
		|||
                choices=categories + ['none']
 | 
			
		||||
            ),
 | 
			
		||||
            'language': SearchLanguageSetting(
 | 
			
		||||
                settings['search'].get('default_lang', ''),
 | 
			
		||||
                settings['search']['default_lang'],
 | 
			
		||||
                is_locked('language'),
 | 
			
		||||
                choices=list(LANGUAGE_CODES) + ['']
 | 
			
		||||
            ),
 | 
			
		||||
            'locale': EnumStringSetting(
 | 
			
		||||
                settings['ui'].get('default_locale', ''),
 | 
			
		||||
                settings['ui']['default_locale'],
 | 
			
		||||
                is_locked('locale'),
 | 
			
		||||
                choices=list(settings['locales'].keys()) + ['']
 | 
			
		||||
            ),
 | 
			
		||||
            'autocomplete': EnumStringSetting(
 | 
			
		||||
                settings['search'].get('autocomplete', ''),
 | 
			
		||||
                settings['search']['autocomplete'],
 | 
			
		||||
                is_locked('autocomplete'),
 | 
			
		||||
                choices=list(autocomplete.backends.keys()) + ['']
 | 
			
		||||
            ),
 | 
			
		||||
            'image_proxy': MapSetting(
 | 
			
		||||
                settings['server'].get('image_proxy', False),
 | 
			
		||||
                settings['server']['image_proxy'],
 | 
			
		||||
                is_locked('image_proxy'),
 | 
			
		||||
                map={
 | 
			
		||||
                    '': settings['server'].get('image_proxy', 0),
 | 
			
		||||
                    '': settings['server']['image_proxy'],
 | 
			
		||||
                    '0': False,
 | 
			
		||||
                    '1': True,
 | 
			
		||||
                    'True': True,
 | 
			
		||||
| 
						 | 
				
			
			@ -359,12 +359,12 @@ class Preferences:
 | 
			
		|||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            'method': EnumStringSetting(
 | 
			
		||||
                settings['server'].get('method', 'POST'),
 | 
			
		||||
                settings['server']['method'],
 | 
			
		||||
                is_locked('method'),
 | 
			
		||||
                choices=('GET', 'POST')
 | 
			
		||||
            ),
 | 
			
		||||
            'safesearch': MapSetting(
 | 
			
		||||
                settings['search'].get('safe_search', 0),
 | 
			
		||||
                settings['search']['safe_search'],
 | 
			
		||||
                is_locked('safesearch'),
 | 
			
		||||
                map={
 | 
			
		||||
                    '0': 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -373,12 +373,12 @@ class Preferences:
 | 
			
		|||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            'theme': EnumStringSetting(
 | 
			
		||||
                settings['ui'].get('default_theme', 'oscar'),
 | 
			
		||||
                settings['ui']['default_theme'],
 | 
			
		||||
                is_locked('theme'),
 | 
			
		||||
                choices=themes
 | 
			
		||||
            ),
 | 
			
		||||
            'results_on_new_tab': MapSetting(
 | 
			
		||||
                settings['ui'].get('results_on_new_tab', False),
 | 
			
		||||
                settings['ui']['results_on_new_tab'],
 | 
			
		||||
                is_locked('results_on_new_tab'),
 | 
			
		||||
                map={
 | 
			
		||||
                    '0': False,
 | 
			
		||||
| 
						 | 
				
			
			@ -393,11 +393,11 @@ class Preferences:
 | 
			
		|||
                choices=DOI_RESOLVERS
 | 
			
		||||
            ),
 | 
			
		||||
            'oscar-style': EnumStringSetting(
 | 
			
		||||
                settings['ui'].get('theme_args', {}).get('oscar_style', 'logicodev'),
 | 
			
		||||
                settings['ui']['theme_args']['oscar_style'],
 | 
			
		||||
                is_locked('oscar-style'),
 | 
			
		||||
                choices=['', 'logicodev', 'logicodev-dark', 'pointhi']),
 | 
			
		||||
            'advanced_search': MapSetting(
 | 
			
		||||
                settings['ui'].get('advanced_search', False),
 | 
			
		||||
                settings['ui']['advanced_search'],
 | 
			
		||||
                is_locked('advanced_search'),
 | 
			
		||||
                map={
 | 
			
		||||
                    '0': False,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,17 +23,6 @@ from searx.search.checker import initialize as initialize_checker
 | 
			
		|||
 | 
			
		||||
logger = logger.getChild('search')
 | 
			
		||||
 | 
			
		||||
max_request_timeout = settings.get('outgoing', {}).get('max_request_timeout' or None)
 | 
			
		||||
if max_request_timeout is None:
 | 
			
		||||
    logger.info('max_request_timeout={0}'.format(max_request_timeout))
 | 
			
		||||
else:
 | 
			
		||||
    if isinstance(max_request_timeout, float):
 | 
			
		||||
        logger.info('max_request_timeout={0} second(s)'.format(max_request_timeout))
 | 
			
		||||
    else:
 | 
			
		||||
        logger.critical('outgoing.max_request_timeout if defined has to be float')
 | 
			
		||||
        import sys
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def initialize(settings_engines=None, enable_checker=False):
 | 
			
		||||
    settings_engines = settings_engines or settings['engines']
 | 
			
		||||
| 
						 | 
				
			
			@ -115,6 +104,7 @@ class Search:
 | 
			
		|||
            default_timeout = max(default_timeout, processor.engine.timeout)
 | 
			
		||||
 | 
			
		||||
        # adjust timeout
 | 
			
		||||
        max_request_timeout = settings['outgoing']['max_request_timeout']
 | 
			
		||||
        actual_timeout = default_timeout
 | 
			
		||||
        query_timeout = self.search_query.timeout_limit
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										202
									
								
								searx/settings_defaults.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								searx/settings_defaults.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,202 @@
 | 
			
		|||
# SPDX-License-Identifier: AGPL-3.0-or-later
 | 
			
		||||
# lint: pylint
 | 
			
		||||
# pylint: disable=missing-function-docstring
 | 
			
		||||
"""Implementation of the default settings.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import typing
 | 
			
		||||
import numbers
 | 
			
		||||
import errno
 | 
			
		||||
import os
 | 
			
		||||
import logging
 | 
			
		||||
from os.path import dirname, abspath
 | 
			
		||||
 | 
			
		||||
from searx.languages import language_codes as languages
 | 
			
		||||
 | 
			
		||||
searx_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 medias',
 | 
			
		||||
]
 | 
			
		||||
STR_TO_BOOL = {
 | 
			
		||||
    '0': False,
 | 
			
		||||
    'false': False,
 | 
			
		||||
    'off': False,
 | 
			
		||||
    '1': True,
 | 
			
		||||
    'true': True,
 | 
			
		||||
    'on': True,
 | 
			
		||||
}
 | 
			
		||||
_UNDEFINED = object()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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 value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class 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 error
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SCHEMA = {
 | 
			
		||||
    'general': {
 | 
			
		||||
        'debug': SettingsValue(bool, False, 'SEARX_DEBUG'),
 | 
			
		||||
        'instance_name': SettingsValue(str, 'searxng'),
 | 
			
		||||
        'contact_url': SettingsValue((None, False, str), None),
 | 
			
		||||
    },
 | 
			
		||||
    'brand': {
 | 
			
		||||
    },
 | 
			
		||||
    '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, 8888),
 | 
			
		||||
        'bind_address': SettingsValue(str, '127.0.0.1', 'SEARX_BIND_ADDRESS'),
 | 
			
		||||
        'secret_key': SettingsValue(str, environ_name='SEARX_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, []),
 | 
			
		||||
    'locales': SettingsValue(dict, {'en': 'English'}),
 | 
			
		||||
    'doi_resolvers': {
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def settings_set_defaults(settings):
 | 
			
		||||
    apply_schema(settings, SCHEMA, [])
 | 
			
		||||
    return settings
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,6 @@ from os.path import splitext, join
 | 
			
		|||
from random import choice
 | 
			
		||||
from html.parser import HTMLParser
 | 
			
		||||
from urllib.parse import urljoin, urlparse
 | 
			
		||||
from collections.abc import Mapping
 | 
			
		||||
 | 
			
		||||
from lxml import html
 | 
			
		||||
from lxml.etree import ElementBase, XPath, XPathError, XPathSyntaxError, _ElementStringResult, _ElementUnicodeResult
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +45,7 @@ def searx_useragent():
 | 
			
		|||
    """Return the searx User Agent"""
 | 
			
		||||
    return 'searx/{searx_version} {suffix}'.format(
 | 
			
		||||
           searx_version=VERSION_STRING,
 | 
			
		||||
           suffix=settings['outgoing'].get('useragent_suffix', '')).strip()
 | 
			
		||||
           suffix=settings['outgoing']['useragent_suffix'].strip())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def gen_useragent(os=None):
 | 
			
		||||
| 
						 | 
				
			
			@ -501,58 +500,6 @@ def get_engine_from_settings(name):
 | 
			
		|||
    return {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
NOT_EXISTS = object()
 | 
			
		||||
"""Singleton used by :py:obj:`get_value` if a key does not exists."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_value(dictionary, *keys, default=NOT_EXISTS):
 | 
			
		||||
    """Return the value from a *deep* mapping type (e.g. the ``settings`` object
 | 
			
		||||
    from yaml).  If the path to the *key* does not exists a :py:obj:`NOT_EXISTS`
 | 
			
		||||
    is returned (non ``KeyError`` exception is raised).
 | 
			
		||||
 | 
			
		||||
    .. code: python
 | 
			
		||||
 | 
			
		||||
       >>> from searx import settings
 | 
			
		||||
       >>> from searx.utils import get_value, NOT_EXISTS
 | 
			
		||||
       >>> get_value(settings, 'checker', 'additional_tests', 'rosebud', 'result_container')
 | 
			
		||||
       ['not_empty', ['one_title_contains', 'citizen kane']]
 | 
			
		||||
 | 
			
		||||
       >>> get_value(settings, 'search', 'xxx') is NOT_EXISTS
 | 
			
		||||
       True
 | 
			
		||||
       >>> get_value(settings, 'search', 'formats')
 | 
			
		||||
       ['html', 'csv', 'json', 'rss']
 | 
			
		||||
 | 
			
		||||
    The list returned from the ``search.format`` key is not a mapping type, you
 | 
			
		||||
    can't traverse along non-mapping types.  If you try it, you will get a
 | 
			
		||||
    :py:ref:`NOT_EXISTS`:
 | 
			
		||||
 | 
			
		||||
    .. code: python
 | 
			
		||||
 | 
			
		||||
       >>> get_value(settings, 'search', 'format', 'csv') is NOT_EXISTS
 | 
			
		||||
       True
 | 
			
		||||
       >>> get_value(settings, 'search', 'formats')[1]
 | 
			
		||||
       'csv'
 | 
			
		||||
 | 
			
		||||
    For convenience you can replace :py:ref:`NOT_EXISTS` by a default value of
 | 
			
		||||
    your choice:
 | 
			
		||||
 | 
			
		||||
    .. code: python
 | 
			
		||||
 | 
			
		||||
       if 'csv' in get_value(settings, 'search', 'formats', default=[]):
 | 
			
		||||
           print("csv format is denied")
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    obj = dictionary
 | 
			
		||||
    for k in keys:
 | 
			
		||||
        if not isinstance(obj, Mapping):
 | 
			
		||||
            raise TypeError("expected mapping type, got %s" % type(obj))
 | 
			
		||||
        obj = obj.get(k, default)
 | 
			
		||||
        if obj is default:
 | 
			
		||||
            return obj
 | 
			
		||||
    return obj
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_xpath(xpath_spec):
 | 
			
		||||
    """Return cached compiled XPath
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,12 +56,12 @@ from flask_babel import (
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
from searx import logger
 | 
			
		||||
from searx import brand, static_path
 | 
			
		||||
from searx import brand
 | 
			
		||||
from searx import (
 | 
			
		||||
    settings,
 | 
			
		||||
    searx_dir,
 | 
			
		||||
    searx_debug,
 | 
			
		||||
)
 | 
			
		||||
from searx.settings_defaults import OUTPUT_FORMATS
 | 
			
		||||
from searx.exceptions import SearxParameterException
 | 
			
		||||
from searx.engines import (
 | 
			
		||||
    categories,
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +71,6 @@ from searx.engines import (
 | 
			
		|||
from searx.webutils import (
 | 
			
		||||
    UnicodeWriter,
 | 
			
		||||
    highlight_content,
 | 
			
		||||
    get_resources_directory,
 | 
			
		||||
    get_static_files,
 | 
			
		||||
    get_result_templates,
 | 
			
		||||
    get_themes,
 | 
			
		||||
| 
						 | 
				
			
			@ -88,7 +87,6 @@ from searx.utils import (
 | 
			
		|||
    gen_useragent,
 | 
			
		||||
    dict_subset,
 | 
			
		||||
    match_language,
 | 
			
		||||
    get_value,
 | 
			
		||||
)
 | 
			
		||||
from searx.version import VERSION_STRING
 | 
			
		||||
from searx.query import RawTextQuery
 | 
			
		||||
| 
						 | 
				
			
			@ -139,7 +137,7 @@ if sys.version_info[0] < 3:
 | 
			
		|||
logger = logger.getChild('webapp')
 | 
			
		||||
 | 
			
		||||
# serve pages with HTTP/1.1
 | 
			
		||||
WSGIRequestHandler.protocol_version = "HTTP/{}".format(settings['server'].get('http_protocol_version', '1.0'))
 | 
			
		||||
WSGIRequestHandler.protocol_version = "HTTP/{}".format(settings['server']['http_protocol_version'])
 | 
			
		||||
 | 
			
		||||
# check secret_key
 | 
			
		||||
if not searx_debug and settings['server']['secret_key'] == 'ultrasecretkey':
 | 
			
		||||
| 
						 | 
				
			
			@ -147,25 +145,22 @@ if not searx_debug and settings['server']['secret_key'] == 'ultrasecretkey':
 | 
			
		|||
    sys.exit(1)
 | 
			
		||||
 | 
			
		||||
# about static
 | 
			
		||||
static_path = get_resources_directory(searx_dir, 'static', settings['ui']['static_path'])
 | 
			
		||||
logger.debug('static directory is %s', static_path)
 | 
			
		||||
static_files = get_static_files(static_path)
 | 
			
		||||
logger.debug('static directory is %s', settings['ui']['static_path'])
 | 
			
		||||
static_files = get_static_files(settings['ui']['static_path'])
 | 
			
		||||
 | 
			
		||||
# about templates
 | 
			
		||||
logger.debug('templates directory is %s', settings['ui']['templates_path'])
 | 
			
		||||
default_theme = settings['ui']['default_theme']
 | 
			
		||||
templates_path = get_resources_directory(searx_dir, 'templates', settings['ui']['templates_path'])
 | 
			
		||||
logger.debug('templates directory is %s', templates_path)
 | 
			
		||||
templates_path = settings['ui']['templates_path']
 | 
			
		||||
themes = get_themes(templates_path)
 | 
			
		||||
result_templates = get_result_templates(templates_path)
 | 
			
		||||
global_favicons = []
 | 
			
		||||
for indice, theme in enumerate(themes):
 | 
			
		||||
    global_favicons.append([])
 | 
			
		||||
    theme_img_path = os.path.join(static_path, 'themes', theme, 'img', 'icons')
 | 
			
		||||
    theme_img_path = os.path.join(settings['ui']['static_path'], 'themes', theme, 'img', 'icons')
 | 
			
		||||
    for (dirpath, dirnames, filenames) in os.walk(theme_img_path):
 | 
			
		||||
        global_favicons[indice].extend(filenames)
 | 
			
		||||
 | 
			
		||||
OUTPUT_FORMATS = ['html', 'csv', 'json', 'rss']
 | 
			
		||||
 | 
			
		||||
STATS_SORT_PARAMETERS = {
 | 
			
		||||
    'name': (False, 'name', ''),
 | 
			
		||||
    'score': (True, 'score', 0),
 | 
			
		||||
| 
						 | 
				
			
			@ -177,7 +172,7 @@ STATS_SORT_PARAMETERS = {
 | 
			
		|||
# Flask app
 | 
			
		||||
app = Flask(
 | 
			
		||||
    __name__,
 | 
			
		||||
    static_folder=static_path,
 | 
			
		||||
    static_folder=settings['ui']['static_path'],
 | 
			
		||||
    template_folder=templates_path
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -517,8 +512,7 @@ def render(template_name, override_theme=None, **kwargs):
 | 
			
		|||
    kwargs['preferences'] = request.preferences
 | 
			
		||||
 | 
			
		||||
    kwargs['search_formats'] = [
 | 
			
		||||
        x for x in get_value(
 | 
			
		||||
            settings, 'search', 'formats', default=OUTPUT_FORMATS)
 | 
			
		||||
        x for x in settings['search']['formats']
 | 
			
		||||
        if x != 'html']
 | 
			
		||||
 | 
			
		||||
    kwargs['brand'] = brand
 | 
			
		||||
| 
						 | 
				
			
			@ -545,12 +539,7 @@ def render(template_name, override_theme=None, **kwargs):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def _get_ordered_categories():
 | 
			
		||||
    ordered_categories = []
 | 
			
		||||
    if 'categories_order' not in settings['ui']:
 | 
			
		||||
        ordered_categories = ['general']
 | 
			
		||||
        ordered_categories.extend(x for x in sorted(categories.keys()) if x != 'general')
 | 
			
		||||
        return ordered_categories
 | 
			
		||||
    ordered_categories = settings['ui']['categories_order']
 | 
			
		||||
    ordered_categories = list(settings['ui']['categories_order'])
 | 
			
		||||
    ordered_categories.extend(x for x in sorted(categories.keys()) if x not in ordered_categories)
 | 
			
		||||
    return ordered_categories
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -610,7 +599,7 @@ def pre_request():
 | 
			
		|||
@app.after_request
 | 
			
		||||
def add_default_headers(response):
 | 
			
		||||
    # set default http headers
 | 
			
		||||
    for header, value in settings['server'].get('default_http_headers', {}).items():
 | 
			
		||||
    for header, value in settings['server']['default_http_headers'].items():
 | 
			
		||||
        if header in response.headers:
 | 
			
		||||
            continue
 | 
			
		||||
        response.headers[header] = value
 | 
			
		||||
| 
						 | 
				
			
			@ -696,7 +685,7 @@ def search():
 | 
			
		|||
    if output_format not in OUTPUT_FORMATS:
 | 
			
		||||
        output_format = 'html'
 | 
			
		||||
 | 
			
		||||
    if output_format not in get_value(settings, 'search', 'formats', default=OUTPUT_FORMATS):
 | 
			
		||||
    if output_format not in settings['search']['formats']:
 | 
			
		||||
        flask.abort(403)
 | 
			
		||||
 | 
			
		||||
    # check if there is query (not None and not an empty string)
 | 
			
		||||
| 
						 | 
				
			
			@ -1069,11 +1058,6 @@ def preferences():
 | 
			
		|||
            'time_range_support': time_range_support,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    locked_preferences = list()
 | 
			
		||||
    if 'preferences' in settings and 'lock' in settings['preferences']:
 | 
			
		||||
        locked_preferences = settings['preferences']['lock']
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    return render('preferences.html',
 | 
			
		||||
                  selected_categories=get_selected_categories(request.preferences, request.form),
 | 
			
		||||
| 
						 | 
				
			
			@ -1098,7 +1082,7 @@ def preferences():
 | 
			
		|||
                  theme=get_current_theme_name(),
 | 
			
		||||
                  preferences_url_params=request.preferences.get_as_url_params(),
 | 
			
		||||
                  base_url=get_base_url(),
 | 
			
		||||
                  locked_preferences=locked_preferences,
 | 
			
		||||
                  locked_preferences=settings['preferences']['lock'],
 | 
			
		||||
                  preferences=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1271,7 +1255,7 @@ def favicon():
 | 
			
		|||
    return send_from_directory(
 | 
			
		||||
        os.path.join(
 | 
			
		||||
            app.root_path,
 | 
			
		||||
            static_path,
 | 
			
		||||
            settings['ui']['static_path'],
 | 
			
		||||
            'themes',
 | 
			
		||||
            get_current_theme_name(),
 | 
			
		||||
            'img'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,14 +47,6 @@ class UnicodeWriter:
 | 
			
		|||
            self.writerow(row)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_resources_directory(searx_directory, subdirectory, resources_directory):
 | 
			
		||||
    if not resources_directory:
 | 
			
		||||
        resources_directory = os.path.join(searx_directory, subdirectory)
 | 
			
		||||
    if not os.path.isdir(resources_directory):
 | 
			
		||||
        raise Exception(resources_directory + " is not a directory")
 | 
			
		||||
    return resources_directory
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_themes(templates_path):
 | 
			
		||||
    """Returns available themes list."""
 | 
			
		||||
    themes = os.listdir(templates_path)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
 | 
			
		||||
from searx.testing import SearxTestCase
 | 
			
		||||
from searx.search import SearchQuery, EngineRef
 | 
			
		||||
from searx import settings
 | 
			
		||||
import searx.search
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +42,7 @@ class SearchTestCase(SearxTestCase):
 | 
			
		|||
        searx.search.initialize(TEST_ENGINES)
 | 
			
		||||
 | 
			
		||||
    def test_timeout_simple(self):
 | 
			
		||||
        searx.search.max_request_timeout = None
 | 
			
		||||
        settings['outgoing']['max_request_timeout'] = None
 | 
			
		||||
        search_query = SearchQuery('test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
 | 
			
		||||
                                   'en-US', SAFESEARCH, PAGENO, None, None)
 | 
			
		||||
        search = searx.search.Search(search_query)
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +50,7 @@ class SearchTestCase(SearxTestCase):
 | 
			
		|||
        self.assertEqual(search.actual_timeout, 3.0)
 | 
			
		||||
 | 
			
		||||
    def test_timeout_query_above_default_nomax(self):
 | 
			
		||||
        searx.search.max_request_timeout = None
 | 
			
		||||
        settings['outgoing']['max_request_timeout'] = None
 | 
			
		||||
        search_query = SearchQuery('test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
 | 
			
		||||
                                   'en-US', SAFESEARCH, PAGENO, None, 5.0)
 | 
			
		||||
        search = searx.search.Search(search_query)
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +58,7 @@ class SearchTestCase(SearxTestCase):
 | 
			
		|||
        self.assertEqual(search.actual_timeout, 3.0)
 | 
			
		||||
 | 
			
		||||
    def test_timeout_query_below_default_nomax(self):
 | 
			
		||||
        searx.search.max_request_timeout = None
 | 
			
		||||
        settings['outgoing']['max_request_timeout'] = None
 | 
			
		||||
        search_query = SearchQuery('test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
 | 
			
		||||
                                   'en-US', SAFESEARCH, PAGENO, None, 1.0)
 | 
			
		||||
        search = searx.search.Search(search_query)
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +66,7 @@ class SearchTestCase(SearxTestCase):
 | 
			
		|||
        self.assertEqual(search.actual_timeout, 1.0)
 | 
			
		||||
 | 
			
		||||
    def test_timeout_query_below_max(self):
 | 
			
		||||
        searx.search.max_request_timeout = 10.0
 | 
			
		||||
        settings['outgoing']['max_request_timeout'] = 10.0
 | 
			
		||||
        search_query = SearchQuery('test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
 | 
			
		||||
                                   'en-US', SAFESEARCH, PAGENO, None, 5.0)
 | 
			
		||||
        search = searx.search.Search(search_query)
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +74,7 @@ class SearchTestCase(SearxTestCase):
 | 
			
		|||
        self.assertEqual(search.actual_timeout, 5.0)
 | 
			
		||||
 | 
			
		||||
    def test_timeout_query_above_max(self):
 | 
			
		||||
        searx.search.max_request_timeout = 10.0
 | 
			
		||||
        settings['outgoing']['max_request_timeout'] = 10.0
 | 
			
		||||
        search_query = SearchQuery('test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
 | 
			
		||||
                                   'en-US', SAFESEARCH, PAGENO, None, 15.0)
 | 
			
		||||
        search = searx.search.Search(search_query)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue