[refactor] migrate plugins from "module" to class SXNGPlugin
This patch brings two major changes: - ``Result.filter_urls(..)`` to pass a filter function for URL fields - The ``enabled_plugins:`` section in SearXNG's settings do no longer exists. To understand plugin development compile documentation: $ make docs.clean docs.live and read http://0.0.0.0:8000/dev/plugins/development.html There is no longer a distinction between built-in and external plugin, all plugins are registered via the settings in the ``plugins:`` section. In SearXNG, plugins can be registered via a fully qualified class name. A configuration (`PluginCfg`) can be transferred to the plugin, e.g. to activate it by default / *opt-in* or *opt-out* from user's point of view. built-in plugins ================ The built-in plugins are all located in the namespace `searx.plugins`. .. code:: yaml plugins: searx.plugins.calculator.SXNGPlugin: active: true searx.plugins.hash_plugin.SXNGPlugin: active: true searx.plugins.self_info.SXNGPlugin: active: true searx.plugins.tracker_url_remover.SXNGPlugin: active: true searx.plugins.unit_converter.SXNGPlugin: active: true searx.plugins.ahmia_filter.SXNGPlugin: active: true searx.plugins.hostnames.SXNGPlugin: active: true searx.plugins.oa_doi_rewrite.SXNGPlugin: active: false searx.plugins.tor_check.SXNGPlugin: active: false external plugins ================ SearXNG supports *external plugins* / there is no need to install one, SearXNG runs out of the box. - Only show green hosted results: https://github.com/return42/tgwf-searx-plugins/ To get a developer installation in a SearXNG developer environment: .. code:: sh $ git clone git@github.com:return42/tgwf-searx-plugins.git $ ./manage pyenv.cmd python -m \ pip install -e tgwf-searx-plugins To register the plugin in SearXNG add ``only_show_green_results.SXNGPlugin`` to the ``plugins:``: .. code:: yaml plugins: # ... only_show_green_results.SXNGPlugin: active: false Result.filter_urls(..) ====================== The ``Result.filter_urls(..)`` can be used to filter and/or modify URL fields. In the following example, the filter function ``my_url_filter``: .. code:: python def my_url_filter(result, field_name, url_src) -> bool | str: if "google" in url_src: return False # remove URL field from result if "facebook" in url_src: new_url = url_src.replace("facebook", "fb-dummy") return new_url # return modified URL return True # leave URL in field unchanged is applied to all URL fields in the :py:obj:`Plugin.on_result` hook: .. code:: python class MyUrlFilter(Plugin): ... def on_result(self, request, search, result) -> bool: result.filter_urls(my_url_filter) return True Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
This commit is contained in:
parent
d36da0a6c3
commit
50f92779bd
@ -4,15 +4,10 @@
|
|||||||
List of plugins
|
List of plugins
|
||||||
===============
|
===============
|
||||||
|
|
||||||
.. sidebar:: Further reading ..
|
Further reading ..
|
||||||
|
|
||||||
- :ref:`SearXNG settings <settings plugins>`
|
- :ref:`SearXNG settings <settings plugins>`
|
||||||
- :ref:`dev plugin`
|
- :ref:`dev plugin`
|
||||||
- :ref:`builtin plugins`
|
|
||||||
|
|
||||||
Configuration defaults (at built time):
|
|
||||||
|
|
||||||
:DO: Default on
|
|
||||||
|
|
||||||
.. _configured plugins:
|
.. _configured plugins:
|
||||||
|
|
||||||
@ -24,13 +19,13 @@ Configuration defaults (at built time):
|
|||||||
:widths: 3 1 9
|
:widths: 3 1 9
|
||||||
|
|
||||||
* - Name
|
* - Name
|
||||||
- DO
|
- Active
|
||||||
- Description
|
- Description
|
||||||
|
|
||||||
{% for plg in plugins %}
|
{% for plg in plugins %}
|
||||||
|
|
||||||
* - {{plg.info.name}}
|
* - {{plg.info.name}}
|
||||||
- {{(plg.default_on and "y") or ""}}
|
- {{(plg.active and "yes") or "no"}}
|
||||||
- {{plg.info.description}}
|
- {{plg.info.description}}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -1,67 +1,77 @@
|
|||||||
.. _settings plugins:
|
.. _settings plugins:
|
||||||
|
|
||||||
=======
|
============
|
||||||
Plugins
|
``plugins:``
|
||||||
=======
|
============
|
||||||
|
|
||||||
|
.. attention::
|
||||||
|
|
||||||
|
The ``enabled_plugins:`` section in SearXNG's settings no longer exists.
|
||||||
|
There is no longer a distinction between built-in and external plugin, all
|
||||||
|
plugins are registered via the settings in the ``plugins:`` section.
|
||||||
|
|
||||||
.. sidebar:: Further reading ..
|
.. sidebar:: Further reading ..
|
||||||
|
|
||||||
- :ref:`plugins admin`
|
- :ref:`plugins admin`
|
||||||
- :ref:`dev plugin`
|
- :ref:`dev plugin`
|
||||||
- :ref:`builtin plugins`
|
|
||||||
|
|
||||||
|
In SearXNG, plugins can be registered in the :py:obj:`PluginStore
|
||||||
|
<searx.plugins.PluginStorage>` via a fully qualified class name.
|
||||||
|
|
||||||
The built-in plugins can be activated or deactivated via the settings
|
A configuration (:py:obj:`PluginCfg <searx.plugins.PluginCfg>`) can be
|
||||||
(:ref:`settings enabled_plugins`) and external plugins can be integrated into
|
transferred to the plugin, e.g. to activate it by default / *opt-in* or
|
||||||
SearXNG (:ref:`settings external_plugins`).
|
*opt-out* from user's point of view.
|
||||||
|
|
||||||
|
Please note that some plugins, such as the :ref:`hostnames plugin` plugin,
|
||||||
|
require further configuration before they can be made available for selection.
|
||||||
|
|
||||||
.. _settings enabled_plugins:
|
built-in plugins
|
||||||
|
================
|
||||||
|
|
||||||
``enabled_plugins:`` (internal)
|
The built-in plugins are all located in the namespace `searx.plugins`.
|
||||||
===============================
|
|
||||||
|
|
||||||
In :ref:`plugins admin` you find a complete list of all plugins, the default
|
|
||||||
configuration looks like:
|
|
||||||
|
|
||||||
.. code:: yaml
|
.. code:: yaml
|
||||||
|
|
||||||
enabled_plugins:
|
plugins:
|
||||||
- 'Basic Calculator'
|
|
||||||
- 'Hash plugin'
|
searx.plugins.calculator.SXNGPlugin:
|
||||||
- 'Self Information'
|
active: true
|
||||||
- 'Tracker URL remover'
|
|
||||||
- 'Unit converter plugin'
|
searx.plugins.hash_plugin.SXNGPlugin:
|
||||||
- 'Ahmia blacklist'
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.self_info.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.tracker_url_remover.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.unit_converter.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.ahmia_filter.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.hostnames.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.oa_doi_rewrite.SXNGPlugin:
|
||||||
|
active: false
|
||||||
|
|
||||||
|
searx.plugins.tor_check.SXNGPlugin:
|
||||||
|
active: false
|
||||||
|
|
||||||
|
|
||||||
.. _settings external_plugins:
|
.. _settings external_plugins:
|
||||||
|
|
||||||
``plugins:`` (external)
|
external plugins
|
||||||
=======================
|
================
|
||||||
|
|
||||||
|
.. _Only show green hosted results:
|
||||||
|
https://github.com/return42/tgwf-searx-plugins/
|
||||||
|
|
||||||
SearXNG supports *external plugins* / there is no need to install one, SearXNG
|
SearXNG supports *external plugins* / there is no need to install one, SearXNG
|
||||||
runs out of the box. But to demonstrate; in the example below we install the
|
runs out of the box.
|
||||||
SearXNG plugins from *The Green Web Foundation* `[ref]
|
|
||||||
<https://www.thegreenwebfoundation.org/news/searching-the-green-web-with-searx/>`__:
|
|
||||||
|
|
||||||
.. code:: bash
|
- `Only show green hosted results`_
|
||||||
|
- ..
|
||||||
$ sudo utils/searxng.sh instance cmd bash -c
|
|
||||||
(searxng-pyenv)$ pip install git+https://github.com/return42/tgwf-searx-plugins
|
|
||||||
|
|
||||||
In the :ref:`settings.yml` activate the ``plugins:`` section and add module
|
|
||||||
``only_show_green_results`` from ``tgwf-searx-plugins``.
|
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
- only_show_green_results
|
|
||||||
# - mypackage.mymodule.MyPlugin
|
|
||||||
# - mypackage.mymodule.MyOtherPlugin
|
|
||||||
|
|
||||||
.. hint::
|
|
||||||
|
|
||||||
``only_show_green_results`` is an old plugin that was still implemented in
|
|
||||||
the old style. There is a legacy treatment for backward compatibility, but
|
|
||||||
new plugins should be implemented as a :py:obj:`searx.plugins.Plugin` class.
|
|
||||||
|
@ -3,29 +3,8 @@
|
|||||||
|
|
||||||
- :ref:`plugins admin`
|
- :ref:`plugins admin`
|
||||||
- :ref:`SearXNG settings <settings plugins>`
|
- :ref:`SearXNG settings <settings plugins>`
|
||||||
- :ref:`builtin plugins`
|
|
||||||
|
|
||||||
Plugins can extend or replace functionality of various components of SearXNG.
|
Plugins can extend or replace functionality of various components of SearXNG.
|
||||||
Here is an example of a very simple plugin that adds a "Hello" into the answer
|
|
||||||
area:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
from flask_babel import gettext as _
|
|
||||||
from searx.plugins import Plugin
|
|
||||||
from searx.result_types import Answer
|
|
||||||
|
|
||||||
class MyPlugin(Plugin):
|
|
||||||
|
|
||||||
id = "self_info"
|
|
||||||
default_on = True
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
info = PluginInfo(id=self.id, name=_("Hello"), description=_("demo plugin"))
|
|
||||||
|
|
||||||
def post_search(self, request, search):
|
|
||||||
return [ Answer(answer="Hello") ]
|
|
||||||
|
|
||||||
Entry points (hooks) define when a plugin runs. Right now only three hooks are
|
Entry points (hooks) define when a plugin runs. Right now only three hooks are
|
||||||
implemented. So feel free to implement a hook if it fits the behaviour of your
|
implemented. So feel free to implement a hook if it fits the behaviour of your
|
||||||
@ -35,9 +14,72 @@ plugin / a plugin doesn't need to implement all the hooks.
|
|||||||
- post search: :py:obj:`Plugin.post_search`
|
- post search: :py:obj:`Plugin.post_search`
|
||||||
- on each result item: :py:obj:`Plugin.on_result`
|
- on each result item: :py:obj:`Plugin.on_result`
|
||||||
|
|
||||||
For a coding example have a look at :ref:`self_info plugin`.
|
Below you will find some examples, for more coding examples have a look at the
|
||||||
|
built-in plugins :origin:`searx/plugins/` or `Only show green hosted results`_.
|
||||||
|
|
||||||
----
|
.. _Only show green hosted results:
|
||||||
|
https://github.com/return42/tgwf-searx-plugins/
|
||||||
|
|
||||||
|
|
||||||
|
Add Answer example
|
||||||
|
==================
|
||||||
|
|
||||||
|
Here is an example of a very simple plugin that adds a "Hello World" into the
|
||||||
|
answer area:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from flask_babel import gettext as _
|
||||||
|
from searx.plugins import Plugin
|
||||||
|
from searx.result_types import Answer
|
||||||
|
|
||||||
|
class MyPlugin(Plugin):
|
||||||
|
|
||||||
|
id = "hello world"
|
||||||
|
|
||||||
|
def __init__(self, plg_cfg):
|
||||||
|
super().__init__(plg_cfg)
|
||||||
|
self.info = PluginInfo(id=self.id, name=_("Hello"), description=_("demo plugin"))
|
||||||
|
|
||||||
|
def post_search(self, request, search):
|
||||||
|
return [ Answer(answer="Hello World") ]
|
||||||
|
|
||||||
|
.. _filter urls example:
|
||||||
|
|
||||||
|
Filter URLs example
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. sidebar:: Further reading ..
|
||||||
|
|
||||||
|
- :py:obj:`Result.filter_urls(..) <searx.result_types._base.Result.filter_urls>`
|
||||||
|
|
||||||
|
The :py:obj:`Result.filter_urls(..) <searx.result_types._base.Result.filter_urls>`
|
||||||
|
can be used to filter and/or modify URL fields. In the following example, the
|
||||||
|
filter function ``my_url_filter``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
def my_url_filter(result, field_name, url_src) -> bool | str:
|
||||||
|
if "google" in url_src:
|
||||||
|
return False # remove URL field from result
|
||||||
|
if "facebook" in url_src:
|
||||||
|
new_url = url_src.replace("facebook", "fb-dummy")
|
||||||
|
return new_url # return modified URL
|
||||||
|
return True # leave URL in field unchanged
|
||||||
|
|
||||||
|
is applied to all URL fields in the :py:obj:`Plugin.on_result` hook:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class MyUrlFilter(Plugin):
|
||||||
|
...
|
||||||
|
def on_result(self, request, search, result) -> bool:
|
||||||
|
result.filter_urls(my_url_filter)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
==============
|
||||||
|
|
||||||
.. autoclass:: Plugin
|
.. autoclass:: Plugin
|
||||||
:members:
|
:members:
|
||||||
@ -48,21 +90,21 @@ For a coding example have a look at :ref:`self_info plugin`.
|
|||||||
.. autoclass:: PluginStorage
|
.. autoclass:: PluginStorage
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: searx.plugins._core.ModulePlugin
|
.. autoclass:: PluginCfg
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
__all__ = ["PluginInfo", "Plugin", "PluginStorage"]
|
__all__ = ["PluginInfo", "Plugin", "PluginStorage", "PluginCfg"]
|
||||||
|
|
||||||
from ._core import PluginInfo, Plugin, PluginStorage
|
|
||||||
|
import searx
|
||||||
|
from ._core import PluginInfo, Plugin, PluginStorage, PluginCfg
|
||||||
|
|
||||||
STORAGE: PluginStorage = PluginStorage()
|
STORAGE: PluginStorage = PluginStorage()
|
||||||
|
|
||||||
|
|
||||||
def initialize(app):
|
def initialize(app):
|
||||||
STORAGE.load_builtins()
|
STORAGE.load_settings(searx.get_setting("plugins"))
|
||||||
STORAGE.init(app)
|
STORAGE.init(app)
|
||||||
|
@ -3,31 +3,24 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
__all__ = ["PluginInfo", "Plugin", "PluginStorage"]
|
__all__ = ["PluginInfo", "Plugin", "PluginCfg", "PluginStorage"]
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import importlib
|
import importlib
|
||||||
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import re
|
||||||
import types
|
|
||||||
import typing
|
import typing
|
||||||
import warnings
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
import flask
|
|
||||||
|
|
||||||
import searx
|
|
||||||
from searx.utils import load_module
|
|
||||||
from searx.extended_types import SXNG_Request
|
from searx.extended_types import SXNG_Request
|
||||||
from searx.result_types import Result
|
from searx.result_types import Result
|
||||||
|
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from searx.search import SearchWithPlugins
|
from searx.search import SearchWithPlugins
|
||||||
|
import flask
|
||||||
|
|
||||||
|
|
||||||
_default = pathlib.Path(__file__).parent
|
|
||||||
log: logging.Logger = logging.getLogger("searx.plugins")
|
log: logging.Logger = logging.getLogger("searx.plugins")
|
||||||
|
|
||||||
|
|
||||||
@ -69,14 +62,17 @@ class PluginInfo:
|
|||||||
"""See :py:obj:`Plugin.keywords`"""
|
"""See :py:obj:`Plugin.keywords`"""
|
||||||
|
|
||||||
|
|
||||||
|
ID_REGXP = re.compile("[a-z][a-z0-9].*")
|
||||||
|
|
||||||
|
|
||||||
class Plugin(abc.ABC):
|
class Plugin(abc.ABC):
|
||||||
"""Abstract base class of all Plugins."""
|
"""Abstract base class of all Plugins."""
|
||||||
|
|
||||||
id: str = ""
|
id: str = ""
|
||||||
"""The ID (suffix) in the HTML form."""
|
"""The ID (suffix) in the HTML form."""
|
||||||
|
|
||||||
default_on: bool = False
|
active: typing.ClassVar[bool]
|
||||||
"""Plugin is enabled/disabled by default."""
|
"""Plugin is enabled/disabled by default (:py:obj:`PluginCfg.active`)."""
|
||||||
|
|
||||||
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
|
||||||
@ -93,19 +89,28 @@ class Plugin(abc.ABC):
|
|||||||
|
|
||||||
fqn: str = ""
|
fqn: str = ""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, plg_cfg: PluginCfg) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if not self.fqn:
|
if not self.fqn:
|
||||||
self.fqn = self.__class__.__mro__[0].__module__
|
self.fqn = self.__class__.__mro__[0].__module__
|
||||||
|
|
||||||
for attr in ["id", "default_on"]:
|
# names from the configuration
|
||||||
|
for n, v in plg_cfg.__dict__.items():
|
||||||
|
setattr(self, n, v)
|
||||||
|
|
||||||
|
# names that must be set by the plugin implementation
|
||||||
|
for attr in [
|
||||||
|
"id",
|
||||||
|
]:
|
||||||
if getattr(self, attr, None) is None:
|
if getattr(self, attr, None) is None:
|
||||||
raise NotImplementedError(f"plugin {self} is missing attribute {attr}")
|
raise NotImplementedError(f"plugin {self} is missing attribute {attr}")
|
||||||
|
|
||||||
if not self.id:
|
if not ID_REGXP.match(self.id):
|
||||||
self.id = f"{self.__class__.__module__}.{self.__class__.__name__}"
|
raise ValueError(f"plugin ID {self.id} contains invalid character (use lowercase ASCII)")
|
||||||
|
|
||||||
if not getattr(self, "log", None):
|
if not getattr(self, "log", None):
|
||||||
self.log = log.getChild(self.id)
|
pkg_name = inspect.getmodule(self.__class__).__package__ # type: ignore
|
||||||
|
self.log = logging.getLogger(f"{pkg_name}.{self.id}")
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
"""The hash value is used in :py:obj:`set`, for example, when an object
|
"""The hash value is used in :py:obj:`set`, for example, when an object
|
||||||
@ -121,7 +126,7 @@ class Plugin(abc.ABC):
|
|||||||
|
|
||||||
return hash(self) == hash(other)
|
return hash(self) == hash(other)
|
||||||
|
|
||||||
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
|
||||||
time the WEB application is set up. The base methode always returns
|
time the WEB application is set up. The base methode always returns
|
||||||
@ -151,7 +156,8 @@ class Plugin(abc.ABC):
|
|||||||
|
|
||||||
.. hint::
|
.. hint::
|
||||||
|
|
||||||
If :py:obj:`Result.url` is modified, :py:obj:`Result.parsed_url` must
|
If :py:obj:`Result.url <searx.result_types._base.Result.url>` is modified,
|
||||||
|
:py:obj:`Result.parsed_url <searx.result_types._base.Result.parsed_url>` must
|
||||||
be changed accordingly:
|
be changed accordingly:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
@ -161,81 +167,24 @@ class Plugin(abc.ABC):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def post_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> None | typing.Sequence[Result]:
|
def post_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> None | typing.Sequence[Result]:
|
||||||
"""Runs AFTER the search request. Can return a list of :py:obj:`Result`
|
"""Runs AFTER the search request. Can return a list of
|
||||||
objects to be added to the final result list."""
|
:py:obj:`Result <searx.result_types._base.Result>` objects to be added to the
|
||||||
|
final result list."""
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class ModulePlugin(Plugin):
|
@dataclass
|
||||||
"""A wrapper class for legacy *plugins*.
|
class PluginCfg:
|
||||||
|
"""Settings of a plugin.
|
||||||
|
|
||||||
.. note::
|
.. code:: yaml
|
||||||
|
|
||||||
For internal use only!
|
mypackage.mymodule.MyPlugin:
|
||||||
|
active: true
|
||||||
In a module plugin, the follwing names are mapped:
|
|
||||||
|
|
||||||
- `module.query_keywords` --> :py:obj:`Plugin.keywords`
|
|
||||||
- `module.plugin_id` --> :py:obj:`Plugin.id`
|
|
||||||
- `module.logger` --> :py:obj:`Plugin.log`
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_required_attrs = (("name", str), ("description", str), ("default_on", bool))
|
active: bool = False
|
||||||
|
"""Plugin is active by default and the user can *opt-out* in the preferences."""
|
||||||
def __init__(self, mod: types.ModuleType, fqn: str):
|
|
||||||
"""In case of missing attributes in the module or wrong types are given,
|
|
||||||
a :py:obj:`TypeError` exception is raised."""
|
|
||||||
|
|
||||||
self.fqn = fqn
|
|
||||||
self.module = mod
|
|
||||||
self.id = getattr(self.module, "plugin_id", self.module.__name__)
|
|
||||||
self.log = logging.getLogger(self.module.__name__)
|
|
||||||
self.keywords = getattr(self.module, "query_keywords", [])
|
|
||||||
|
|
||||||
for attr, attr_type in self._required_attrs:
|
|
||||||
if not hasattr(self.module, attr):
|
|
||||||
msg = f"missing attribute {attr}, cannot load plugin"
|
|
||||||
self.log.critical(msg)
|
|
||||||
raise TypeError(msg)
|
|
||||||
if not isinstance(getattr(self.module, attr), attr_type):
|
|
||||||
msg = f"attribute {attr} is not of type {attr_type}"
|
|
||||||
self.log.critical(msg)
|
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
self.default_on = mod.default_on
|
|
||||||
self.info = PluginInfo(
|
|
||||||
id=self.id,
|
|
||||||
name=self.module.name,
|
|
||||||
description=self.module.description,
|
|
||||||
preference_section=getattr(self.module, "preference_section", None),
|
|
||||||
examples=getattr(self.module, "query_examples", []),
|
|
||||||
keywords=self.keywords,
|
|
||||||
)
|
|
||||||
|
|
||||||
# monkeypatch module
|
|
||||||
self.module.logger = self.log # type: ignore
|
|
||||||
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def init(self, app: flask.Flask) -> bool:
|
|
||||||
if not hasattr(self.module, "init"):
|
|
||||||
return True
|
|
||||||
return self.module.init(app)
|
|
||||||
|
|
||||||
def pre_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> bool:
|
|
||||||
if not hasattr(self.module, "pre_search"):
|
|
||||||
return True
|
|
||||||
return self.module.pre_search(request, search)
|
|
||||||
|
|
||||||
def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: Result) -> bool:
|
|
||||||
if not hasattr(self.module, "on_result"):
|
|
||||||
return True
|
|
||||||
return self.module.on_result(request, search, result)
|
|
||||||
|
|
||||||
def post_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> None | list[Result]:
|
|
||||||
if not hasattr(self.module, "post_search"):
|
|
||||||
return None
|
|
||||||
return self.module.post_search(request, search)
|
|
||||||
|
|
||||||
|
|
||||||
class PluginStorage:
|
class PluginStorage:
|
||||||
@ -244,22 +193,10 @@ class PluginStorage:
|
|||||||
plugin_list: set[Plugin]
|
plugin_list: set[Plugin]
|
||||||
"""The list of :py:obj:`Plugins` in this storage."""
|
"""The list of :py:obj:`Plugins` in this storage."""
|
||||||
|
|
||||||
legacy_plugins = [
|
|
||||||
"ahmia_filter",
|
|
||||||
"calculator",
|
|
||||||
"hostnames",
|
|
||||||
"oa_doi_rewrite",
|
|
||||||
"tor_check",
|
|
||||||
"tracker_url_remover",
|
|
||||||
"unit_converter",
|
|
||||||
]
|
|
||||||
"""Internal plugins implemented in the legacy style (as module / deprecated!)."""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.plugin_list = set()
|
self.plugin_list = set()
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
|
|
||||||
yield from self.plugin_list
|
yield from self.plugin_list
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
@ -267,102 +204,42 @@ class PluginStorage:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def info(self) -> list[PluginInfo]:
|
def info(self) -> list[PluginInfo]:
|
||||||
|
|
||||||
return [p.info for p in self.plugin_list]
|
return [p.info for p in self.plugin_list]
|
||||||
|
|
||||||
def load_builtins(self):
|
def load_settings(self, cfg: dict[str, dict]):
|
||||||
"""Load plugin modules from:
|
"""Load plugins configured in SearXNG's settings :ref:`settings
|
||||||
|
plugins`."""
|
||||||
|
|
||||||
- the python packages in :origin:`searx/plugins` and
|
for fqn, plg_settings in cfg.items():
|
||||||
- the external plugins from :ref:`settings plugins`.
|
cls = None
|
||||||
"""
|
mod_name, cls_name = fqn.rsplit('.', 1)
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module(mod_name)
|
||||||
|
cls = getattr(mod, cls_name, None)
|
||||||
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
||||||
|
log.exception(exc)
|
||||||
|
|
||||||
for f in _default.iterdir():
|
if cls is None:
|
||||||
|
msg = f"plugin {fqn} is not implemented"
|
||||||
if f.name.startswith("_"):
|
raise ValueError(msg)
|
||||||
continue
|
plg = cls(PluginCfg(**plg_settings))
|
||||||
|
self.register(plg)
|
||||||
if f.stem not in self.legacy_plugins:
|
|
||||||
self.register_by_fqn(f"searx.plugins.{f.stem}.SXNGPlugin")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# for backward compatibility
|
|
||||||
mod = load_module(f.name, str(f.parent))
|
|
||||||
self.register(ModulePlugin(mod, f"searx.plugins.{f.stem}"))
|
|
||||||
|
|
||||||
for fqn in searx.get_setting("plugins"): # type: ignore
|
|
||||||
self.register_by_fqn(fqn)
|
|
||||||
|
|
||||||
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
|
||||||
plugins have same ID) a :py:obj:`KeyError` exception is raised.
|
plugins have same ID) a :py:obj:`KeyError` exception is raised.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if plugin in self.plugin_list:
|
if plugin in [p.id for p in self.plugin_list]:
|
||||||
msg = f"name collision '{plugin.id}'"
|
msg = f"name collision '{plugin.id}'"
|
||||||
plugin.log.critical(msg)
|
plugin.log.critical(msg)
|
||||||
raise KeyError(msg)
|
raise KeyError(msg)
|
||||||
|
|
||||||
if not plugin.fqn.startswith("searx.plugins."):
|
|
||||||
self.plugin_list.add(plugin)
|
|
||||||
plugin.log.debug("plugin has been registered")
|
|
||||||
return
|
|
||||||
|
|
||||||
# backward compatibility for the enabled_plugins setting
|
|
||||||
# https://docs.searxng.org/admin/settings/settings_plugins.html#enabled-plugins-internal
|
|
||||||
en_plgs: list[str] | None = searx.get_setting("enabled_plugins") # type:ignore
|
|
||||||
|
|
||||||
if en_plgs is None:
|
|
||||||
# enabled_plugins not listed in the /etc/searxng/settings.yml:
|
|
||||||
# check default_on before register ..
|
|
||||||
if plugin.default_on:
|
|
||||||
self.plugin_list.add(plugin)
|
|
||||||
plugin.log.debug("builtin plugin has been registered by SearXNG's defaults")
|
|
||||||
return
|
|
||||||
plugin.log.debug("builtin plugin is not registered by SearXNG's defaults")
|
|
||||||
return
|
|
||||||
|
|
||||||
if plugin.info.name not in en_plgs:
|
|
||||||
# enabled_plugins listed in the /etc/searxng/settings.yml,
|
|
||||||
# but this plugin is not listed in:
|
|
||||||
plugin.log.debug("builtin plugin is not registered by maintainer's settings")
|
|
||||||
return
|
|
||||||
|
|
||||||
# if the plugin is in enabled_plugins, then it is on by default.
|
|
||||||
plugin.default_on = True
|
|
||||||
self.plugin_list.add(plugin)
|
self.plugin_list.add(plugin)
|
||||||
plugin.log.debug("builtin plugin is registered by maintainer's settings")
|
plugin.log.debug("plugin has been loaded")
|
||||||
|
|
||||||
def register_by_fqn(self, fqn: str):
|
def init(self, app: "flask.Flask") -> None:
|
||||||
"""Register a :py:obj:`Plugin` via its fully qualified class name (FQN).
|
|
||||||
The FQNs of external plugins could be read from a configuration, for
|
|
||||||
example, and registered using this method
|
|
||||||
"""
|
|
||||||
|
|
||||||
mod_name, _, obj_name = fqn.rpartition('.')
|
|
||||||
if not mod_name:
|
|
||||||
# for backward compatibility
|
|
||||||
code_obj = importlib.import_module(fqn)
|
|
||||||
else:
|
|
||||||
mod = importlib.import_module(mod_name)
|
|
||||||
code_obj = getattr(mod, obj_name, None)
|
|
||||||
|
|
||||||
if code_obj is None:
|
|
||||||
msg = f"plugin {fqn} is not implemented"
|
|
||||||
log.critical(msg)
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
if isinstance(code_obj, types.ModuleType):
|
|
||||||
# for backward compatibility
|
|
||||||
warnings.warn(
|
|
||||||
f"plugin {fqn} is implemented in a legacy module / migrate to searx.plugins.Plugin", DeprecationWarning
|
|
||||||
)
|
|
||||||
|
|
||||||
self.register(ModulePlugin(code_obj, fqn))
|
|
||||||
return
|
|
||||||
|
|
||||||
self.register(code_obj())
|
|
||||||
|
|
||||||
def init(self, app: flask.Flask) -> None:
|
|
||||||
"""Calls the method :py:obj:`Plugin.init` of each plugin in this
|
"""Calls the method :py:obj:`Plugin.init` of each plugin in this
|
||||||
storage. Depending on its return value, the plugin is removed from
|
storage. Depending on its return value, the plugin is removed from
|
||||||
*this* storage or not."""
|
*this* storage or not."""
|
||||||
|
@ -1,34 +1,51 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
# pylint: disable=missing-module-docstring
|
# pylint: disable=missing-module-docstring
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import typing
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
|
||||||
import flask
|
from flask_babel import gettext
|
||||||
|
|
||||||
from searx.data import ahmia_blacklist_loader
|
from searx.data import ahmia_blacklist_loader
|
||||||
from searx import get_setting
|
from searx import get_setting
|
||||||
|
from searx.plugins import Plugin, PluginInfo
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
name = "Ahmia blacklist"
|
import flask
|
||||||
description = "Filter out onion results that appear in Ahmia's blacklist. (See https://ahmia.fi/blacklist)"
|
from searx.search import SearchWithPlugins
|
||||||
default_on = True
|
from searx.extended_types import SXNG_Request
|
||||||
preference_section = 'onions'
|
from searx.result_types import Result
|
||||||
|
from searx.plugins import PluginCfg
|
||||||
|
|
||||||
ahmia_blacklist: list = []
|
ahmia_blacklist: list = []
|
||||||
|
|
||||||
|
|
||||||
def on_result(_request, _search, result) -> bool:
|
class SXNGPlugin(Plugin):
|
||||||
if not getattr(result, 'is_onion', None) or not getattr(result, 'parsed_url', None):
|
"""Filter out onion results that appear in Ahmia's blacklist (See https://ahmia.fi/blacklist)."""
|
||||||
|
|
||||||
|
id = "ahmia_filter"
|
||||||
|
|
||||||
|
def __init__(self, plg_cfg: "PluginCfg") -> None:
|
||||||
|
super().__init__(plg_cfg)
|
||||||
|
self.info = PluginInfo(
|
||||||
|
id=self.id,
|
||||||
|
name=gettext("Ahmia blacklist"),
|
||||||
|
description=gettext("Filter out onion results that appear in Ahmia's blacklist."),
|
||||||
|
preference_section="general",
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_result(
|
||||||
|
self, request: "SXNG_Request", search: "SearchWithPlugins", result: Result
|
||||||
|
) -> bool: # pylint: disable=unused-argument
|
||||||
|
if not getattr(result, "is_onion", False) or not getattr(result, "parsed_url", False):
|
||||||
|
return True
|
||||||
|
result_hash = md5(result["parsed_url"].hostname.encode()).hexdigest()
|
||||||
|
return result_hash not in ahmia_blacklist
|
||||||
|
|
||||||
|
def init(self, app: "flask.Flask") -> bool: # pylint: disable=unused-argument
|
||||||
|
global ahmia_blacklist # pylint: disable=global-statement
|
||||||
|
if not get_setting("outgoing.using_tor_proxy"):
|
||||||
|
# disable the plugin
|
||||||
|
return False
|
||||||
|
ahmia_blacklist = ahmia_blacklist_loader()
|
||||||
return True
|
return True
|
||||||
result_hash = md5(result['parsed_url'].hostname.encode()).hexdigest()
|
|
||||||
return result_hash not in ahmia_blacklist
|
|
||||||
|
|
||||||
|
|
||||||
def init(app=flask.Flask) -> bool: # pylint: disable=unused-argument
|
|
||||||
global ahmia_blacklist # pylint: disable=global-statement
|
|
||||||
if not get_setting("outgoing.using_tor_proxy"):
|
|
||||||
# disable the plugin
|
|
||||||
return False
|
|
||||||
ahmia_blacklist = ahmia_blacklist_loader()
|
|
||||||
return True
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Calculate mathematical expressions using :py:obj`ast.parse` (mode="eval").
|
"""Calculate mathematical expressions using :py:obj:`ast.parse` (mode="eval").
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Callable
|
import typing
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
import re
|
import re
|
||||||
@ -15,14 +15,78 @@ import babel.numbers
|
|||||||
from flask_babel import gettext
|
from flask_babel import gettext
|
||||||
|
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
|
from searx.plugins import Plugin, PluginInfo
|
||||||
|
|
||||||
name = "Basic Calculator"
|
if typing.TYPE_CHECKING:
|
||||||
description = gettext("Calculate mathematical expressions via the search bar")
|
from searx.search import SearchWithPlugins
|
||||||
default_on = True
|
from searx.extended_types import SXNG_Request
|
||||||
preference_section = 'general'
|
from searx.plugins import PluginCfg
|
||||||
plugin_id = 'calculator'
|
|
||||||
|
|
||||||
operators: dict[type, Callable] = {
|
|
||||||
|
class SXNGPlugin(Plugin):
|
||||||
|
"""Plugin converts strings to different hash digests. The results are
|
||||||
|
displayed in area for the "answers".
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = "calculator"
|
||||||
|
|
||||||
|
def __init__(self, plg_cfg: "PluginCfg") -> None:
|
||||||
|
super().__init__(plg_cfg)
|
||||||
|
|
||||||
|
self.info = PluginInfo(
|
||||||
|
id=self.id,
|
||||||
|
name=gettext("Basic Calculator"),
|
||||||
|
description=gettext("Calculate mathematical expressions via the search bar"),
|
||||||
|
preference_section="general",
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> EngineResults:
|
||||||
|
results = EngineResults()
|
||||||
|
|
||||||
|
# only show the result of the expression on the first page
|
||||||
|
if search.search_query.pageno > 1:
|
||||||
|
return results
|
||||||
|
|
||||||
|
query = search.search_query.query
|
||||||
|
# in order to avoid DoS attacks with long expressions, ignore long expressions
|
||||||
|
if len(query) > 100:
|
||||||
|
return results
|
||||||
|
|
||||||
|
# replace commonly used math operators with their proper Python operator
|
||||||
|
query = query.replace("x", "*").replace(":", "/")
|
||||||
|
|
||||||
|
# use UI language
|
||||||
|
ui_locale = babel.Locale.parse(request.preferences.get_value("locale"), sep="-")
|
||||||
|
|
||||||
|
# parse the number system in a localized way
|
||||||
|
def _decimal(match: re.Match) -> str:
|
||||||
|
val = match.string[match.start() : match.end()]
|
||||||
|
val = babel.numbers.parse_decimal(val, ui_locale, numbering_system="latn")
|
||||||
|
return str(val)
|
||||||
|
|
||||||
|
decimal = ui_locale.number_symbols["latn"]["decimal"]
|
||||||
|
group = ui_locale.number_symbols["latn"]["group"]
|
||||||
|
query = re.sub(f"[0-9]+[{decimal}|{group}][0-9]+[{decimal}|{group}]?[0-9]?", _decimal, query)
|
||||||
|
|
||||||
|
# only numbers and math operators are accepted
|
||||||
|
if any(str.isalpha(c) for c in query):
|
||||||
|
return results
|
||||||
|
|
||||||
|
# in python, powers are calculated via **
|
||||||
|
query_py_formatted = query.replace("^", "**")
|
||||||
|
|
||||||
|
# Prevent the runtime from being longer than 50 ms
|
||||||
|
res = timeout_func(0.05, _eval_expr, query_py_formatted)
|
||||||
|
if res is None or res == "":
|
||||||
|
return results
|
||||||
|
|
||||||
|
res = babel.numbers.format_decimal(res, locale=ui_locale)
|
||||||
|
results.add(results.types.Answer(answer=f"{search.search_query.query} = {res}"))
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
operators: dict[type, typing.Callable] = {
|
||||||
ast.Add: operator.add,
|
ast.Add: operator.add,
|
||||||
ast.Sub: operator.sub,
|
ast.Sub: operator.sub,
|
||||||
ast.Mult: operator.mul,
|
ast.Mult: operator.mul,
|
||||||
@ -92,49 +156,3 @@ def timeout_func(timeout, func, *args, **kwargs):
|
|||||||
p.join()
|
p.join()
|
||||||
p.close()
|
p.close()
|
||||||
return ret_val
|
return ret_val
|
||||||
|
|
||||||
|
|
||||||
def post_search(request, search) -> EngineResults:
|
|
||||||
results = EngineResults()
|
|
||||||
|
|
||||||
# only show the result of the expression on the first page
|
|
||||||
if search.search_query.pageno > 1:
|
|
||||||
return results
|
|
||||||
|
|
||||||
query = search.search_query.query
|
|
||||||
# in order to avoid DoS attacks with long expressions, ignore long expressions
|
|
||||||
if len(query) > 100:
|
|
||||||
return results
|
|
||||||
|
|
||||||
# replace commonly used math operators with their proper Python operator
|
|
||||||
query = query.replace("x", "*").replace(":", "/")
|
|
||||||
|
|
||||||
# use UI language
|
|
||||||
ui_locale = babel.Locale.parse(request.preferences.get_value('locale'), sep='-')
|
|
||||||
|
|
||||||
# parse the number system in a localized way
|
|
||||||
def _decimal(match: re.Match) -> str:
|
|
||||||
val = match.string[match.start() : match.end()]
|
|
||||||
val = babel.numbers.parse_decimal(val, ui_locale, numbering_system="latn")
|
|
||||||
return str(val)
|
|
||||||
|
|
||||||
decimal = ui_locale.number_symbols["latn"]["decimal"]
|
|
||||||
group = ui_locale.number_symbols["latn"]["group"]
|
|
||||||
query = re.sub(f"[0-9]+[{decimal}|{group}][0-9]+[{decimal}|{group}]?[0-9]?", _decimal, query)
|
|
||||||
|
|
||||||
# only numbers and math operators are accepted
|
|
||||||
if any(str.isalpha(c) for c in query):
|
|
||||||
return results
|
|
||||||
|
|
||||||
# in python, powers are calculated via **
|
|
||||||
query_py_formatted = query.replace("^", "**")
|
|
||||||
|
|
||||||
# Prevent the runtime from being longer than 50 ms
|
|
||||||
res = timeout_func(0.05, _eval_expr, query_py_formatted)
|
|
||||||
if res is None or res == "":
|
|
||||||
return results
|
|
||||||
|
|
||||||
res = babel.numbers.format_decimal(res, locale=ui_locale)
|
|
||||||
results.add(results.types.Answer(answer=f"{search.search_query.query} = {res}"))
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
@ -14,6 +14,7 @@ from searx.result_types import EngineResults
|
|||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from searx.search import SearchWithPlugins
|
from searx.search import SearchWithPlugins
|
||||||
from searx.extended_types import SXNG_Request
|
from searx.extended_types import SXNG_Request
|
||||||
|
from searx.plugins import PluginCfg
|
||||||
|
|
||||||
|
|
||||||
class SXNGPlugin(Plugin):
|
class SXNGPlugin(Plugin):
|
||||||
@ -22,11 +23,10 @@ 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, plg_cfg: "PluginCfg") -> None:
|
||||||
super().__init__()
|
super().__init__(plg_cfg)
|
||||||
|
|
||||||
self.parser_re = re.compile(f"({'|'.join(self.keywords)}) (.*)", re.I)
|
self.parser_re = re.compile(f"({'|'.join(self.keywords)}) (.*)", re.I)
|
||||||
self.info = PluginInfo(
|
self.info = PluginInfo(
|
||||||
|
@ -1,19 +1,10 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches, unused-argument
|
||||||
"""
|
"""
|
||||||
.. attention::
|
|
||||||
|
|
||||||
The **"Hostname replace"** plugin has been replace by **"Hostnames
|
During the initialization phase, the plugin checks whether a ``hostnames:``
|
||||||
plugin"**, see :pull:`3463` & :pull:`3552`.
|
configuration exists. If this is not the case, the plugin is not included
|
||||||
|
in the PluginStorage (it is not available for selection).
|
||||||
The **Hostnames plugin** can be enabled by adding it to the
|
|
||||||
``enabled_plugins`` **list** in the ``setting.yml`` like so.
|
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
enabled_plugins:
|
|
||||||
- 'Hostnames plugin'
|
|
||||||
...
|
|
||||||
|
|
||||||
- ``hostnames.replace``: A **mapping** of regular expressions to hostnames to be
|
- ``hostnames.replace``: A **mapping** of regular expressions to hostnames to be
|
||||||
replaced by other hostnames.
|
replaced by other hostnames.
|
||||||
@ -92,6 +83,7 @@ something like this:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import typing
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from urllib.parse import urlunparse, urlparse
|
from urllib.parse import urlunparse, urlparse
|
||||||
@ -99,84 +91,114 @@ from urllib.parse import urlunparse, urlparse
|
|||||||
from flask_babel import gettext
|
from flask_babel import gettext
|
||||||
|
|
||||||
from searx import settings
|
from searx import settings
|
||||||
|
from searx.result_types._base import MainResult, LegacyResult
|
||||||
from searx.settings_loader import get_yaml_cfg
|
from searx.settings_loader import get_yaml_cfg
|
||||||
|
from searx.plugins import Plugin, PluginInfo
|
||||||
|
|
||||||
|
from ._core import log
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
import flask
|
||||||
|
from searx.search import SearchWithPlugins
|
||||||
|
from searx.extended_types import SXNG_Request
|
||||||
|
from searx.result_types import Result
|
||||||
|
from searx.plugins import PluginCfg
|
||||||
|
|
||||||
|
|
||||||
name = gettext('Hostnames plugin')
|
REPLACE: dict[re.Pattern, str] = {}
|
||||||
description = gettext('Rewrite hostnames, remove results or prioritize them based on the hostname')
|
REMOVE: set = set()
|
||||||
default_on = False
|
HIGH: set = set()
|
||||||
preference_section = 'general'
|
LOW: set = set()
|
||||||
|
|
||||||
plugin_id = 'hostnames'
|
|
||||||
|
|
||||||
parsed = 'parsed_url'
|
|
||||||
_url_fields = ['iframe_src', 'audio_src']
|
|
||||||
|
|
||||||
|
|
||||||
def _load_regular_expressions(settings_key) -> dict | set | None:
|
class SXNGPlugin(Plugin):
|
||||||
setting_value = settings.get(plugin_id, {}).get(settings_key)
|
"""Rewrite hostnames, remove results or prioritize them."""
|
||||||
|
|
||||||
if not setting_value:
|
id = "hostnames"
|
||||||
return None
|
|
||||||
|
|
||||||
# load external file with configuration
|
def __init__(self, plg_cfg: "PluginCfg") -> None:
|
||||||
if isinstance(setting_value, str):
|
super().__init__(plg_cfg)
|
||||||
setting_value = get_yaml_cfg(setting_value)
|
self.info = PluginInfo(
|
||||||
|
id=self.id,
|
||||||
|
name=gettext("Hostnames plugin"),
|
||||||
|
description=gettext("Rewrite hostnames, remove results or prioritize them based on the hostname"),
|
||||||
|
preference_section="general",
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(setting_value, list):
|
def on_result(self, request: "SXNG_Request", search: "SearchWithPlugins", result: Result) -> bool:
|
||||||
return {re.compile(r) for r in setting_value}
|
|
||||||
|
|
||||||
if isinstance(setting_value, dict):
|
for pattern in REMOVE:
|
||||||
return {re.compile(p): r for (p, r) in setting_value.items()}
|
if result.parsed_url and pattern.search(result.parsed_url.netloc):
|
||||||
|
# if the link (parsed_url) of the result match, then remove the
|
||||||
|
# result from the result list, in any other case, the result
|
||||||
|
# remains in the list / see final "return True" below.
|
||||||
|
# log.debug("FIXME: remove [url/parsed_url] %s %s", pattern.pattern, result.url)
|
||||||
|
return False
|
||||||
|
|
||||||
return None
|
result.filter_urls(filter_url_field)
|
||||||
|
|
||||||
|
if isinstance(result, (MainResult, LegacyResult)):
|
||||||
|
for pattern in LOW:
|
||||||
|
if result.parsed_url and pattern.search(result.parsed_url.netloc):
|
||||||
|
result.priority = "low"
|
||||||
|
|
||||||
replacements: dict = _load_regular_expressions('replace') or {} # type: ignore
|
for pattern in HIGH:
|
||||||
removables: set = _load_regular_expressions('remove') or set() # type: ignore
|
if result.parsed_url and pattern.search(result.parsed_url.netloc):
|
||||||
high_priority: set = _load_regular_expressions('high_priority') or set() # type: ignore
|
result.priority = "high"
|
||||||
low_priority: set = _load_regular_expressions('low_priority') or set() # type: ignore
|
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def _matches_parsed_url(result, pattern):
|
def init(self, app: "flask.Flask") -> bool: # pylint: disable=unused-argument
|
||||||
return result[parsed] and (parsed in result and pattern.search(result[parsed].netloc))
|
global REPLACE, REMOVE, HIGH, LOW # pylint: disable=global-statement
|
||||||
|
|
||||||
|
if not settings.get(self.id):
|
||||||
def on_result(_request, _search, result) -> bool:
|
# Remove plugin, if there isn't a "hostnames:" setting
|
||||||
for pattern, replacement in replacements.items():
|
|
||||||
if _matches_parsed_url(result, pattern):
|
|
||||||
# logger.debug(result['url'])
|
|
||||||
result[parsed] = result[parsed]._replace(netloc=pattern.sub(replacement, result[parsed].netloc))
|
|
||||||
result['url'] = urlunparse(result[parsed])
|
|
||||||
# logger.debug(result['url'])
|
|
||||||
|
|
||||||
for url_field in _url_fields:
|
|
||||||
if not getattr(result, url_field, None):
|
|
||||||
continue
|
|
||||||
|
|
||||||
url_src = urlparse(result[url_field])
|
|
||||||
if pattern.search(url_src.netloc):
|
|
||||||
url_src = url_src._replace(netloc=pattern.sub(replacement, url_src.netloc))
|
|
||||||
result[url_field] = urlunparse(url_src)
|
|
||||||
|
|
||||||
for pattern in removables:
|
|
||||||
if _matches_parsed_url(result, pattern):
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for url_field in _url_fields:
|
REPLACE = self._load_regular_expressions("replace") or {} # type: ignore
|
||||||
if not getattr(result, url_field, None):
|
REMOVE = self._load_regular_expressions("remove") or set() # type: ignore
|
||||||
continue
|
HIGH = self._load_regular_expressions("high_priority") or set() # type: ignore
|
||||||
|
LOW = self._load_regular_expressions("low_priority") or set() # type: ignore
|
||||||
|
|
||||||
url_src = urlparse(result[url_field])
|
return True
|
||||||
if pattern.search(url_src.netloc):
|
|
||||||
del result[url_field]
|
|
||||||
|
|
||||||
for pattern in low_priority:
|
def _load_regular_expressions(self, settings_key) -> dict[re.Pattern, str] | set | None:
|
||||||
if _matches_parsed_url(result, pattern):
|
setting_value = settings.get(self.id, {}).get(settings_key)
|
||||||
result['priority'] = 'low'
|
|
||||||
|
|
||||||
for pattern in high_priority:
|
if not setting_value:
|
||||||
if _matches_parsed_url(result, pattern):
|
return None
|
||||||
result['priority'] = 'high'
|
|
||||||
|
# load external file with configuration
|
||||||
|
if isinstance(setting_value, str):
|
||||||
|
setting_value = get_yaml_cfg(setting_value)
|
||||||
|
|
||||||
|
if isinstance(setting_value, list):
|
||||||
|
return {re.compile(r) for r in setting_value}
|
||||||
|
|
||||||
|
if isinstance(setting_value, dict):
|
||||||
|
return {re.compile(p): r for (p, r) in setting_value.items()}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def filter_url_field(result: "Result|LegacyResult", field_name: str, url_src: str) -> bool | str:
|
||||||
|
"""Returns bool ``True`` to use URL unchanged (``False`` to ignore URL).
|
||||||
|
If URL should be modified, the returned string is the new URL to use."""
|
||||||
|
|
||||||
|
if not url_src:
|
||||||
|
log.debug("missing a URL in field %s", field_name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
url_src_parsed = urlparse(url=url_src)
|
||||||
|
|
||||||
|
for pattern in REMOVE:
|
||||||
|
if pattern.search(url_src_parsed.netloc):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for pattern, replacement in REPLACE.items():
|
||||||
|
if pattern.search(url_src_parsed.netloc):
|
||||||
|
new_url = url_src_parsed._replace(netloc=pattern.sub(replacement, url_src_parsed.netloc))
|
||||||
|
new_url = urlunparse(new_url)
|
||||||
|
return new_url
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -1,54 +1,90 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
# pylint: disable=missing-module-docstring
|
# pylint: disable=missing-module-docstring
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import typing
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from urllib.parse import urlparse, parse_qsl
|
from urllib.parse import parse_qsl
|
||||||
|
|
||||||
from flask_babel import gettext
|
from flask_babel import gettext
|
||||||
|
from searx import get_setting
|
||||||
|
from searx.plugins import Plugin, PluginInfo
|
||||||
|
from searx.extended_types import sxng_request
|
||||||
|
|
||||||
from searx import settings
|
from ._core import log
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from searx.search import SearchWithPlugins
|
||||||
|
from searx.extended_types import SXNG_Request
|
||||||
|
from searx.result_types import Result, LegacyResult
|
||||||
|
from searx.plugins import PluginCfg
|
||||||
|
|
||||||
|
|
||||||
|
ahmia_blacklist: list = []
|
||||||
|
|
||||||
|
|
||||||
|
def filter_url_field(result: "Result|LegacyResult", field_name: str, url_src: str) -> bool | str:
|
||||||
|
"""Returns bool ``True`` to use URL unchanged (``False`` to ignore URL).
|
||||||
|
If URL should be modified, the returned string is the new URL to use."""
|
||||||
|
|
||||||
|
if field_name != "url":
|
||||||
|
return True # use it unchanged
|
||||||
|
|
||||||
|
doi = extract_doi(result.parsed_url)
|
||||||
|
if doi and len(doi) < 50:
|
||||||
|
for suffix in ("/", ".pdf", ".xml", "/full", "/meta", "/abstract"):
|
||||||
|
doi = doi.removesuffix(suffix)
|
||||||
|
new_url = get_doi_resolver() + doi
|
||||||
|
if "doi" not in result:
|
||||||
|
result["doi"] = doi
|
||||||
|
log.debug("oa_doi_rewrite: [URL field: %s] %s -> %s", field_name, url_src, new_url)
|
||||||
|
return new_url # use new url
|
||||||
|
|
||||||
|
return True # use it unchanged
|
||||||
|
|
||||||
|
|
||||||
|
class SXNGPlugin(Plugin):
|
||||||
|
"""Avoid paywalls by redirecting to open-access."""
|
||||||
|
|
||||||
|
id = "oa_doi_rewrite"
|
||||||
|
|
||||||
|
def __init__(self, plg_cfg: "PluginCfg") -> None:
|
||||||
|
super().__init__(plg_cfg)
|
||||||
|
self.info = PluginInfo(
|
||||||
|
id=self.id,
|
||||||
|
name=gettext("Open Access DOI rewrite"),
|
||||||
|
description=gettext("Avoid paywalls by redirecting to open-access versions of publications when available"),
|
||||||
|
preference_section="general",
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_result(
|
||||||
|
self,
|
||||||
|
request: "SXNG_Request",
|
||||||
|
search: "SearchWithPlugins",
|
||||||
|
result: "Result",
|
||||||
|
) -> bool: # pylint: disable=unused-argument
|
||||||
|
if result.parsed_url:
|
||||||
|
result.filter_urls(filter_url_field)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
regex = re.compile(r'10\.\d{4,9}/[^\s]+')
|
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'
|
|
||||||
|
|
||||||
|
|
||||||
def extract_doi(url):
|
def extract_doi(url):
|
||||||
match = regex.search(url.path)
|
m = regex.search(url.path)
|
||||||
if match:
|
if m:
|
||||||
return match.group(0)
|
return m.group(0)
|
||||||
for _, v in parse_qsl(url.query):
|
for _, v in parse_qsl(url.query):
|
||||||
match = regex.search(v)
|
m = regex.search(v)
|
||||||
if match:
|
if m:
|
||||||
return match.group(0)
|
return m.group(0)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_doi_resolver(preferences):
|
def get_doi_resolver() -> str:
|
||||||
doi_resolvers = settings['doi_resolvers']
|
doi_resolvers = get_setting("doi_resolvers")
|
||||||
selected_resolver = preferences.get_value('doi_resolver')[0]
|
selected_resolver = sxng_request.preferences.get_value('doi_resolver')[0]
|
||||||
if selected_resolver not in doi_resolvers:
|
if selected_resolver not in doi_resolvers:
|
||||||
selected_resolver = settings['default_doi_resolver']
|
selected_resolver = get_setting("default_doi_resolver")
|
||||||
return doi_resolvers[selected_resolver]
|
return doi_resolvers[selected_resolver]
|
||||||
|
|
||||||
|
|
||||||
def on_result(request, _search, result) -> bool:
|
|
||||||
|
|
||||||
if not result.parsed_url:
|
|
||||||
return True
|
|
||||||
|
|
||||||
doi = extract_doi(result['parsed_url'])
|
|
||||||
if doi and len(doi) < 50:
|
|
||||||
for suffix in ('/', '.pdf', '.xml', '/full', '/meta', '/abstract'):
|
|
||||||
if doi.endswith(suffix):
|
|
||||||
doi = doi[: -len(suffix)]
|
|
||||||
result['url'] = get_doi_resolver(request.preferences) + doi
|
|
||||||
result['parsed_url'] = urlparse(result['url'])
|
|
||||||
if 'doi' not in result:
|
|
||||||
result['doi'] = doi
|
|
||||||
return True
|
|
||||||
|
@ -14,6 +14,7 @@ from . import Plugin, PluginInfo
|
|||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from searx.search import SearchWithPlugins
|
from searx.search import SearchWithPlugins
|
||||||
from searx.extended_types import SXNG_Request
|
from searx.extended_types import SXNG_Request
|
||||||
|
from . import PluginCfg
|
||||||
|
|
||||||
|
|
||||||
class SXNGPlugin(Plugin):
|
class SXNGPlugin(Plugin):
|
||||||
@ -23,11 +24,10 @@ 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, plg_cfg: "PluginCfg"):
|
||||||
super().__init__()
|
super().__init__(plg_cfg)
|
||||||
|
|
||||||
self.ip_regex = re.compile(r"^ip", re.IGNORECASE)
|
self.ip_regex = re.compile(r"^ip", re.IGNORECASE)
|
||||||
self.ua_regex = re.compile(r"^user-agent", re.IGNORECASE)
|
self.ua_regex = re.compile(r"^user-agent", re.IGNORECASE)
|
||||||
|
@ -3,47 +3,24 @@
|
|||||||
user searches for ``tor-check``. It fetches the tor exit node list from
|
user searches for ``tor-check``. It fetches the tor exit node list from
|
||||||
:py:obj:`url_exit_list` and parses all the IPs into a list, then checks if the
|
:py:obj:`url_exit_list` and parses all the IPs into a list, then checks if the
|
||||||
user's IP address is in it.
|
user's IP address is in it.
|
||||||
|
|
||||||
Enable in ``settings.yml``:
|
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
enabled_plugins:
|
|
||||||
..
|
|
||||||
- 'Tor check plugin'
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import typing
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from flask_babel import gettext
|
from flask_babel import gettext
|
||||||
from httpx import HTTPError
|
from httpx import HTTPError
|
||||||
|
|
||||||
from searx.network import get
|
from searx.network import get
|
||||||
|
from searx.plugins import Plugin, PluginInfo
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
from searx.botdetection import get_real_ip
|
from searx.botdetection import get_real_ip
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from searx.search import SearchWithPlugins
|
||||||
|
from searx.extended_types import SXNG_Request
|
||||||
|
from searx.plugins import PluginCfg
|
||||||
|
|
||||||
default_on = False
|
|
||||||
|
|
||||||
name = gettext("Tor check plugin")
|
|
||||||
'''Translated name of the plugin'''
|
|
||||||
|
|
||||||
description = gettext(
|
|
||||||
"This plugin checks if the address of the request is a Tor exit-node, and"
|
|
||||||
" informs the user if it is; like check.torproject.org, but from SearXNG."
|
|
||||||
)
|
|
||||||
'''Translated description of the plugin.'''
|
|
||||||
|
|
||||||
preference_section = 'query'
|
|
||||||
'''The preference section where the plugin is shown.'''
|
|
||||||
|
|
||||||
query_keywords = ['tor-check']
|
|
||||||
'''Query keywords shown in the preferences.'''
|
|
||||||
|
|
||||||
query_examples = ''
|
|
||||||
'''Query examples shown in the preferences.'''
|
|
||||||
|
|
||||||
# Regex for exit node addresses in the list.
|
# Regex for exit node addresses in the list.
|
||||||
reg = re.compile(r"(?<=ExitAddress )\S+")
|
reg = re.compile(r"(?<=ExitAddress )\S+")
|
||||||
@ -52,33 +29,51 @@ url_exit_list = "https://check.torproject.org/exit-addresses"
|
|||||||
"""URL to load Tor exit list from."""
|
"""URL to load Tor exit list from."""
|
||||||
|
|
||||||
|
|
||||||
def post_search(request, search) -> EngineResults:
|
class SXNGPlugin(Plugin):
|
||||||
results = EngineResults()
|
"""Rewrite hostnames, remove results or prioritize them."""
|
||||||
|
|
||||||
if search.search_query.pageno > 1:
|
id = "tor_check"
|
||||||
return results
|
keywords = ["tor-check"]
|
||||||
|
|
||||||
if search.search_query.query.lower() == "tor-check":
|
def __init__(self, plg_cfg: "PluginCfg") -> None:
|
||||||
|
super().__init__(plg_cfg)
|
||||||
|
self.info = PluginInfo(
|
||||||
|
id=self.id,
|
||||||
|
name=gettext("Tor check plugin"),
|
||||||
|
description=gettext(
|
||||||
|
"This plugin checks if the address of the request is a Tor exit-node, and"
|
||||||
|
" informs the user if it is; like check.torproject.org, but from SearXNG."
|
||||||
|
),
|
||||||
|
preference_section="query",
|
||||||
|
)
|
||||||
|
|
||||||
# Request the list of tor exit nodes.
|
def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> EngineResults:
|
||||||
try:
|
results = EngineResults()
|
||||||
resp = get(url_exit_list)
|
|
||||||
node_list = re.findall(reg, resp.text) # type: ignore
|
|
||||||
|
|
||||||
except HTTPError:
|
if search.search_query.pageno > 1:
|
||||||
# No answer, return error
|
|
||||||
msg = gettext("Could not download the list of Tor exit-nodes from")
|
|
||||||
results.add(results.types.Answer(answer=f"{msg} {url_exit_list}"))
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
real_ip = get_real_ip(request)
|
if search.search_query.query.lower() == "tor-check":
|
||||||
|
|
||||||
if real_ip in node_list:
|
# Request the list of tor exit nodes.
|
||||||
msg = gettext("You are using Tor and it looks like you have the external IP address")
|
try:
|
||||||
results.add(results.types.Answer(answer=f"{msg} {real_ip}"))
|
resp = get(url_exit_list)
|
||||||
|
node_list = re.findall(reg, resp.text) # type: ignore
|
||||||
|
|
||||||
else:
|
except HTTPError:
|
||||||
msg = gettext("You are not using Tor and you have the external IP address")
|
# No answer, return error
|
||||||
results.add(results.types.Answer(answer=f"{msg} {real_ip}"))
|
msg = gettext("Could not download the list of Tor exit-nodes from")
|
||||||
|
results.add(results.types.Answer(answer=f"{msg} {url_exit_list}"))
|
||||||
|
return results
|
||||||
|
|
||||||
return results
|
real_ip = get_real_ip(request)
|
||||||
|
|
||||||
|
if real_ip in node_list:
|
||||||
|
msg = gettext("You are using Tor and it looks like you have the external IP address")
|
||||||
|
results.add(results.types.Answer(answer=f"{msg} {real_ip}"))
|
||||||
|
|
||||||
|
else:
|
||||||
|
msg = gettext("You are not using Tor and you have the external IP address")
|
||||||
|
results.add(results.types.Answer(answer=f"{msg} {real_ip}"))
|
||||||
|
|
||||||
|
return results
|
||||||
|
@ -2,12 +2,21 @@
|
|||||||
# pylint: disable=missing-module-docstring
|
# pylint: disable=missing-module-docstring
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import typing
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from urllib.parse import urlunparse, parse_qsl, urlencode
|
from urllib.parse import urlunparse, parse_qsl, urlencode
|
||||||
|
|
||||||
from flask_babel import gettext
|
from flask_babel import gettext
|
||||||
|
|
||||||
|
from searx.plugins import Plugin, PluginInfo
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from searx.search import SearchWithPlugins
|
||||||
|
from searx.extended_types import SXNG_Request
|
||||||
|
from searx.result_types import Result
|
||||||
|
from searx.plugins import PluginCfg
|
||||||
|
|
||||||
regexes = {
|
regexes = {
|
||||||
re.compile(r'utm_[^&]+'),
|
re.compile(r'utm_[^&]+'),
|
||||||
re.compile(r'(wkey|wemail)[^&]*'),
|
re.compile(r'(wkey|wemail)[^&]*'),
|
||||||
@ -15,30 +24,35 @@ regexes = {
|
|||||||
re.compile(r'&$'),
|
re.compile(r'&$'),
|
||||||
}
|
}
|
||||||
|
|
||||||
name = gettext('Tracker URL remover')
|
|
||||||
description = gettext('Remove trackers arguments from the returned URL')
|
|
||||||
default_on = True
|
|
||||||
preference_section = 'privacy'
|
|
||||||
|
|
||||||
|
class SXNGPlugin(Plugin):
|
||||||
|
"""Remove trackers arguments from the returned URL"""
|
||||||
|
|
||||||
def on_result(_request, _search, result) -> bool:
|
id = "tracker_url_remover"
|
||||||
|
|
||||||
|
def __init__(self, plg_cfg: "PluginCfg") -> None:
|
||||||
|
super().__init__(plg_cfg)
|
||||||
|
self.info = PluginInfo(
|
||||||
|
id=self.id,
|
||||||
|
name=gettext("Tracker URL remover"),
|
||||||
|
description=gettext("Remove trackers arguments from the returned URL"),
|
||||||
|
preference_section="privacy",
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_result(
|
||||||
|
self, request: "SXNG_Request", search: "SearchWithPlugins", result: Result
|
||||||
|
) -> bool: # pylint: disable=unused-argument
|
||||||
|
if not result.parsed_url:
|
||||||
|
return True
|
||||||
|
|
||||||
|
parsed_query: list[tuple[str, str]] = parse_qsl(result.parsed_url.query)
|
||||||
|
for name_value in list(parsed_query):
|
||||||
|
param_name = name_value[0]
|
||||||
|
for reg in regexes:
|
||||||
|
if reg.match(param_name):
|
||||||
|
parsed_query.remove(name_value)
|
||||||
|
result.parsed_url = result.parsed_url._replace(query=urlencode(parsed_query))
|
||||||
|
result.url = urlunparse(result.parsed_url)
|
||||||
|
break
|
||||||
|
|
||||||
parsed_url = getattr(result, "parsed_url", None)
|
|
||||||
if not parsed_url:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if parsed_url.query == "":
|
|
||||||
return True
|
|
||||||
|
|
||||||
parsed_query = parse_qsl(parsed_url.query)
|
|
||||||
changes = 0
|
|
||||||
for i, (param_name, _) in enumerate(list(parsed_query)):
|
|
||||||
for reg in regexes:
|
|
||||||
if reg.match(param_name):
|
|
||||||
parsed_query.pop(i - changes)
|
|
||||||
changes += 1
|
|
||||||
result.parsed_url = result.parsed_url._replace(query=urlencode(parsed_query))
|
|
||||||
result.url = urlunparse(result.parsed_url)
|
|
||||||
break
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
@ -7,36 +7,74 @@ converters, each converter is one item in the list (compare
|
|||||||
:py:obj:`ADDITIONAL_UNITS`). If the symbols are ambiguous, the matching units
|
:py:obj:`ADDITIONAL_UNITS`). If the symbols are ambiguous, the matching units
|
||||||
of measurement are evaluated. The weighting in the evaluation results from the
|
of measurement are evaluated. The weighting in the evaluation results from the
|
||||||
sorting of the :py:obj:`list of unit converters<symbol_to_si>`.
|
sorting of the :py:obj:`list of unit converters<symbol_to_si>`.
|
||||||
|
|
||||||
Enable in ``settings.yml``:
|
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
enabled_plugins:
|
|
||||||
..
|
|
||||||
- 'Unit converter plugin'
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import typing
|
||||||
import re
|
import re
|
||||||
import babel.numbers
|
import babel.numbers
|
||||||
|
|
||||||
from flask_babel import gettext, get_locale
|
from flask_babel import gettext, get_locale
|
||||||
|
|
||||||
from searx import data
|
from searx import data
|
||||||
|
from searx.plugins import Plugin, PluginInfo
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from searx.search import SearchWithPlugins
|
||||||
|
from searx.extended_types import SXNG_Request
|
||||||
|
from searx.plugins import PluginCfg
|
||||||
|
|
||||||
name = "Unit converter plugin"
|
|
||||||
description = gettext("Convert between units")
|
|
||||||
default_on = True
|
|
||||||
|
|
||||||
plugin_id = "unit_converter"
|
name = ""
|
||||||
preference_section = "general"
|
description = gettext("")
|
||||||
|
|
||||||
|
plugin_id = ""
|
||||||
|
preference_section = ""
|
||||||
|
|
||||||
CONVERT_KEYWORDS = ["in", "to", "as"]
|
CONVERT_KEYWORDS = ["in", "to", "as"]
|
||||||
|
|
||||||
|
|
||||||
|
class SXNGPlugin(Plugin):
|
||||||
|
"""Convert between units. The result is displayed in area for the
|
||||||
|
"answers".
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = "unit_converter"
|
||||||
|
|
||||||
|
def __init__(self, plg_cfg: "PluginCfg") -> None:
|
||||||
|
super().__init__(plg_cfg)
|
||||||
|
|
||||||
|
self.info = PluginInfo(
|
||||||
|
id=self.id,
|
||||||
|
name=gettext("Unit converter plugin"),
|
||||||
|
description=gettext("Convert between units"),
|
||||||
|
preference_section="general",
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> EngineResults:
|
||||||
|
results = EngineResults()
|
||||||
|
|
||||||
|
# only convert between units on the first page
|
||||||
|
if search.search_query.pageno > 1:
|
||||||
|
return results
|
||||||
|
|
||||||
|
query = search.search_query.query
|
||||||
|
query_parts = query.split(" ")
|
||||||
|
|
||||||
|
if len(query_parts) < 3:
|
||||||
|
return results
|
||||||
|
|
||||||
|
for query_part in query_parts:
|
||||||
|
for keyword in CONVERT_KEYWORDS:
|
||||||
|
if query_part == keyword:
|
||||||
|
from_query, to_query = query.split(keyword, 1)
|
||||||
|
target_val = _parse_text_and_convert(from_query.strip(), to_query.strip())
|
||||||
|
if target_val:
|
||||||
|
results.add(results.types.Answer(answer=target_val))
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
# inspired from https://stackoverflow.com/a/42475086
|
# inspired from https://stackoverflow.com/a/42475086
|
||||||
RE_MEASURE = r'''
|
RE_MEASURE = r'''
|
||||||
(?P<sign>[-+]?) # +/- or nothing for positive
|
(?P<sign>[-+]?) # +/- or nothing for positive
|
||||||
@ -243,27 +281,3 @@ def _parse_text_and_convert(from_query, to_query) -> str | None:
|
|||||||
result = babel.numbers.format_decimal(value, locale=_locale, format='#,##0.##########;-#')
|
result = babel.numbers.format_decimal(value, locale=_locale, format='#,##0.##########;-#')
|
||||||
|
|
||||||
return f'{result} {target_symbol}'
|
return f'{result} {target_symbol}'
|
||||||
|
|
||||||
|
|
||||||
def post_search(_request, search) -> EngineResults:
|
|
||||||
results = EngineResults()
|
|
||||||
|
|
||||||
# only convert between units on the first page
|
|
||||||
if search.search_query.pageno > 1:
|
|
||||||
return results
|
|
||||||
|
|
||||||
query = search.search_query.query
|
|
||||||
query_parts = query.split(" ")
|
|
||||||
|
|
||||||
if len(query_parts) < 3:
|
|
||||||
return results
|
|
||||||
|
|
||||||
for query_part in query_parts:
|
|
||||||
for keyword in CONVERT_KEYWORDS:
|
|
||||||
if query_part == keyword:
|
|
||||||
from_query, to_query = query.split(keyword, 1)
|
|
||||||
target_val = _parse_text_and_convert(from_query.strip(), to_query.strip())
|
|
||||||
if target_val:
|
|
||||||
results.add(results.types.Answer(answer=target_val))
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
@ -316,7 +316,7 @@ 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})
|
super().__init__(default_value, {plugin.id: plugin.active for plugin in plugins})
|
||||||
|
|
||||||
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]
|
||||||
|
@ -26,11 +26,14 @@ import urllib.parse
|
|||||||
import warnings
|
import warnings
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
import msgspec
|
import msgspec
|
||||||
|
|
||||||
from searx import logger as log
|
from searx import logger as log
|
||||||
|
|
||||||
WHITESPACE_REGEX = re.compile('( |\t|\n)+', re.M | re.U)
|
WHITESPACE_REGEX = re.compile('( |\t|\n)+', re.M | re.U)
|
||||||
|
UNKNOWN = object()
|
||||||
|
|
||||||
|
|
||||||
def _normalize_url_fields(result: Result | LegacyResult):
|
def _normalize_url_fields(result: Result | LegacyResult):
|
||||||
@ -50,8 +53,6 @@ def _normalize_url_fields(result: Result | LegacyResult):
|
|||||||
result.parsed_url = result.parsed_url._replace(
|
result.parsed_url = result.parsed_url._replace(
|
||||||
# if the result has no scheme, use http as default
|
# if the result has no scheme, use http as default
|
||||||
scheme=result.parsed_url.scheme or "http",
|
scheme=result.parsed_url.scheme or "http",
|
||||||
# normalize ``www.example.com`` to ``example.com``
|
|
||||||
# netloc=result.parsed_url.netloc.replace("www.", ""),
|
|
||||||
# normalize ``example.com/path/`` to ``example.com/path``
|
# normalize ``example.com/path/`` to ``example.com/path``
|
||||||
path=result.parsed_url.path.rstrip("/"),
|
path=result.parsed_url.path.rstrip("/"),
|
||||||
)
|
)
|
||||||
@ -107,6 +108,110 @@ def _normalize_text_fields(result: MainResult | LegacyResult):
|
|||||||
result.content = ""
|
result.content = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_urls(result: Result | LegacyResult, filter_func: Callable[[Result | LegacyResult, str, str], str | bool]):
|
||||||
|
# pylint: disable=too-many-branches, too-many-statements
|
||||||
|
|
||||||
|
# As soon we need LegacyResult not any longer, we can move this function to
|
||||||
|
# method Result.
|
||||||
|
|
||||||
|
url_fields = ["url", "iframe_src", "audio_src", "img_src", "thumbnail_src", "thumbnail"]
|
||||||
|
|
||||||
|
for field_name in url_fields:
|
||||||
|
url_src = getattr(result, field_name, "")
|
||||||
|
if not url_src:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_url = filter_func(result, field_name, url_src)
|
||||||
|
# log.debug("filter_urls: filter_func(result, %s) '%s' -> '%s'", field_name, field_value, new_url)
|
||||||
|
if isinstance(new_url, bool):
|
||||||
|
if new_url:
|
||||||
|
# log.debug("filter_urls: unchanged field %s URL %s", field_name, field_value)
|
||||||
|
continue
|
||||||
|
log.debug("filter_urls: drop field %s URL %s", field_name, url_src)
|
||||||
|
new_url = None
|
||||||
|
else:
|
||||||
|
log.debug("filter_urls: modify field %s URL %s -> %s", field_name, url_src, new_url)
|
||||||
|
|
||||||
|
setattr(result, field_name, new_url)
|
||||||
|
if field_name == "url":
|
||||||
|
# sync parsed_url with new_url
|
||||||
|
if not new_url:
|
||||||
|
result.parsed_url = None
|
||||||
|
elif isinstance(new_url, str):
|
||||||
|
result.parsed_url = urllib.parse.urlparse(new_url)
|
||||||
|
|
||||||
|
# "urls": are from infobox
|
||||||
|
#
|
||||||
|
# As soon we have InfoboxResult, we can move this function to method
|
||||||
|
# InfoboxResult.normalize_result_fields
|
||||||
|
|
||||||
|
infobox_urls: list[dict[str, str]] = getattr(result, "urls", [])
|
||||||
|
|
||||||
|
if infobox_urls:
|
||||||
|
# log.debug("filter_urls: infobox_urls .. %s", infobox_urls)
|
||||||
|
new_infobox_urls: list[dict[str, str]] = []
|
||||||
|
|
||||||
|
for item in infobox_urls:
|
||||||
|
url_src = item.get("url")
|
||||||
|
if not url_src:
|
||||||
|
new_infobox_urls.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_url = filter_func(result, "infobox_urls", url_src)
|
||||||
|
if isinstance(new_url, bool):
|
||||||
|
if new_url:
|
||||||
|
new_infobox_urls.append(item)
|
||||||
|
# log.debug("filter_urls: leave URL in field 'urls' ('infobox_urls') unchanged -> %s", _url)
|
||||||
|
continue
|
||||||
|
log.debug("filter_urls: remove URL from field 'urls' ('infobox_urls') URL %s", url_src)
|
||||||
|
new_url = None
|
||||||
|
if new_url:
|
||||||
|
log.debug("filter_urls: modify URL from field 'urls' ('infobox_urls') URL %s -> %s", url_src, new_url)
|
||||||
|
item["url"] = new_url
|
||||||
|
new_infobox_urls.append(item)
|
||||||
|
|
||||||
|
setattr(result, "urls", new_infobox_urls)
|
||||||
|
|
||||||
|
# "attributes": are from infobox
|
||||||
|
#
|
||||||
|
# The infobox has additional subsections for attributes, urls and relatedTopics:
|
||||||
|
|
||||||
|
infobox_attributes: list[dict[str, dict]] = getattr(result, "attributes", [])
|
||||||
|
|
||||||
|
if infobox_attributes:
|
||||||
|
# log.debug("filter_urls: infobox_attributes .. %s", infobox_attributes)
|
||||||
|
new_infobox_attributes: list[dict[str, dict]] = []
|
||||||
|
|
||||||
|
for item in infobox_attributes:
|
||||||
|
image = item.get("image", {})
|
||||||
|
url_src = image.get("src", "")
|
||||||
|
if not url_src:
|
||||||
|
new_infobox_attributes.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_url = filter_func(result, "infobox_attributes", url_src)
|
||||||
|
if isinstance(new_url, bool):
|
||||||
|
if new_url:
|
||||||
|
new_infobox_attributes.append(item)
|
||||||
|
# log.debug("filter_urls: leave URL in field 'image.src' unchanged -> %s", url_src)
|
||||||
|
continue
|
||||||
|
log.debug("filter_urls: drop field 'image.src' ('infobox_attributes') URL %s", url_src)
|
||||||
|
new_url = None
|
||||||
|
|
||||||
|
if new_url:
|
||||||
|
log.debug(
|
||||||
|
"filter_urls: modify 'image.src' ('infobox_attributes') URL %s -> %s",
|
||||||
|
url_src,
|
||||||
|
new_url,
|
||||||
|
)
|
||||||
|
item["image"]["src"] = new_url
|
||||||
|
new_infobox_attributes.append(item)
|
||||||
|
|
||||||
|
setattr(result, "attributes", new_infobox_attributes)
|
||||||
|
|
||||||
|
result.normalize_result_fields()
|
||||||
|
|
||||||
|
|
||||||
class Result(msgspec.Struct, kw_only=True):
|
class Result(msgspec.Struct, kw_only=True):
|
||||||
"""Base class of all result types :ref:`result types`."""
|
"""Base class of all result types :ref:`result types`."""
|
||||||
|
|
||||||
@ -142,9 +247,6 @@ class Result(msgspec.Struct, kw_only=True):
|
|||||||
with the resulting value in ``parse_url``, if ``url`` and
|
with the resulting value in ``parse_url``, if ``url`` and
|
||||||
``parse_url`` are not equal.
|
``parse_url`` are not equal.
|
||||||
|
|
||||||
- ``www.example.com`` and ``example.com`` are equivalent and are normalized
|
|
||||||
to ``example.com``.
|
|
||||||
|
|
||||||
- ``example.com/path/`` and ``example.com/path`` are equivalent and are
|
- ``example.com/path/`` and ``example.com/path`` are equivalent and are
|
||||||
normalized to ``example.com/path``.
|
normalized to ``example.com/path``.
|
||||||
"""
|
"""
|
||||||
@ -153,6 +255,33 @@ class Result(msgspec.Struct, kw_only=True):
|
|||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def filter_urls(self, filter_func: Callable[[Result | LegacyResult, str, str], str | bool]):
|
||||||
|
"""A filter function is passed in the ``filter_func`` argument to
|
||||||
|
filter and/or modify the URLs.
|
||||||
|
|
||||||
|
The filter function receives the :py:obj:`result object <Result>` as
|
||||||
|
the first argument and the field name (``str``) in the second argument.
|
||||||
|
In the third argument the URL string value is passed to the filter function.
|
||||||
|
|
||||||
|
The filter function is applied to all fields that contain a URL,
|
||||||
|
in addition to the familiar ``url`` field, these include fields such as::
|
||||||
|
|
||||||
|
["url", "iframe_src", "audio_src", "img_src", "thumbnail_src", "thumbnail"]
|
||||||
|
|
||||||
|
and the ``urls`` list of items of the infobox.
|
||||||
|
|
||||||
|
For each field, the filter function is called and returns a bool or a
|
||||||
|
string value:
|
||||||
|
|
||||||
|
- ``True``: leave URL in field unchanged
|
||||||
|
- ``False``: remove URL field from result (or remove entire result)
|
||||||
|
- ``str``: modified URL to be used instead
|
||||||
|
|
||||||
|
See :ref:`filter urls example`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_filter_urls(self, filter_func=filter_func)
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
"""Generates a hash value that uniquely identifies the content of *this*
|
"""Generates a hash value that uniquely identifies the content of *this*
|
||||||
result. The method can be adapted in the inheritance to compare results
|
result. The method can be adapted in the inheritance to compare results
|
||||||
@ -394,3 +523,7 @@ class LegacyResult(dict):
|
|||||||
for k, v in other.items():
|
for k, v in other.items():
|
||||||
if not self.get(k):
|
if not self.get(k):
|
||||||
self[k] = v
|
self[k] = v
|
||||||
|
|
||||||
|
def filter_urls(self, filter_func: Callable[[Result | LegacyResult, str, str], str | bool]):
|
||||||
|
"""See :py:obj:`Result.filter_urls`"""
|
||||||
|
_filter_urls(self, filter_func=filter_func)
|
||||||
|
@ -226,29 +226,38 @@ outgoing:
|
|||||||
# - 1.1.1.2
|
# - 1.1.1.2
|
||||||
# - fe80::/126
|
# - fe80::/126
|
||||||
|
|
||||||
# External plugin configuration, for more details see
|
# Plugin configuration, for more details see
|
||||||
# https://docs.searxng.org/admin/settings/settings_plugins.html
|
# https://docs.searxng.org/admin/settings/settings_plugins.html
|
||||||
#
|
#
|
||||||
# plugins:
|
plugins:
|
||||||
# - mypackage.mymodule.MyPlugin
|
|
||||||
# - mypackage.mymodule.MyOtherPlugin
|
searx.plugins.calculator.SXNGPlugin:
|
||||||
# - ...
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.hash_plugin.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.self_info.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.unit_converter.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.ahmia_filter.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.hostnames.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.oa_doi_rewrite.SXNGPlugin:
|
||||||
|
active: false
|
||||||
|
|
||||||
|
searx.plugins.tor_check.SXNGPlugin:
|
||||||
|
active: false
|
||||||
|
|
||||||
|
searx.plugins.tracker_url_remover.SXNGPlugin:
|
||||||
|
active: 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":
|
||||||
#
|
#
|
||||||
|
@ -235,8 +235,7 @@ SCHEMA = {
|
|||||||
'key': SettingsBytesValue((None, bytes), None),
|
'key': SettingsBytesValue((None, bytes), None),
|
||||||
'proxify_results': SettingsValue(bool, False),
|
'proxify_results': SettingsValue(bool, False),
|
||||||
},
|
},
|
||||||
'plugins': SettingsValue(list, []),
|
'plugins': SettingsValue(dict, {}),
|
||||||
'enabled_plugins': SettingsValue((None, list), None),
|
|
||||||
'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),
|
||||||
|
@ -361,7 +361,7 @@ def get_client_settings():
|
|||||||
'query_in_title': req_pref.get_value('query_in_title'),
|
'query_in_title': req_pref.get_value('query_in_title'),
|
||||||
'safesearch': str(req_pref.get_value('safesearch')),
|
'safesearch': str(req_pref.get_value('safesearch')),
|
||||||
'theme': req_pref.get_value('theme'),
|
'theme': req_pref.get_value('theme'),
|
||||||
'doi_resolver': get_doi_resolver(req_pref),
|
'doi_resolver': get_doi_resolver(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1003,7 +1003,7 @@ def preferences():
|
|||||||
shortcuts = {y: x for x, y in engine_shortcuts.items()},
|
shortcuts = {y: x for x, y in engine_shortcuts.items()},
|
||||||
themes = themes,
|
themes = themes,
|
||||||
plugins_storage = searx.plugins.STORAGE.info,
|
plugins_storage = searx.plugins.STORAGE.info,
|
||||||
current_doi_resolver = get_doi_resolver(sxng_request.preferences),
|
current_doi_resolver = get_doi_resolver(),
|
||||||
allowed_plugins = allowed_plugins,
|
allowed_plugins = allowed_plugins,
|
||||||
preferences_url_params = sxng_request.preferences.get_as_url_params(),
|
preferences_url_params = sxng_request.preferences.get_as_url_params(),
|
||||||
locked_preferences = get_setting("preferences.lock", []),
|
locked_preferences = get_setting("preferences.lock", []),
|
||||||
@ -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, 'enabled': _.active})
|
||||||
|
|
||||||
_limiter_cfg = limiter.get_cfg()
|
_limiter_cfg = limiter.get_cfg()
|
||||||
|
|
||||||
|
@ -32,6 +32,36 @@ ui:
|
|||||||
theme_args:
|
theme_args:
|
||||||
simple_style: auto
|
simple_style: auto
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
|
||||||
|
searx.plugins.calculator.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.hash_plugin.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.self_info.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.tracker_url_remover.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.unit_converter.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.ahmia_filter.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.hostnames.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.oa_doi_rewrite.SXNGPlugin:
|
||||||
|
active: false
|
||||||
|
|
||||||
|
searx.plugins.tor_check.SXNGPlugin:
|
||||||
|
active: false
|
||||||
|
|
||||||
|
|
||||||
engines:
|
engines:
|
||||||
- name: wikidata
|
- name: wikidata
|
||||||
engine: wikidata
|
engine: wikidata
|
||||||
|
@ -7,9 +7,7 @@ import searx.plugins
|
|||||||
import searx.preferences
|
import searx.preferences
|
||||||
|
|
||||||
from searx.extended_types import sxng_request
|
from searx.extended_types import sxng_request
|
||||||
from searx.plugins._core import _default, ModulePlugin
|
|
||||||
from searx.result_types import Answer
|
from searx.result_types import Answer
|
||||||
from searx.utils import load_module
|
|
||||||
|
|
||||||
from tests import SearxTestCase
|
from tests import SearxTestCase
|
||||||
from .test_utils import random_string
|
from .test_utils import random_string
|
||||||
@ -20,13 +18,10 @@ class PluginCalculator(SearxTestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
f = _default / "calculator.py"
|
|
||||||
mod = load_module(f.name, str(f.parent))
|
|
||||||
engines = {}
|
engines = {}
|
||||||
|
|
||||||
self.storage = searx.plugins.PluginStorage()
|
self.storage = searx.plugins.PluginStorage()
|
||||||
self.storage.register(ModulePlugin(mod, "searx.plugins.calculator"))
|
self.storage.load_settings({"searx.plugins.calculator.SXNGPlugin": {"active": True}})
|
||||||
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"})
|
||||||
|
@ -39,7 +39,7 @@ class PluginHashTest(SearxTestCase):
|
|||||||
engines = {}
|
engines = {}
|
||||||
|
|
||||||
self.storage = searx.plugins.PluginStorage()
|
self.storage = searx.plugins.PluginStorage()
|
||||||
self.storage.register_by_fqn("searx.plugins.hash_plugin.SXNGPlugin")
|
self.storage.load_settings({"searx.plugins.hash_plugin.SXNGPlugin": {"active": True}})
|
||||||
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"})
|
||||||
|
@ -21,12 +21,14 @@ class PluginIPSelfInfo(SearxTestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
engines = {}
|
||||||
|
|
||||||
self.storage = searx.plugins.PluginStorage()
|
self.storage = searx.plugins.PluginStorage()
|
||||||
self.storage.register_by_fqn("searx.plugins.self_info.SXNGPlugin")
|
self.storage.load_settings({"searx.plugins.self_info.SXNGPlugin": {"active": True}})
|
||||||
self.storage.init(self.app)
|
self.storage.init(self.app)
|
||||||
self.pref = searx.preferences.Preferences(["simple"], ["general"], {}, self.storage)
|
self.pref = searx.preferences.Preferences(["simple"], ["general"], engines, self.storage)
|
||||||
self.pref.parse_dict({"locale": "en"})
|
self.pref.parse_dict({"locale": "en"})
|
||||||
|
|
||||||
cfg = searx.limiter.get_cfg()
|
cfg = searx.limiter.get_cfg()
|
||||||
searx.botdetection.init(cfg, None)
|
searx.botdetection.init(cfg, None)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import babel
|
import babel
|
||||||
from mock import Mock
|
from mock import Mock
|
||||||
|
|
||||||
|
import searx
|
||||||
import searx.plugins
|
import searx.plugins
|
||||||
import searx.preferences
|
import searx.preferences
|
||||||
import searx.results
|
import searx.results
|
||||||
@ -14,7 +15,7 @@ from searx.extended_types import sxng_request
|
|||||||
from tests import SearxTestCase
|
from tests import SearxTestCase
|
||||||
|
|
||||||
plg_store = searx.plugins.PluginStorage()
|
plg_store = searx.plugins.PluginStorage()
|
||||||
plg_store.load_builtins()
|
plg_store.load_settings(searx.get_setting("plugins"))
|
||||||
|
|
||||||
|
|
||||||
def get_search_mock(query, **kwargs):
|
def get_search_mock(query, **kwargs):
|
||||||
@ -47,11 +48,11 @@ 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, active: bool):
|
||||||
|
plg_cfg = searx.plugins.PluginCfg(active=active)
|
||||||
self.id = _id
|
self.id = _id
|
||||||
self.default_on = default_on
|
|
||||||
self._name = name
|
self._name = name
|
||||||
super().__init__()
|
super().__init__(plg_cfg)
|
||||||
|
|
||||||
# pylint: disable= unused-argument
|
# pylint: disable= unused-argument
|
||||||
def pre_search(self, request, search) -> bool:
|
def pre_search(self, request, search) -> bool:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user