[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 cf4c8f98b8
No known key found for this signature in database
20 changed files with 168 additions and 108 deletions

View File

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

View File

@ -12,13 +12,13 @@ Plugins
The built-in plugins can be activated or deactivated via the settings The built-in plugins can be activated or deactivated via the settings
(:ref:`settings enabled_plugins`) and external plugins can be integrated into (:ref:`settings plugins`) and external plugins can be integrated into
SearXNG (:ref:`settings external_plugins`). SearXNG (:ref:`settings external_plugins`).
.. _settings enabled_plugins: .. _settings plugins:
``enabled_plugins:`` (internal) ``plugins:`` (internal)
=============================== ===============================
In :ref:`plugins admin` you find a complete list of all plugins, the default In :ref:`plugins admin` you find a complete list of all plugins, the default
@ -26,14 +26,23 @@ configuration looks like:
.. code:: yaml .. code:: yaml
enabled_plugins: plugins:
- 'Basic Calculator' - id: 'calculator'
- 'Hash plugin' default_on: true
- 'Self Information' - id: 'hash_plugin'
- 'Tracker URL remover' default_on: true
- 'Unit converter plugin' - id: 'self_info'
- 'Ahmia blacklist' default_on: true
- id: 'tracker_url_remover'
default_on: true
- id: 'unit_converter'
default_on: true
- id: 'ahmia_filter' # 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``. To completely disable a
plugin, you can set `inactive` to `true`.
.. _settings external_plugins: .. _settings external_plugins:
@ -56,9 +65,11 @@ In the :ref:`settings.yml` activate the ``plugins:`` section and add module
.. code:: yaml .. code:: yaml
plugins: plugins:
- only_show_green_results - id: only_show_green_results
# - mypackage.mymodule.MyPlugin default_on: true
# - mypackage.mymodule.MyOtherPlugin # - fqn: mypackage.mymodule.MyPlugin
# - fqn: mypackage.mymodule.MyOtherPlugin
# default_on: false
.. hint:: .. hint::

View File

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

View File

@ -75,9 +75,6 @@ class Plugin(abc.ABC):
id: typing.ClassVar[str] id: typing.ClassVar[str]
"""The ID (suffix) in the HTML form.""" """The ID (suffix) in the HTML form."""
default_on: typing.ClassVar[bool]
"""Plugin is enabled/disabled by default."""
keywords: list[str] = [] keywords: list[str] = []
"""Keywords in the search query that activate the plugin. The *keyword* is """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 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: def __init__(self) -> None:
super().__init__() super().__init__()
for attr in ["id", "default_on"]: if getattr(self, "id", None) is None:
if getattr(self, attr, None) is None: raise NotImplementedError(f"plugin {self} is missing attribute id")
raise NotImplementedError(f"plugin {self} is missing attribute {attr}")
if not self.id: if not self.id:
self.id = f"{self.__class__.__module__}.{self.__class__.__name__}" self.id = f"{self.__class__.__module__}.{self.__class__.__name__}"
@ -117,6 +113,25 @@ class Plugin(abc.ABC):
return hash(self) == hash(other) 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!
"""
plugins = searx.get_setting('plugins', [])
for plugin in plugins:
if isinstance(plugin, dict):
if plugin.get('id') == self.id:
return plugin.get('default_on', True)
# legacy way of enabling plugins (list of strings) - TODO: remove in the future
legacy_enabled_plugins = searx.get_setting('enabled_plugins', [])
if not legacy_enabled_plugins:
return False
return self.info.name in legacy_enabled_plugins
def init(self, app: flask.Flask) -> bool: # pylint: disable=unused-argument def init(self, app: flask.Flask) -> bool: # pylint: disable=unused-argument
"""Initialization of the plugin, the return value decides whether this """Initialization of the plugin, the return value decides whether this
plugin is active or not. Initialization only takes place once, at the plugin is active or not. Initialization only takes place once, at the
@ -176,7 +191,7 @@ class ModulePlugin(Plugin):
- `module.logger` --> :py:obj:`Plugin.log` - `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): def __init__(self, mod: types.ModuleType):
"""In case of missing attributes in the module or wrong types are given, """In case of missing attributes in the module or wrong types are given,
@ -197,7 +212,6 @@ class ModulePlugin(Plugin):
self.log.critical(msg) self.log.critical(msg)
raise TypeError(msg) raise TypeError(msg)
self.default_on = mod.default_on
self.info = PluginInfo( self.info = PluginInfo(
id=self.id, id=self.id,
name=self.module.name, name=self.module.name,
@ -271,21 +285,40 @@ class PluginStorage:
- the external plugins from :ref:`settings plugins`. - the external plugins from :ref:`settings plugins`.
""" """
for f in _default.iterdir(): plugins = searx.get_setting('plugins', [])
# only there for backwards compatibility - TODO: remove
if not plugins and searx.get_setting('enabled_plugins', []):
for f in _default.iterdir():
if f.name.startswith("_"): if f.name.startswith("_"):
continue continue
if f.stem not in self.legacy_plugins: self.load_by_id(f.stem)
self.register_by_fqn(f"searx.plugins.{f.stem}.SXNGPlugin")
for plugin in plugins:
if isinstance(plugin, dict):
if plugin.get('inactive', False):
continue continue
# for backward compatibility if 'fqn' in plugin:
mod = load_module(f.name, str(f.parent)) self.register_by_fqn(plugin['fqn'])
self.register(ModulePlugin(mod)) elif 'id' in plugin:
self.load_by_id(plugin['id'])
else:
log.debug('Invalid plugin configuration: %s', plugin)
for fqn in searx.get_setting("plugins"): # type: ignore # legacy way of enabling plugins - TODO: remove in the future
self.register_by_fqn(fqn) elif isinstance(plugin, str):
self.register_by_fqn(plugin)
def load_by_id(self, plugin_id):
if plugin_id not in self.legacy_plugins:
self.register_by_fqn(f"searx.plugins.{plugin_id}.SXNGPlugin")
return
# for backward compatibility
mod = load_module(f"{plugin_id}.py", _default)
self.register(ModulePlugin(mod))
def register(self, plugin: Plugin): def register(self, plugin: Plugin):
"""Register a :py:obj:`Plugin`. In case of name collision (if two """Register a :py:obj:`Plugin`. In case of name collision (if two

View File

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

View File

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

View File

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

View File

@ -7,12 +7,13 @@
plugin"**, see :pull:`3463` & :pull:`3552`. plugin"**, see :pull:`3463` & :pull:`3552`.
The **Hostnames plugin** can be enabled by adding it to the The **Hostnames plugin** can be enabled by adding it to the
``enabled_plugins`` **list** in the ``setting.yml`` like so. ``plugins`` **list** in the ``setting.yml`` like so.
.. code:: yaml .. code:: yaml
enabled_plugins: plugins:
- 'Hostnames plugin' - id: 'hostnames'
default_on: true
... ...
- ``hostnames.replace``: A **mapping** of regular expressions to hostnames to be - ``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') name = gettext('Hostnames plugin')
description = gettext('Rewrite hostnames, remove results or prioritize them based on the hostname') description = gettext('Rewrite hostnames, remove results or prioritize them based on the hostname')
default_on = False
preference_section = 'general' preference_section = 'general'
plugin_id = 'hostnames' plugin_id = 'hostnames'

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,8 @@ Enable in ``settings.yml``:
enabled_plugins: 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" name = "Unit converter plugin"
description = gettext("Convert between units") description = gettext("Convert between units")
default_on = True
plugin_id = "unit_converter" plugin_id = "unit_converter"
preference_section = "general" preference_section = "general"

View File

@ -316,7 +316,8 @@ class PluginsSetting(BooleanChoices):
"""Plugin settings""" """Plugin settings"""
def __init__(self, default_value, plugins: Iterable[searx.plugins.Plugin]): 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): def transform_form_items(self, items):
return [item[len('plugin_') :] for item in items] return [item[len('plugin_') :] for item in items]

View File

@ -226,30 +226,43 @@ outgoing:
# - 1.1.1.2 # - 1.1.1.2
# - fe80::/126 # - fe80::/126
# 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
#
# You can set 'inactive' to 'true' in order to force disable a plugin
# that's enabled by default.
plugins:
- id: 'calculator'
default_on: true
- id: 'hash_plugin'
default_on: true
- id: 'self_info'
default_on: true
- id: 'tracker_url_remover'
default_on: true
- id: 'unit_converter'
default_on: true
- id: 'ahmia_filter' # activation depends on outgoing.using_tor_proxy
default_on: true
#
# These plugins are completely disabled if nothing is configured ..
# - id: 'hostnames' # see 'hostnames' configuration below
# default_on: false
# - id: 'oa_doi_rewrite'
# default_on: false
# - id: 'tor_check'
# default_on: false
#
# External plugin configuration, for more details see # External plugin configuration, for more details see
# https://docs.searxng.org/admin/settings/settings_plugins.html # https://docs.searxng.org/admin/settings/settings_plugins.html
# # - fqn: mypackage.mymodule.MyPlugin
# plugins: # default_on: true
# - mypackage.mymodule.MyPlugin # - fqn: mypackage.mymodule.MyOtherPlugin
# - mypackage.mymodule.MyOtherPlugin # default_on: false
# - ... # - ...
# 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'
# Configuration of the "Hostnames plugin": # Configuration of the "Hostnames plugin":
# #
# hostnames: # hostnames:

View File

@ -1,7 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementation of the default settings. """Implementation of the default settings."""
"""
import typing import typing
import numbers import numbers
@ -236,7 +234,7 @@ SCHEMA = {
'proxify_results': SettingsValue(bool, False), 'proxify_results': SettingsValue(bool, False),
}, },
'plugins': SettingsValue(list, []), 'plugins': SettingsValue(list, []),
'enabled_plugins': SettingsValue((None, list), None), 'enabled_plugins': SettingsValue((None, list), None), # legacy - TODO: remove
'checker': { 'checker': {
'off_when_debug': SettingsValue(bool, True, None), 'off_when_debug': SettingsValue(bool, True, None),
'scheduling': SettingsValue((None, dict), None, None), 'scheduling': SettingsValue((None, dict), None, None),

View File

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

View File

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

View File

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

View File

@ -31,16 +31,20 @@ ui:
# - autocomplete # - autocomplete
# - method # - method
enabled_plugins: plugins:
- 'Hash plugin' - id: 'calculator'
- 'Self Information' default_on: true
- 'Tracker URL remover' - id: 'hash_plugin'
- 'Ahmia blacklist' default_on: true
# - 'Hostnames plugin' # see 'hostnames' configuration below - id: 'self_info'
# - 'Open Access DOI rewrite' default_on: true
- id: 'tracker_url_remover'
# plugins: default_on: true
# - only_show_green_results - id: 'unit_converter'
default_on: true
# - id: 'ahmia_filter' # activation depends on outgoing.using_tor_proxy
# - fqn: only_show_green_results
# default_on: true
# hostnames: # hostnames:
# replace: # replace: