__init__.py 7.6 KB

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