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