__init__.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. # pylint: disable=missing-module-docstring, missing-class-docstring
  3. import sys
  4. from hashlib import sha256
  5. from importlib import import_module
  6. from os import listdir, makedirs, remove, stat, utime
  7. from os.path import abspath, basename, dirname, exists, join
  8. from shutil import copyfile
  9. from pkgutil import iter_modules
  10. from logging import getLogger
  11. from typing import List, Tuple
  12. from searx import logger, settings
  13. class Plugin: # pylint: disable=too-few-public-methods
  14. """This class is currently never initialized and only used for type hinting."""
  15. id: str
  16. name: str
  17. description: str
  18. default_on: bool
  19. js_dependencies: Tuple[str]
  20. css_dependencies: Tuple[str]
  21. preference_section: str
  22. logger = logger.getChild("plugins")
  23. required_attrs = (
  24. # fmt: off
  25. ("name", str),
  26. ("description", str),
  27. ("default_on", bool)
  28. # fmt: on
  29. )
  30. optional_attrs = (
  31. # fmt: off
  32. ("js_dependencies", tuple),
  33. ("css_dependencies", tuple),
  34. ("preference_section", str),
  35. # fmt: on
  36. )
  37. def sha_sum(filename):
  38. with open(filename, "rb") as f:
  39. file_content_bytes = f.read()
  40. return sha256(file_content_bytes).hexdigest()
  41. def sync_resource(base_path, resource_path, name, target_dir, plugin_dir):
  42. dep_path = join(base_path, resource_path)
  43. file_name = basename(dep_path)
  44. resource_path = join(target_dir, file_name)
  45. if not exists(resource_path) or sha_sum(dep_path) != sha_sum(resource_path):
  46. try:
  47. copyfile(dep_path, resource_path)
  48. # copy atime_ns and mtime_ns, so the weak ETags (generated by
  49. # the HTTP server) do not change
  50. dep_stat = stat(dep_path)
  51. utime(resource_path, ns=(dep_stat.st_atime_ns, dep_stat.st_mtime_ns))
  52. except IOError:
  53. logger.critical("failed to copy plugin resource {0} for plugin {1}".format(file_name, name))
  54. sys.exit(3)
  55. # returning with the web path of the resource
  56. return join("plugins/external_plugins", plugin_dir, file_name)
  57. def prepare_package_resources(plugin, plugin_module_name):
  58. plugin_base_path = dirname(abspath(plugin.__file__))
  59. plugin_dir = plugin_module_name
  60. target_dir = join(settings["ui"]["static_path"], "plugins/external_plugins", plugin_dir)
  61. try:
  62. makedirs(target_dir, exist_ok=True)
  63. except IOError:
  64. logger.critical("failed to create resource directory {0} for plugin {1}".format(target_dir, plugin_module_name))
  65. sys.exit(3)
  66. resources = []
  67. if hasattr(plugin, "js_dependencies"):
  68. resources.extend(map(basename, plugin.js_dependencies))
  69. plugin.js_dependencies = [
  70. sync_resource(plugin_base_path, x, plugin_module_name, target_dir, plugin_dir)
  71. for x in plugin.js_dependencies
  72. ]
  73. if hasattr(plugin, "css_dependencies"):
  74. resources.extend(map(basename, plugin.css_dependencies))
  75. plugin.css_dependencies = [
  76. sync_resource(plugin_base_path, x, plugin_module_name, target_dir, plugin_dir)
  77. for x in plugin.css_dependencies
  78. ]
  79. for f in listdir(target_dir):
  80. if basename(f) not in resources:
  81. resource_path = join(target_dir, basename(f))
  82. try:
  83. remove(resource_path)
  84. except IOError:
  85. logger.critical(
  86. "failed to remove unused resource file {0} for plugin {1}".format(resource_path, plugin_module_name)
  87. )
  88. sys.exit(3)
  89. def load_plugin(plugin_module_name, external):
  90. # pylint: disable=too-many-branches
  91. try:
  92. plugin = import_module(plugin_module_name)
  93. except (
  94. SyntaxError,
  95. KeyboardInterrupt,
  96. SystemExit,
  97. SystemError,
  98. ImportError,
  99. RuntimeError,
  100. ) as e:
  101. logger.critical("%s: fatal exception", plugin_module_name, exc_info=e)
  102. sys.exit(3)
  103. except BaseException:
  104. logger.exception("%s: exception while loading, the plugin is disabled", plugin_module_name)
  105. return None
  106. # difference with searx: use module name instead of the user name
  107. plugin.id = plugin_module_name
  108. #
  109. plugin.logger = getLogger(plugin_module_name)
  110. for plugin_attr, plugin_attr_type in required_attrs:
  111. if not hasattr(plugin, plugin_attr):
  112. logger.critical('%s: missing attribute "%s", cannot load plugin', plugin, plugin_attr)
  113. sys.exit(3)
  114. attr = getattr(plugin, plugin_attr)
  115. if not isinstance(attr, plugin_attr_type):
  116. type_attr = str(type(attr))
  117. logger.critical(
  118. '{1}: attribute "{0}" is of type {2}, must be of type {3}, cannot load plugin'.format(
  119. plugin, plugin_attr, type_attr, plugin_attr_type
  120. )
  121. )
  122. sys.exit(3)
  123. for plugin_attr, plugin_attr_type in optional_attrs:
  124. if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type):
  125. setattr(plugin, plugin_attr, plugin_attr_type())
  126. if not hasattr(plugin, "preference_section"):
  127. plugin.preference_section = "general"
  128. # query plugin
  129. if plugin.preference_section == "query":
  130. for plugin_attr in ("query_keywords", "query_examples"):
  131. if not hasattr(plugin, plugin_attr):
  132. logger.critical('missing attribute "{0}", cannot load plugin: {1}'.format(plugin_attr, plugin))
  133. sys.exit(3)
  134. if settings.get("enabled_plugins"):
  135. # searx compatibility: plugin.name in settings['enabled_plugins']
  136. plugin.default_on = plugin.name in settings["enabled_plugins"] or plugin.id in settings["enabled_plugins"]
  137. # copy resources if this is an external plugin
  138. if external:
  139. prepare_package_resources(plugin, plugin_module_name)
  140. logger.debug("%s: loaded", plugin_module_name)
  141. return plugin
  142. def load_and_initialize_plugin(plugin_module_name, external, init_args):
  143. plugin = load_plugin(plugin_module_name, external)
  144. if plugin and hasattr(plugin, 'init'):
  145. try:
  146. return plugin if plugin.init(*init_args) else None
  147. except Exception: # pylint: disable=broad-except
  148. plugin.logger.exception("Exception while calling init, the plugin is disabled")
  149. return None
  150. return plugin
  151. class PluginStore:
  152. def __init__(self):
  153. self.plugins: List[Plugin] = []
  154. def __iter__(self):
  155. yield from self.plugins
  156. def register(self, plugin):
  157. self.plugins.append(plugin)
  158. def call(self, ordered_plugin_list, plugin_type, *args, **kwargs):
  159. ret = True
  160. for plugin in ordered_plugin_list:
  161. if hasattr(plugin, plugin_type):
  162. try:
  163. ret = getattr(plugin, plugin_type)(*args, **kwargs)
  164. if not ret:
  165. break
  166. except Exception: # pylint: disable=broad-except
  167. plugin.logger.exception("Exception while calling %s", plugin_type)
  168. return ret
  169. plugins = PluginStore()
  170. def plugin_module_names():
  171. yield_plugins = set()
  172. # embedded plugins
  173. for module in iter_modules(path=[dirname(__file__)]):
  174. yield (__name__ + "." + module.name, False)
  175. yield_plugins.add(module.name)
  176. # external plugins
  177. for module_name in settings['plugins']:
  178. if module_name not in yield_plugins:
  179. yield (module_name, True)
  180. yield_plugins.add(module_name)
  181. def initialize(app):
  182. for module_name, external in plugin_module_names():
  183. plugin = load_and_initialize_plugin(module_name, external, (app, settings))
  184. if plugin:
  185. plugins.register(plugin)