| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | # SPDX-License-Identifier: AGPL-3.0-or-later | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  | """Implementations for loading configurations from YAML files.  This essentially
 | 
					
						
							|  |  |  | includes 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 default | 
					
						
							|  |  |  | configuration can be completely replaced or :ref:`customized individually | 
					
						
							|  |  |  | <use_default_settings.yml>` and the ``SEARXNG_SETTINGS_PATH`` environment | 
					
						
							|  |  |  | variable can be used to set the location from which the local customizations are | 
					
						
							|  |  |  | to be loaded. The rules used for this can be found in the | 
					
						
							|  |  |  | :py:obj:`get_user_cfg_folder` function. | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  | - 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 annotations | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import os.path | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | from collections.abc import Mapping | 
					
						
							|  |  |  | from itertools import filterfalse | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  | from pathlib import Path | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | import yaml | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from searx.exceptions import SearxSettingsException | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  | searx_dir = os.path.abspath(os.path.dirname(__file__)) | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  | SETTINGS_YAML = Path("settings.yml") | 
					
						
							|  |  |  | DEFAULT_SETTINGS_FILE = Path(searx_dir) / SETTINGS_YAML | 
					
						
							|  |  |  | """The :origin:`searx/settings.yml` file with all the default settings.""" | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  | def load_yaml(file_name: str | Path): | 
					
						
							|  |  |  |     """Load YAML config from a file.""" | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  |     try: | 
					
						
							|  |  |  |         with open(file_name, 'r', encoding='utf-8') as settings_yaml: | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |             return yaml.safe_load(settings_yaml) or {} | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  |     except IOError as e: | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |         raise SearxSettingsException(e, str(file_name)) from e | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  |     except yaml.YAMLError as e: | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |         raise SearxSettingsException(e, str(file_name)) from e | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def 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) | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  | def get_user_cfg_folder() -> Path | None: | 
					
						
							|  |  |  |     """Returns folder where the local configurations are located.
 | 
					
						
							| 
									
										
										
										
											2024-05-05 23:17:35 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |     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``). | 
					
						
							| 
									
										
										
										
											2024-05-05 23:17:35 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |     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. | 
					
						
							| 
									
										
										
										
											2024-05-05 23:17:35 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |        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. | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |     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. | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-27 13:41:14 +02:00
										 |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |     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) | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |     if not folder and not disable_etc: | 
					
						
							|  |  |  |         # default: rule 3. | 
					
						
							|  |  |  |         folder = Path("/etc/searxng") | 
					
						
							|  |  |  |         if not folder.is_dir(): | 
					
						
							|  |  |  |             folder = None | 
					
						
							| 
									
										
										
										
											2021-05-18 17:23:21 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |     return folder | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def 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_dict | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  | def update_settings(default_settings: dict, user_settings: dict): | 
					
						
							|  |  |  |     # pylint: disable=too-many-branches | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  |     # merge everything except the engines | 
					
						
							|  |  |  |     for k, v in user_settings.items(): | 
					
						
							|  |  |  |         if k not in ('use_default_settings', 'engines'): | 
					
						
							| 
									
										
										
											
												[fix] settings_loader.py - use update_dict only for mapping types
I can't set `default_doi_resolver` in `settings.yml` if I'm using
`use_default_settings`.  Searx seems to try to interpret all settings at root
level in `settings.yml` as dict, which is correct except for
`default_doi_resolver` which is at root level and a string::
    File "/usr/lib/python3.9/site-packages/searx/settings_loader.py", line 125, in load_settings
        update_settings(default_settings, user_settings)
    File "/usr/lib/python3.9/site-packages/searx/settings_loader.py", line 61, in update_settings
        update_dict(default_settings[k], v)
    File "/usr/lib/python3.9/site-packages/searx/settings_loader.py", line 48, in update_dict
        for k, v in user_dict.items():
    AttributeError: 'str' object has no attribute 'items'
Signed-off-by: Markus Heiser <markus@darmarit.de>
Suggested-by:  @0xhtml https://github.com/searx/searx/issues/2722#issuecomment-813391659
											
										 
											2021-04-05 16:33:48 +02:00
										 |  |  |             if k in default_settings and isinstance(v, Mapping): | 
					
						
							| 
									
										
										
										
											2020-12-03 11:35:12 +01:00
										 |  |  |                 update_dict(default_settings[k], v) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 default_settings[k] = v | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-23 18:46:05 +02:00
										 |  |  |     categories_as_tabs = user_settings.get('categories_as_tabs') | 
					
						
							|  |  |  |     if categories_as_tabs: | 
					
						
							|  |  |  |         default_settings['categories_as_tabs'] = categories_as_tabs | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  |     # 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_settings | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def is_use_default_settings(user_settings): | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  |     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') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  | 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>`)."""
 | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |     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): | 
					
						
							| 
									
										
										
										
											2020-11-27 19:32:45 +01:00
										 |  |  |         # the user settings are merged with the default configuration | 
					
						
							| 
									
										
										
										
											2024-06-12 18:01:18 +02:00
										 |  |  |         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 |