[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 %}
* - {{plg.info.name}}
- {{(plg.default_on and "y") or ""}}
- {{plg.info.description}}
{% endfor %}

View File

@ -12,13 +12,13 @@ Plugins
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`).
.. _settings enabled_plugins:
.. _settings plugins:
``enabled_plugins:`` (internal)
``plugins:`` (internal)
===============================
In :ref:`plugins admin` you find a complete list of all plugins, the default
@ -26,14 +26,23 @@ configuration looks like:
.. code:: yaml
enabled_plugins:
- 'Basic Calculator'
- 'Hash plugin'
- 'Self Information'
- 'Tracker URL remover'
- 'Unit converter plugin'
- 'Ahmia blacklist'
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
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:
@ -56,9 +65,11 @@ In the :ref:`settings.yml` activate the ``plugins:`` section and add module
.. code:: yaml
plugins:
- only_show_green_results
# - mypackage.mymodule.MyPlugin
# - mypackage.mymodule.MyOtherPlugin
- id: only_show_green_results
default_on: true
# - fqn: mypackage.mymodule.MyPlugin
# - fqn: mypackage.mymodule.MyOtherPlugin
# default_on: false
.. hint::

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,25 @@ 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!
"""
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
"""Initialization of the plugin, the return value decides whether this
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`
"""
_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 +212,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,
@ -271,21 +285,40 @@ class PluginStorage:
- the external plugins from :ref:`settings plugins`.
"""
for f in _default.iterdir():
plugins = searx.get_setting('plugins', [])
if f.name.startswith("_"):
continue
# 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("_"):
continue
if f.stem not in self.legacy_plugins:
self.register_by_fqn(f"searx.plugins.{f.stem}.SXNGPlugin")
continue
self.load_by_id(f.stem)
# for backward compatibility
mod = load_module(f.name, str(f.parent))
self.register(ModulePlugin(mod))
for plugin in plugins:
if isinstance(plugin, dict):
if plugin.get('inactive', False):
continue
for fqn in searx.get_setting("plugins"): # type: ignore
self.register_by_fqn(fqn)
if 'fqn' in plugin:
self.register_by_fqn(plugin['fqn'])
elif 'id' in plugin:
self.load_by_id(plugin['id'])
else:
log.debug('Invalid plugin configuration: %s', plugin)
# legacy way of enabling plugins - TODO: remove in the future
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):
"""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"
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

@ -7,12 +7,13 @@
plugin"**, see :pull:`3463` & :pull:`3552`.
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
enabled_plugins:
- 'Hostnames plugin'
plugins:
- id: 'hostnames'
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

@ -8,9 +8,10 @@ Enable in ``settings.yml``:
.. 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
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

@ -226,30 +226,43 @@ outgoing:
# - 1.1.1.2
# - 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
# https://docs.searxng.org/admin/settings/settings_plugins.html
#
# plugins:
# - mypackage.mymodule.MyPlugin
# - mypackage.mymodule.MyOtherPlugin
# - fqn: mypackage.mymodule.MyPlugin
# default_on: true
# - fqn: 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":
#
# hostnames:

View File

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

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,13 @@ class PluginStorage(SearxTestCase):
super().setUp()
engines = {}
searx.settings['plugins'] = [
{'id': 'plg001', 'default_on': True},
{'id': 'plg002', 'default_on': False},
]
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['plugins'] = [
{
'id': 'plg001',
},
{
'id': 'plg002',
'default_on': False,
},
{
'id': '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']))

View File

@ -31,16 +31,20 @@ ui:
# - autocomplete
# - method
enabled_plugins:
- 'Hash plugin'
- 'Self Information'
- 'Tracker URL remover'
- 'Ahmia blacklist'
# - 'Hostnames plugin' # see 'hostnames' configuration below
# - 'Open Access DOI rewrite'
# plugins:
# - only_show_green_results
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
# - fqn: only_show_green_results
# default_on: true
# hostnames:
# replace: