[refactor] plugins: allow setting default enabled state via settings.yml

This commit is contained in:
Bnyro 2025-02-05 12:24:05 +01:00
parent 4ab7984edd
commit 37716f47bb
18 changed files with 129 additions and 68 deletions

View File

@ -30,7 +30,6 @@ Configuration defaults (at built time):
{% for plg in plugins %}
* - {{plg.info.name}}
- {{(plg.default_on and "y") or ""}}
- {{plg.info.description}}
{% endfor %}

View File

@ -27,13 +27,21 @@ configuration looks like:
.. code:: yaml
enabled_plugins:
- 'Basic Calculator'
- 'Hash plugin'
- 'Self Information'
- 'Tracker URL remover'
- 'Unit converter plugin'
- 'Ahmia blacklist'
- name: 'Basic Calculator'
default_on: true
- name: 'Hash plugin'
default_on: true
- name: 'Self Information'
default_on: true
- name: 'Tracker URL remover'
default_on: true
- name: 'Unit converter plugin'
default_on: true
- name: 'Ahmia blacklist' # activation depends on outgoing.using_tor_proxy
default_on: true
In order to disable a plugin by default, but still allow users to use it by enabling
it in their user settings, set ``default_on`` to ``false``.
.. _settings external_plugins:

View File

@ -18,7 +18,6 @@ area:
class MyPlugin(Plugin):
id = "self_info"
default_on = True
def __init__(self):
super().__init__()

View File

