settings_loader.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. # pylint: disable=missing-module-docstring, too-many-branches
  3. from typing import Optional
  4. from os import environ
  5. from os.path import dirname, join, abspath, isfile
  6. from collections.abc import Mapping
  7. from itertools import filterfalse
  8. import yaml
  9. from searx.exceptions import SearxSettingsException
  10. searx_dir = abspath(dirname(__file__))
  11. def existing_filename_or_none(file_name: str) -> Optional[str]:
  12. if isfile(file_name):
  13. return file_name
  14. return None
  15. def load_yaml(file_name):
  16. try:
  17. with open(file_name, 'r', encoding='utf-8') as settings_yaml:
  18. return yaml.safe_load(settings_yaml)
  19. except IOError as e:
  20. raise SearxSettingsException(e, file_name) from e
  21. except yaml.YAMLError as e:
  22. raise SearxSettingsException(e, file_name) from e
  23. def get_yaml_file(file_name):
  24. path = existing_filename_or_none(join(searx_dir, file_name))
  25. if path is None:
  26. raise FileNotFoundError(f"File {file_name} does not exist!")
  27. return load_yaml(path)
  28. def get_default_settings_path():
  29. return existing_filename_or_none(join(searx_dir, 'settings.yml'))
  30. def get_user_settings_path() -> Optional[str]:
  31. """Get an user settings file.
  32. By descending priority:
  33. 1. ``environ['SEARXNG_SETTINGS_PATH']``
  34. 2. ``/etc/searxng/settings.yml`` except if ``SEARXNG_DISABLE_ETC_SETTINGS`` is ``true`` or ``1``
  35. 3. ``None``
  36. """
  37. # check the environment variable SEARXNG_SETTINGS_PATH
  38. # if the environment variable is defined, this is the last check
  39. if 'SEARXNG_SETTINGS_PATH' in environ:
  40. return existing_filename_or_none(environ['SEARXNG_SETTINGS_PATH'])
  41. # if SEARXNG_DISABLE_ETC_SETTINGS don't look any further
  42. if environ.get('SEARXNG_DISABLE_ETC_SETTINGS', '').lower() in ('1', 'true'):
  43. return None
  44. # check /etc/searxng/settings.yml
  45. # (continue with other locations if the file is not found)
  46. return existing_filename_or_none('/etc/searxng/settings.yml')
  47. def update_dict(default_dict, user_dict):
  48. for k, v in user_dict.items():
  49. if isinstance(v, Mapping):
  50. default_dict[k] = update_dict(default_dict.get(k, {}), v)
  51. else:
  52. default_dict[k] = v
  53. return default_dict
  54. def update_settings(default_settings, user_settings):
  55. # merge everything except the engines
  56. for k, v in user_settings.items():
  57. if k not in ('use_default_settings', 'engines'):
  58. if k in default_settings and isinstance(v, Mapping):
  59. update_dict(default_settings[k], v)
  60. else:
  61. default_settings[k] = v
  62. categories_as_tabs = user_settings.get('categories_as_tabs')
  63. if categories_as_tabs:
  64. default_settings['categories_as_tabs'] = categories_as_tabs
  65. # parse the engines
  66. remove_engines = None
  67. keep_only_engines = None
  68. use_default_settings = user_settings.get('use_default_settings')
  69. if isinstance(use_default_settings, dict):
  70. remove_engines = use_default_settings.get('engines', {}).get('remove')
  71. keep_only_engines = use_default_settings.get('engines', {}).get('keep_only')
  72. if 'engines' in user_settings or remove_engines is not None or keep_only_engines is not None:
  73. engines = default_settings['engines']
  74. # parse "use_default_settings.engines.remove"
  75. if remove_engines is not None:
  76. engines = list(filterfalse(lambda engine: (engine.get('name')) in remove_engines, engines))
  77. # parse "use_default_settings.engines.keep_only"
  78. if keep_only_engines is not None:
  79. engines = list(filter(lambda engine: (engine.get('name')) in keep_only_engines, engines))
  80. # parse "engines"
  81. user_engines = user_settings.get('engines')
  82. if user_engines:
  83. engines_dict = dict((definition['name'], definition) for definition in engines)
  84. for user_engine in user_engines:
  85. default_engine = engines_dict.get(user_engine['name'])
  86. if default_engine:
  87. update_dict(default_engine, user_engine)
  88. else:
  89. engines.append(user_engine)
  90. # store the result
  91. default_settings['engines'] = engines
  92. return default_settings
  93. def is_use_default_settings(user_settings):
  94. use_default_settings = user_settings.get('use_default_settings')
  95. if use_default_settings is True:
  96. return True
  97. if isinstance(use_default_settings, dict):
  98. return True
  99. if use_default_settings is False or use_default_settings is None:
  100. return False
  101. raise ValueError('Invalid value for use_default_settings')
  102. def load_settings(load_user_settings=True):
  103. default_settings_path = get_default_settings_path()
  104. user_settings_path = get_user_settings_path()
  105. if user_settings_path is None or not load_user_settings:
  106. # no user settings
  107. return (load_yaml(default_settings_path), 'load the default settings from {}'.format(default_settings_path))
  108. # user settings
  109. user_settings = load_yaml(user_settings_path)
  110. if is_use_default_settings(user_settings):
  111. # the user settings are merged with the default configuration
  112. default_settings = load_yaml(default_settings_path)
  113. update_settings(default_settings, user_settings)
  114. return (
  115. default_settings,
  116. 'merge the default settings ( {} ) and the user settings ( {} )'.format(
  117. default_settings_path, user_settings_path
  118. ),
  119. )
  120. # the user settings, fully replace the default configuration
  121. return (user_settings, 'load the user settings from {}'.format(user_settings_path))