| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 | # SPDX-License-Identifier: AGPL-3.0-or-later"""Implementations for loading configurations from YAML files.  This essentiallyincludes the configuration of the (:ref:`SearXNG appl <searxng settings.yml>`)server. The default configuration for the application server is loaded from the:origin:`DEFAULT_SETTINGS_FILE <searx/settings.yml>`.  This defaultconfiguration can be completely replaced or :ref:`customized individually<use_default_settings.yml>` and the ``SEARXNG_SETTINGS_PATH`` environmentvariable can be used to set the location from which the local customizations areto be loaded. The rules used for this can be found in the:py:obj:`get_user_cfg_folder` function.- By default, local configurations are expected in folder ``/etc/searxng`` from  where applications can load them with the :py:obj:`get_yaml_cfg` function.- By default, customized :ref:`SearXNG appl <searxng settings.yml>` settings are  expected in a file named ``settings.yml``."""from __future__ import annotationsimport os.pathfrom collections.abc import Mappingfrom itertools import filterfalsefrom pathlib import Pathimport yamlfrom searx.exceptions import SearxSettingsExceptionsearx_dir = os.path.abspath(os.path.dirname(__file__))SETTINGS_YAML = Path("settings.yml")DEFAULT_SETTINGS_FILE = Path(searx_dir) / SETTINGS_YAML"""The :origin:`searx/settings.yml` file with all the default settings."""def load_yaml(file_name: str | Path):    """Load YAML config from a file."""    try:        with open(file_name, 'r', encoding='utf-8') as settings_yaml:            return yaml.safe_load(settings_yaml) or {}    except IOError as e:        raise SearxSettingsException(e, str(file_name)) from e    except yaml.YAMLError as e:        raise SearxSettingsException(e, str(file_name)) from edef get_yaml_cfg(file_name: str | Path) -> dict:    """Shortcut to load a YAML config from a file, located in the    - :py:obj:`get_user_cfg_folder` or    - in the ``searx`` folder of the SearXNG installation    """    folder = get_user_cfg_folder() or Path(searx_dir)    fname = folder / file_name    if not fname.is_file():        raise FileNotFoundError(f"File {fname} does not exist!")    return load_yaml(fname)def get_user_cfg_folder() -> Path | None:    """Returns folder where the local configurations are located.    1. If the ``SEARXNG_SETTINGS_PATH`` environment is set and points to a       folder (e.g. ``/etc/mysxng/``), all local configurations are expected in       this folder.  The settings of the :ref:`SearXNG appl <searxng       settings.yml>` then expected in ``settings.yml``       (e.g. ``/etc/mysxng/settings.yml``).    2. If the ``SEARXNG_SETTINGS_PATH`` environment is set and points to a file       (e.g. ``/etc/mysxng/myinstance.yml``), this file contains the settings of       the :ref:`SearXNG appl <searxng settings.yml>` and the folder       (e.g. ``/etc/mysxng/``) is used for all other configurations.       This type (``SEARXNG_SETTINGS_PATH`` points to a file) is suitable for       use cases in which different profiles of the :ref:`SearXNG appl <searxng       settings.yml>` are to be managed, such as in test scenarios.    3. If folder ``/etc/searxng`` exists, it is used.    In case none of the above path exists, ``None`` is returned.  In case of    environment ``SEARXNG_SETTINGS_PATH`` is set, but the (folder or file) does    not exists, a :py:obj:`EnvironmentError` is raised.    """    folder = None    settings_path = os.environ.get("SEARXNG_SETTINGS_PATH")    # Disable default /etc/searxng is intended exclusively for internal testing purposes    # and is therefore not documented!    disable_etc = os.environ.get('SEARXNG_DISABLE_ETC_SETTINGS', '').lower() in ('1', 'true')    if settings_path:        # rule 1. and 2.        settings_path = Path(settings_path)        if settings_path.is_dir():            folder = settings_path        elif settings_path.is_file():            folder = settings_path.parent        else:            raise EnvironmentError(1, f"{settings_path} not exists!", settings_path)    if not folder and not disable_etc:        # default: rule 3.        folder = Path("/etc/searxng")        if not folder.is_dir():            folder = None    return folderdef update_dict(default_dict, user_dict):    for k, v in user_dict.items():        if isinstance(v, Mapping):            default_dict[k] = update_dict(default_dict.get(k, {}), v)        else:            default_dict[k] = v    return default_dictdef update_settings(default_settings: dict, user_settings: dict):    # pylint: disable=too-many-branches    # merge everything except the engines    for k, v in user_settings.items():        if k not in ('use_default_settings', 'engines'):            if k in default_settings and isinstance(v, Mapping):                update_dict(default_settings[k], v)            else:                default_settings[k] = v    categories_as_tabs = user_settings.get('categories_as_tabs')    if categories_as_tabs:        default_settings['categories_as_tabs'] = categories_as_tabs    # parse the engines    remove_engines = None    keep_only_engines = None    use_default_settings = user_settings.get('use_default_settings')    if isinstance(use_default_settings, dict):        remove_engines = use_default_settings.get('engines', {}).get('remove')        keep_only_engines = use_default_settings.get('engines', {}).get('keep_only')    if 'engines' in user_settings or remove_engines is not None or keep_only_engines is not None:        engines = default_settings['engines']        # parse "use_default_settings.engines.remove"        if remove_engines is not None:            engines = list(filterfalse(lambda engine: (engine.get('name')) in remove_engines, engines))        # parse "use_default_settings.engines.keep_only"        if keep_only_engines is not None:            engines = list(filter(lambda engine: (engine.get('name')) in keep_only_engines, engines))        # parse "engines"        user_engines = user_settings.get('engines')        if user_engines:            engines_dict = dict((definition['name'], definition) for definition in engines)            for user_engine in user_engines:                default_engine = engines_dict.get(user_engine['name'])                if default_engine:                    update_dict(default_engine, user_engine)                else:                    engines.append(user_engine)        # store the result        default_settings['engines'] = engines    return default_settingsdef is_use_default_settings(user_settings):    use_default_settings = user_settings.get('use_default_settings')    if use_default_settings is True:        return True    if isinstance(use_default_settings, dict):        return True    if use_default_settings is False or use_default_settings is None:        return False    raise ValueError('Invalid value for use_default_settings')def load_settings(load_user_settings=True) -> tuple[dict, str]:    """Function for loading the settings of the SearXNG application    (:ref:`settings.yml <searxng settings.yml>`)."""    msg = f"load the default settings from {DEFAULT_SETTINGS_FILE}"    cfg = load_yaml(DEFAULT_SETTINGS_FILE)    cfg_folder = get_user_cfg_folder()    if not load_user_settings or not cfg_folder:        return cfg, msg    settings_yml = os.environ.get("SEARXNG_SETTINGS_PATH")    if settings_yml and Path(settings_yml).is_file():        # see get_user_cfg_folder() --> SEARXNG_SETTINGS_PATH points to a file        settings_yml = Path(settings_yml).name    else:        # see get_user_cfg_folder() --> SEARXNG_SETTINGS_PATH points to a folder        settings_yml = SETTINGS_YAML    cfg_file = cfg_folder / settings_yml    if not cfg_file.exists():        return cfg, msg    msg = f"load the user settings from {cfg_file}"    user_cfg = load_yaml(cfg_file)    if is_use_default_settings(user_cfg):        # the user settings are merged with the default configuration        msg = f"merge the default settings ( {DEFAULT_SETTINGS_FILE} ) and the user settings ( {cfg_file} )"        update_settings(cfg, user_cfg)    else:        cfg = user_cfg    return cfg, msg
 |