@ -75,9 +75,6 @@ class Plugin(abc.ABC):
id: typing.ClassVar[str]
"""The ID (suffix) in the HTML form."""
default_on: typing.ClassVar[bool]
"""Plugin is enabled/disabled by default."""
keywords: list[str] = []
"""Keywords in the search query that activate the plugin. The *keyword* is
the first word in a search query. If a plugin should be executed regardless
@ -94,9 +91,8 @@ class Plugin(abc.ABC):
def __init__(self) -> None:
super().__init__()
for attr in ["id", "default_on"]:
if getattr(self, attr, None) is None:
raise NotImplementedError(f"plugin {self} is missing attribute {attr}")
if getattr(self, "id", None) is None:
raise NotImplementedError(f"plugin {self} is missing attribute id")
if not self.id:
self.id = f"{self.__class__.__module__}.{self.__class__.__name__}"
@ -117,6 +113,26 @@ class Plugin(abc.ABC):
return hash(self) == hash(other)
def is_enabled_by_default(self) -> bool:
"""
Check whether a plugin is enabled by default based on the instance's configuration
This method may not be overriden in any plugin implementation!
"""
enabled_plugins = searx.get_setting('enabled_plugins', [])
if not enabled_plugins:
return False
for enabled_plugin in enabled_plugins:
if isinstance(enabled_plugin, dict):
# for legacy reasons, it's still allowed to reference plugins by their
# name instead of their ID in the settings
if enabled_plugin.get('name') in (self.info.name, self.id):
return enabled_plugin.get('default_on', True)
# legacy way of enabling plugins (list of strings) - TODO: remove in the future
return self.info.name in enabled_plugins
def init(self, app: flask.Flask) -> bool: # pylint: disable=unused-argument
"""Initialization of the plugin, the return value decides whether this
plugin is active or not. Initialization only takes place once, at the
@ -176,7 +192,7 @@ class ModulePlugin(Plugin):
- `module.logger` --> :py:obj:`Plugin.log`
"""
_required_attrs = (("name", str), ("description", str), ("default_on", bool))
_required_attrs = (("name", str), ("description", str))
def __init__(self, mod: types.ModuleType):
"""In case of missing attributes in the module or wrong types are given,
@ -197,7 +213,6 @@ class ModulePlugin(Plugin):
self.log.critical(msg)
raise TypeError(msg)
self.default_on = mod.default_on
self.info = PluginInfo(
id=self.id,
name=self.module.name,
@ -291,6 +306,8 @@ class PluginStorage:
"""Register a :py:obj:`Plugin`. In case of name collision (if two
plugins have same ID) a :py:obj:`KeyError` exception is raised.
"""
if not self.plugin_enabled(plugin):
return
if plugin in self.plugin_list:
msg = f"name collision '{plugin.id}'"
@ -329,6 +346,28 @@ class PluginStorage:
self.register(code_obj())
def plugin_enabled(self, plugin: searx.plugins.Plugin) -> bool:
"""
Check whether a plugin is enabled based on the instance's configuration
"""
enabled_plugins = searx.get_setting('enabled_plugins', [])
if not enabled_plugins:
return False
for enabled_plugin in enabled_plugins:
if isinstance(enabled_plugin, dict):
# for legacy reasons, it's still allowed to reference plugins by their
# name instead of their ID in the settings
if enabled_plugin.get('name') in (plugin.info.name, plugin.id):
return True
# legacy way of enabling plugins - TODO: remove in the future
elif isinstance(enabled_plugin, str):
if enabled_plugin == plugin.info.name:
return True
return False
def init(self, app: flask.Flask) -> None:
"""Calls the method :py:obj:`Plugin.init` of each plugin in this
storage. Depending on its return value, the plugin is removed from

View File

@ -12,7 +12,6 @@ from searx import get_setting
name = "Ahmia blacklist"
description = "Filter out onion results that appear in Ahmia's blacklist. (See https://ahmia.fi/blacklist)"
default_on = True
preference_section = 'onions'
ahmia_blacklist: list = []

View File

@ -18,7 +18,6 @@ from searx.result_types import EngineResults
name = "Basic Calculator"
description = gettext("Calculate mathematical expressions via the search bar")
default_on = True
preference_section = 'general'
plugin_id = 'calculator'

View File

@ -22,7 +22,6 @@ class SXNGPlugin(Plugin):
"""
id = "hash_plugin"
default_on = True
keywords = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"]
def __init__(self):

View File

@ -12,7 +12,8 @@ The **Hostnames plugin** can be enabled by adding it to the
.. code:: yaml
enabled_plugins:
- 'Hostnames plugin'
- name: 'Hostnames plugin'
default_on: true
...
- ``hostnames.replace``: A **mapping** of regular expressions to hostnames to be
@ -104,7 +105,6 @@ from searx.settings_loader import get_yaml_cfg
name = gettext('Hostnames plugin')
description = gettext('Rewrite hostnames, remove results or prioritize them based on the hostname')
default_on = False
preference_section = 'general'
plugin_id = 'hostnames'

View File

@ -14,7 +14,6 @@ regex = re.compile(r'10\.\d{4,9}/[^\s]+')
name = gettext('Open Access DOI rewrite')
description = gettext('Avoid paywalls by redirecting to open-access versions of publications when available')
default_on = False
preference_section = 'general/doi_resolver'

View File

@ -23,7 +23,6 @@ class SXNGPlugin(Plugin):
"""
id = "self_info"
default_on = True
keywords = ["ip", "user-agent"]
def __init__(self):

View File

@ -10,7 +10,8 @@ Enable in ``settings.yml``:
enabled_plugins:
..
- 'Tor check plugin'
- name: 'Tor check plugin'
default_on: true
"""
@ -24,8 +25,6 @@ from searx.network import get
from searx.result_types import Answer
default_on = False
name = gettext("Tor check plugin")
'''Translated name of the plugin'''

View File

@ -17,7 +17,6 @@ regexes = {
name = gettext('Tracker URL remover')
description = gettext('Remove trackers arguments from the returned URL')
default_on = True
preference_section = 'privacy'

View File

@ -14,7 +14,8 @@ Enable in ``settings.yml``:
enabled_plugins:
..
- 'Unit converter plugin'
- name: 'Unit converter plugin'
default_on: true
"""
@ -30,7 +31,6 @@ from searx.result_types import Answer
name = "Unit converter plugin"
description = gettext("Convert between units")
default_on = True
plugin_id = "unit_converter"
preference_section = "general"

View File

@ -316,7 +316,8 @@ class PluginsSetting(BooleanChoices):
"""Plugin settings"""
def __init__(self, default_value, plugins: Iterable[searx.plugins.Plugin]):
super().__init__(default_value, {plugin.id: plugin.default_on for plugin in plugins})
plugin_states = {plugin.id: plugin.is_enabled_by_default() for plugin in plugins}
super().__init__(default_value, plugin_states)
def transform_form_items(self, items):
return [item[len('plugin_') :] for item in items]

View File

@ -234,21 +234,30 @@ outgoing:
# - mypackage.mymodule.MyOtherPlugin
# - ...
# Comment or un-comment plugin to activate / deactivate by default.
# https://docs.searxng.org/admin/settings/settings_plugins.html
#
# enabled_plugins:
# # these plugins are enabled if nothing is configured ..
# - 'Basic Calculator'
# - 'Hash plugin'
# - 'Self Information'
# - 'Tracker URL remover'
# - 'Unit converter plugin'
# - 'Ahmia blacklist' # activation depends on outgoing.using_tor_proxy
# # these plugins are disabled if nothing is configured ..
# - 'Hostnames plugin' # see 'hostnames' configuration below
# - 'Open Access DOI rewrite'
# - 'Tor check plugin'
# Comment plugins out to completely disable them.
# Set 'default_on' to false in order to disable them by default,
# but allow users to manually enable them in the settings.
# see https://docs.searxng.org/admin/settings/settings_plugins.html
enabled_plugins:
- name: 'Basic Calculator'
default_on: true
- name: 'Hash plugin'
default_on: true
- name: 'Self Information'
default_on: true
- name: 'Tracker URL remover'
default_on: true
- name: 'Unit converter plugin'
default_on: true
- name: 'Ahmia blacklist' # activation depends on outgoing.using_tor_proxy
default_on: true
# these plugins are completely disabled if nothing is configured ..
# - name: 'Hostnames plugin' # see 'hostnames' configuration below
# default_on: false
# - name: 'Open Access DOI rewrite'
# default_on: false
# - name: 'Tor check plugin'
# default_on: false
# Configuration of the "Hostnames plugin":
#

View File

@ -1292,7 +1292,7 @@ def config():
_plugins = []
for _ in searx.plugins.STORAGE:
_plugins.append({'name': _.id, 'enabled': _.default_on})
_plugins.append({'name': _.id})
_limiter_cfg = limiter.get_cfg()

View File

@ -47,10 +47,16 @@ def do_post_search(query, storage, **kwargs) -> Mock:
class PluginMock(searx.plugins.Plugin):
def __init__(self, _id: str, name: str, default_on: bool):
def __init__(self, _id: str, name: str, default_enabled: bool = False):
self.id = _id
self.default_on = default_on
self._name = name
self.default_enabled = default_enabled
self.info = searx.plugins.PluginInfo(
id=id,
name=name,
description=f"Dummy plugin: {id}",
preference_section="general",
)
super().__init__()
# pylint: disable= unused-argument
@ -63,14 +69,6 @@ class PluginMock(searx.plugins.Plugin):
def on_result(self, request, search, result) -> bool:
return False
def info(self):
return searx.plugins.PluginInfo(
id=self.id,
name=self._name,
description=f"Dummy plugin: {self.id}",
preference_section="general",
)
class PluginStorage(SearxTestCase):
@ -78,9 +76,17 @@ class PluginStorage(SearxTestCase):
super().setUp()
engines = {}
searx.settings['enabled_plugins'] = [
{
'name': 'plg001',
},
{
'name': 'plg002',
},
]
self.storage = searx.plugins.PluginStorage()
self.storage.register(PluginMock("plg001", "first plugin", True))
self.storage.register(PluginMock("plg002", "second plugin", True))
self.storage.register(PluginMock("plg001", "first plugin"))
self.storage.register(PluginMock("plg002", "second plugin"))
self.storage.init(self.app)
self.pref = searx.preferences.Preferences(["simple"], ["general"], engines, self.storage)
self.pref.parse_dict({"locale": "en"})

View File

@ -115,19 +115,26 @@ class TestSettings(SearxTestCase):
# plugins settings
def test_plugins_setting_all_default_enabled(self):
storage = searx.plugins.PluginStorage()
storage.register(PluginMock("plg001", "first plugin", True))
storage.register(PluginMock("plg002", "second plugin", True))
plgs_settings = PluginsSetting(False, storage)
self.assertEqual(set(plgs_settings.get_enabled()), {"plg001", "plg002"})
def test_plugins_setting_few_default_enabled(self):
searx.settings['enabled_plugins'] = [
{
'name': 'plg001',
},
{
'name': 'plg002',
'default_on': False,
},
{
'name': 'plg003',
'default_on': True,
},
]
storage = searx.plugins.PluginStorage()
storage.register(PluginMock("plg001", "first plugin", True))
storage.register(PluginMock("plg002", "second plugin", False))
storage.register(PluginMock("plg003", "third plugin", True))
storage.register(PluginMock("plg001", "first plugin"))
storage.register(PluginMock("plg002", "second plugin"))
storage.register(PluginMock("plg003", "third plugin"))
plgs_settings = PluginsSetting(False, storage)
self.assertEqual(set(plgs_settings.get_disabled()), set(['plg002']))
self.assertEqual(set(plgs_settings.get_enabled()), set(['plg001', 'plg003']))