settings_defaults.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. # lint: pylint
  3. """Implementation of the default settings.
  4. """
  5. import typing
  6. import numbers
  7. import errno
  8. import os
  9. import logging
  10. from os.path import dirname, abspath
  11. from searx.languages import language_codes as languages
  12. searx_dir = abspath(dirname(__file__))
  13. logger = logging.getLogger('searx')
  14. OUTPUT_FORMATS = ['html', 'csv', 'json', 'rss']
  15. LANGUAGE_CODES = ['all'] + list(l[0] for l in languages)
  16. SIMPLE_STYLE = ('auto', 'light', 'dark')
  17. CATEGORIES_AS_TABS = {
  18. 'general': {},
  19. 'images': {},
  20. 'videos': {},
  21. 'news': {},
  22. 'map': {},
  23. 'music': {},
  24. 'it': {},
  25. 'science': {},
  26. 'files': {},
  27. 'social media': {},
  28. }
  29. STR_TO_BOOL = {
  30. '0': False,
  31. 'false': False,
  32. 'off': False,
  33. '1': True,
  34. 'true': True,
  35. 'on': True,
  36. }
  37. _UNDEFINED = object()
  38. class SettingsValue:
  39. """Check and update a setting value"""
  40. def __init__(
  41. self,
  42. type_definition: typing.Union[None, typing.Any, typing.Tuple[typing.Any]] = None,
  43. default: typing.Any = None,
  44. environ_name: str = None,
  45. ):
  46. self.type_definition = (
  47. type_definition if type_definition is None or isinstance(type_definition, tuple) else (type_definition,)
  48. )
  49. self.default = default
  50. self.environ_name = environ_name
  51. @property
  52. def type_definition_repr(self):
  53. types_str = [t.__name__ if isinstance(t, type) else repr(t) for t in self.type_definition]
  54. return ', '.join(types_str)
  55. def check_type_definition(self, value: typing.Any) -> None:
  56. if value in self.type_definition:
  57. return
  58. type_list = tuple(t for t in self.type_definition if isinstance(t, type))
  59. if not isinstance(value, type_list):
  60. raise ValueError('The value has to be one of these types/values: {}'.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. self.check_type_definition(value)
  70. return value
  71. class SettingSublistValue(SettingsValue):
  72. """Check the value is a sublist of type definition."""
  73. def check_type_definition(self, value: typing.Any) -> typing.Any:
  74. if not isinstance(value, list):
  75. raise ValueError('The value has to a list')
  76. for item in value:
  77. if not item in self.type_definition[0]:
  78. raise ValueError('{} not in {}'.format(item, self.type_definition))
  79. class SettingsDirectoryValue(SettingsValue):
  80. """Check and update a setting value that is a directory path"""
  81. def check_type_definition(self, value: typing.Any) -> typing.Any:
  82. super().check_type_definition(value)
  83. if not os.path.isdir(value):
  84. raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), value)
  85. def __call__(self, value: typing.Any) -> typing.Any:
  86. if value == '':
  87. value = self.default
  88. return super().__call__(value)
  89. def apply_schema(settings, schema, path_list):
  90. error = False
  91. for key, value in schema.items():
  92. if isinstance(value, SettingsValue):
  93. try:
  94. settings[key] = value(settings.get(key, _UNDEFINED))
  95. except Exception as e: # pylint: disable=broad-except
  96. # don't stop now: check other values
  97. logger.error('%s: %s', '.'.join([*path_list, key]), e)
  98. error = True
  99. elif isinstance(value, dict):
  100. error = error or apply_schema(settings.setdefault(key, {}), schema[key], [*path_list, key])
  101. else:
  102. settings.setdefault(key, value)
  103. if len(path_list) == 0 and error:
  104. raise ValueError('Invalid settings.yml')
  105. return error
  106. SCHEMA = {
  107. 'general': {
  108. 'debug': SettingsValue(bool, False, 'SEARXNG_DEBUG'),
  109. 'instance_name': SettingsValue(str, 'SearXNG'),
  110. 'privacypolicy_url': SettingsValue((None, False, str), None),
  111. 'contact_url': SettingsValue((None, False, str), None),
  112. 'donation_url': SettingsValue((bool, str), "https://docs.searxng.org/donate.html"),
  113. 'enable_metrics': SettingsValue(bool, True),
  114. },
  115. 'brand': {
  116. 'issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues'),
  117. 'new_issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues/new'),
  118. 'docs_url': SettingsValue(str, 'https://docs.searxng.org'),
  119. 'public_instances': SettingsValue((False, str), 'https://searx.space'),
  120. 'wiki_url': SettingsValue(str, 'https://github.com/searxng/searxng/wiki'),
  121. },
  122. 'search': {
  123. 'safe_search': SettingsValue((0, 1, 2), 0),
  124. 'autocomplete': SettingsValue(str, ''),
  125. 'autocomplete_min': SettingsValue(int, 4),
  126. 'default_lang': SettingsValue(tuple(LANGUAGE_CODES + ['']), ''),
  127. 'languages': SettingSublistValue(LANGUAGE_CODES, LANGUAGE_CODES),
  128. 'ban_time_on_fail': SettingsValue(numbers.Real, 5),
  129. 'max_ban_time_on_fail': SettingsValue(numbers.Real, 120),
  130. 'formats': SettingsValue(list, OUTPUT_FORMATS),
  131. },
  132. 'server': {
  133. 'port': SettingsValue((int, str), 8888, 'SEARXNG_PORT'),
  134. 'bind_address': SettingsValue(str, '127.0.0.1', 'SEARXNG_BIND_ADDRESS'),
  135. 'limiter': SettingsValue(bool, False),
  136. 'secret_key': SettingsValue(str, environ_name='SEARXNG_SECRET'),
  137. 'base_url': SettingsValue((False, str), False, 'SEARXNG_BASE_URL'),
  138. 'image_proxy': SettingsValue(bool, False),
  139. 'http_protocol_version': SettingsValue(('1.0', '1.1'), '1.0'),
  140. 'method': SettingsValue(('POST', 'GET'), 'POST'),
  141. 'default_http_headers': SettingsValue(dict, {}),
  142. },
  143. 'redis': {
  144. 'url': SettingsValue(str, 'unix:///usr/local/searxng-redis/run/redis.sock?db=0'),
  145. },
  146. 'ui': {
  147. 'static_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'static')),
  148. 'static_use_hash': SettingsValue(bool, False),
  149. 'templates_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'templates')),
  150. 'default_theme': SettingsValue(str, 'simple'),
  151. 'default_locale': SettingsValue(str, ''),
  152. 'theme_args': {
  153. 'simple_style': SettingsValue(SIMPLE_STYLE, 'auto'),
  154. },
  155. 'center_alignment': SettingsValue(bool, False),
  156. 'results_on_new_tab': SettingsValue(bool, False),
  157. 'advanced_search': SettingsValue(bool, False),
  158. 'query_in_title': SettingsValue(bool, False),
  159. 'infinite_scroll': SettingsValue(bool, False),
  160. },
  161. 'preferences': {
  162. 'lock': SettingsValue(list, []),
  163. },
  164. 'outgoing': {
  165. 'useragent_suffix': SettingsValue(str, ''),
  166. 'request_timeout': SettingsValue(numbers.Real, 3.0),
  167. 'enable_http2': SettingsValue(bool, True),
  168. 'max_request_timeout': SettingsValue((None, numbers.Real), None),
  169. # Magic number kept from previous code
  170. 'pool_connections': SettingsValue(int, 100),
  171. # Picked from constructor
  172. 'pool_maxsize': SettingsValue(int, 10),
  173. 'keepalive_expiry': SettingsValue(numbers.Real, 5.0),
  174. # default maximum redirect
  175. # from https://github.com/psf/requests/blob/8c211a96cdbe9fe320d63d9e1ae15c5c07e179f8/requests/models.py#L55
  176. 'max_redirects': SettingsValue(int, 30),
  177. 'retries': SettingsValue(int, 0),
  178. 'proxies': SettingsValue((None, str, dict), None),
  179. 'source_ips': SettingsValue((None, str, list), None),
  180. # Tor configuration
  181. 'using_tor_proxy': SettingsValue(bool, False),
  182. 'extra_proxy_timeout': SettingsValue(int, 0),
  183. 'networks': {},
  184. },
  185. 'plugins': SettingsValue(list, []),
  186. 'enabled_plugins': SettingsValue((None, list), None),
  187. 'checker': {
  188. 'off_when_debug': SettingsValue(bool, True),
  189. },
  190. 'categories_as_tabs': SettingsValue(dict, CATEGORIES_AS_TABS),
  191. 'engines': SettingsValue(list, []),
  192. 'doi_resolvers': {},
  193. }
  194. def settings_set_defaults(settings):
  195. apply_schema(settings, SCHEMA, [])
  196. return settings