settings_defaults.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. # lint: pylint
  3. # pylint: disable=missing-function-docstring, missing-module-docstring
  4. import typing
  5. import numbers
  6. import errno
  7. import os
  8. import logging
  9. from os.path import dirname, abspath
  10. from searx.languages import language_codes as languages
  11. searx_dir = abspath(dirname(__file__))
  12. logger = logging.getLogger('searx')
  13. OUTPUT_FORMATS = ['html', 'csv', 'json', 'rss']
  14. LANGUAGE_CODES = ('', 'all') + tuple(l[0] for l in languages)
  15. OSCAR_STYLE = ('logicodev', 'logicodev-dark', 'pointhi')
  16. CATEGORY_ORDER = [
  17. 'general',
  18. 'images',
  19. 'videos',
  20. 'news',
  21. 'map',
  22. 'music',
  23. 'it',
  24. 'science',
  25. 'files',
  26. 'social medias',
  27. ]
  28. STR_TO_BOOL = {
  29. '0': False,
  30. 'false': False,
  31. 'off': False,
  32. '1': True,
  33. 'true': True,
  34. 'on': True,
  35. }
  36. _UNDEFINED = object()
  37. class SettingsValue:
  38. """Check and update a setting value
  39. """
  40. def __init__(self,
  41. type_definition: typing.Union[None, typing.Any, typing.Tuple[typing.Any]]=None,
  42. default: typing.Any=None,
  43. environ_name: str=None):
  44. self.type_definition = type_definition \
  45. if type_definition is None or isinstance(type_definition, tuple) \
  46. else (type_definition,)
  47. self.default = default
  48. self.environ_name = environ_name
  49. @property
  50. def type_definition_repr(self):
  51. types_str = [t.__name__ if isinstance(t, type) else repr(t)
  52. for t in self.type_definition]
  53. return ', '.join(types_str)
  54. def check_type_definition(self, value: typing.Any) -> None:
  55. if value in self.type_definition:
  56. return
  57. type_list = tuple(t for t in self.type_definition if isinstance(t, type))
  58. if not isinstance(value, type_list):
  59. raise ValueError('The value has to be one of these types/values: {}'\
  60. .format(self.type_definition_repr))
  61. def __call__(self, value: typing.Any) -> typing.Any:
  62. if value == _UNDEFINED:
  63. value = self.default
  64. # override existing value with environ
  65. if self.environ_name and self.environ_name in os.environ:
  66. value = os.environ[self.environ_name]
  67. if self.type_definition == (bool,):
  68. value = STR_TO_BOOL[value.lower()]
  69. #
  70. self.check_type_definition(value)
  71. return value
  72. class SettingsDirectoryValue(SettingsValue):
  73. """Check and update a setting value that is a directory path
  74. """
  75. def check_type_definition(self, value: typing.Any) -> typing.Any:
  76. super().check_type_definition(value)
  77. if not os.path.isdir(value):
  78. raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), value)
  79. def __call__(self, value: typing.Any) -> typing.Any:
  80. if value == '':
  81. value = self.default
  82. return super().__call__(value)
  83. def apply_schema(settings, schema, path_list):
  84. error = False
  85. for key, value in schema.items():
  86. if isinstance(value, SettingsValue):
  87. try:
  88. settings[key] = value(settings.get(key, _UNDEFINED))
  89. except Exception as e: # pylint: disable=broad-except
  90. # don't stop now: check other values
  91. logger.error('%s: %s', '.'.join([*path_list, key]), e)
  92. error = True
  93. elif isinstance(value, dict):
  94. error = error or apply_schema(settings.setdefault(key, {}), schema[key], [*path_list, key])
  95. else:
  96. settings.setdefault(key, value)
  97. if len(path_list) == 0 and error:
  98. raise ValueError('Invalid settings.yml')
  99. return error
  100. SCHEMA = {
  101. 'general': {
  102. 'debug': SettingsValue(bool, False, 'SEARX_DEBUG'),
  103. 'instance_name': SettingsValue(str, 'searxng'),
  104. 'contact_url': SettingsValue((None, False, str), None),
  105. },
  106. 'brand': {
  107. },
  108. 'search': {
  109. 'safe_search': SettingsValue((0,1,2), 0),
  110. 'autocomplete': SettingsValue(str, ''),
  111. 'default_lang': SettingsValue(LANGUAGE_CODES, ''),
  112. 'ban_time_on_fail': SettingsValue(numbers.Real, 5),
  113. 'max_ban_time_on_fail': SettingsValue(numbers.Real, 120),
  114. 'formats': SettingsValue(list, OUTPUT_FORMATS),
  115. },
  116. 'server': {
  117. 'port': SettingsValue(int, 8888),
  118. 'bind_address': SettingsValue(str, '127.0.0.1', 'SEARX_BIND_ADDRESS'),
  119. 'secret_key': SettingsValue(str, environ_name='SEARX_SECRET'),
  120. 'base_url': SettingsValue((False, str), False),
  121. 'image_proxy': SettingsValue(bool, False),
  122. 'http_protocol_version': SettingsValue(('1.0', '1.1'), '1.0'),
  123. 'method': SettingsValue(('POST', 'GET'), 'POST'),
  124. 'default_http_headers': SettingsValue(dict, {}),
  125. },
  126. 'ui': {
  127. 'static_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'static')),
  128. 'templates_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'templates')),
  129. 'default_theme': SettingsValue(str, 'oscar'),
  130. 'default_locale': SettingsValue(str, ''),
  131. 'theme_args': {
  132. 'oscar_style': SettingsValue(OSCAR_STYLE, 'logicodev'),
  133. },
  134. 'results_on_new_tab': SettingsValue(bool, False),
  135. 'advanced_search': SettingsValue(bool, False),
  136. 'categories_order': SettingsValue(list, CATEGORY_ORDER),
  137. },
  138. 'preferences': {
  139. 'lock': SettingsValue(list, []),
  140. },
  141. 'outgoing': {
  142. 'useragent_suffix': SettingsValue(str, ''),
  143. 'request_timeout': SettingsValue(numbers.Real, 3.0),
  144. 'enable_http2': SettingsValue(bool, True),
  145. 'max_request_timeout': SettingsValue((None, numbers.Real), None),
  146. # Magic number kept from previous code
  147. 'pool_connections': SettingsValue(int, 100),
  148. # Picked from constructor
  149. 'pool_maxsize': SettingsValue(int, 10),
  150. 'keepalive_expiry': SettingsValue(numbers.Real, 5.0),
  151. # default maximum redirect
  152. # from https://github.com/psf/requests/blob/8c211a96cdbe9fe320d63d9e1ae15c5c07e179f8/requests/models.py#L55
  153. 'max_redirects': SettingsValue(int, 30),
  154. 'retries': SettingsValue(int, 0),
  155. 'proxies': SettingsValue((None, str, dict), None),
  156. 'source_ips': SettingsValue((None, str, list), None),
  157. # Tor configuration
  158. 'using_tor_proxy': SettingsValue(bool, False),
  159. 'extra_proxy_timeout': SettingsValue(int, 0),
  160. 'networks': {
  161. },
  162. },
  163. 'plugins': SettingsValue((None, list), None),
  164. 'enabled_plugins': SettingsValue(list, []),
  165. 'checker': {
  166. 'off_when_debug': SettingsValue(bool, True),
  167. },
  168. 'engines': SettingsValue(list, []),
  169. 'locales': SettingsValue(dict, {'en': 'English'}),
  170. 'doi_resolvers': {
  171. },
  172. }
  173. def settings_set_defaults(settings):
  174. apply_schema(settings, SCHEMA, [])
  175. return settings