Support a centralized plugin blacklist (opt-in)
A centralized plugin blacklist read from plugins.octoprint.org on server start now allows us to stop plugins from being loaded that are known to cause severe issues with OctoPrint's regular operation. Blacklist entries can be restricted for specific plugin versions & OctoPrint versions, allowing for very granular control of any kind of blocking. Additionally users may disable blacklist processing in general (an opt-in wizard and a new section in the settings have been added) and at server start via the new --ignore-blacklist parameter available for "octoprint serve" and "octoprint daemon". If a plugin is blacklisted, OctoPrint will not even import the plugin module in question (if only a plugin key is specified OR a key and a version and the plugin's version is already known during import time, which is the case for plugins loaded from entry points) or at the very least stop the plugin from being enabled (if a plugin key and a version is specified and the plugin's version is only known after loading, which is the case for plugins loaded from directories).
This commit is contained in:
parent
3d1f3be4fc
commit
0afa854763
24 changed files with 588 additions and 127 deletions
|
|
@ -57,8 +57,9 @@ class FatalStartupError(BaseException):
|
|||
|
||||
def init_platform(basedir, configfile, use_logging_file=True, logging_file=None,
|
||||
logging_config=None, debug=False, verbosity=0, uncaught_logger=None,
|
||||
uncaught_handler=None, safe_mode=False, after_preinit_logging=None,
|
||||
uncaught_handler=None, safe_mode=False, ignore_blacklist=False, after_preinit_logging=None,
|
||||
after_settings=None, after_logging=None, after_safe_mode=None,
|
||||
after_event_manager=None, after_connectivity_checker=None,
|
||||
after_plugin_manager=None):
|
||||
kwargs = dict()
|
||||
|
||||
|
|
@ -94,13 +95,28 @@ def init_platform(basedir, configfile, use_logging_file=True, logging_file=None,
|
|||
if callable(after_safe_mode):
|
||||
after_safe_mode(**kwargs)
|
||||
|
||||
plugin_manager = init_pluginsystem(settings, safe_mode=safe_mode)
|
||||
event_manager = init_event_manager(settings)
|
||||
|
||||
kwargs["event_manager"] = event_manager
|
||||
if callable(after_event_manager):
|
||||
after_event_manager(**kwargs)
|
||||
|
||||
connectivity_checker = init_connectivity_checker(settings, event_manager)
|
||||
|
||||
kwargs["connectivity_checker"] = connectivity_checker
|
||||
if callable(after_connectivity_checker):
|
||||
after_connectivity_checker(**kwargs)
|
||||
|
||||
plugin_manager = init_pluginsystem(settings,
|
||||
safe_mode=safe_mode,
|
||||
ignore_blacklist=ignore_blacklist,
|
||||
connectivity_checker=connectivity_checker)
|
||||
kwargs["plugin_manager"] = plugin_manager
|
||||
|
||||
if callable(after_plugin_manager):
|
||||
after_plugin_manager(**kwargs)
|
||||
|
||||
return settings, logger, safe_mode, plugin_manager
|
||||
return settings, logger, safe_mode, event_manager, connectivity_checker, plugin_manager
|
||||
|
||||
|
||||
def init_settings(basedir, configfile):
|
||||
|
|
@ -281,17 +297,21 @@ def set_logging_config(config, debug, verbosity, uncaught_logger, uncaught_handl
|
|||
return logger
|
||||
|
||||
|
||||
def init_pluginsystem(settings, safe_mode=False):
|
||||
def init_pluginsystem(settings, safe_mode=False, ignore_blacklist=True, connectivity_checker=None):
|
||||
"""Initializes the plugin manager based on the settings."""
|
||||
|
||||
import os
|
||||
|
||||
logger = log.getLogger(__name__)
|
||||
logger = log.getLogger(__name__ + ".startup")
|
||||
|
||||
plugin_folders = [(os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "plugins")), True),
|
||||
settings.getBaseFolder("plugins")]
|
||||
plugin_entry_points = ["octoprint.plugin"]
|
||||
plugin_disabled_list = settings.get(["plugins", "_disabled"])
|
||||
|
||||
plugin_blacklist = []
|
||||
if not ignore_blacklist and settings.getBoolean(["server", "pluginBlacklist", "enabled"]):
|
||||
plugin_blacklist = get_plugin_blacklist(settings, connectivity_checker=connectivity_checker)
|
||||
|
||||
plugin_validators = []
|
||||
if safe_mode:
|
||||
|
|
@ -311,13 +331,14 @@ def init_pluginsystem(settings, safe_mode=False):
|
|||
plugin_folders=plugin_folders,
|
||||
plugin_entry_points=plugin_entry_points,
|
||||
plugin_disabled_list=plugin_disabled_list,
|
||||
plugin_blacklist=plugin_blacklist,
|
||||
plugin_validators=plugin_validators)
|
||||
|
||||
settings_overlays = dict()
|
||||
disabled_from_overlays = dict()
|
||||
|
||||
def handle_plugin_loaded(name, plugin):
|
||||
if hasattr(plugin.instance, "__plugin_settings_overlay__"):
|
||||
if plugin.instance and hasattr(plugin.instance, "__plugin_settings_overlay__"):
|
||||
plugin.needs_restart = True
|
||||
|
||||
# plugin has a settings overlay, inject it
|
||||
|
|
@ -356,7 +377,7 @@ def init_pluginsystem(settings, safe_mode=False):
|
|||
|
||||
if not addon in already_processed and not addon in disabled_list:
|
||||
disabled_list.append(addon)
|
||||
logger.info("Disabling plugin {} as defined by plugin {} through settings overlay".format(addon, name))
|
||||
logger.info("Disabling plugin {} as defined by plugin {}".format(addon, name))
|
||||
already_processed.append(name)
|
||||
|
||||
def handle_plugin_enabled(name, plugin):
|
||||
|
|
@ -370,6 +391,137 @@ def init_pluginsystem(settings, safe_mode=False):
|
|||
pm.reload_plugins(startup=True, initialize_implementations=False)
|
||||
return pm
|
||||
|
||||
|
||||
def get_plugin_blacklist(settings, connectivity_checker=None):
|
||||
import requests
|
||||
import os
|
||||
import time
|
||||
import yaml
|
||||
|
||||
from octoprint.util import bom_aware_open
|
||||
from octoprint.util.version import is_octoprint_compatible
|
||||
|
||||
logger = log.getLogger(__name__ + ".startup")
|
||||
|
||||
if connectivity_checker is not None and not connectivity_checker.online:
|
||||
logger.info("We don't appear to be online, not fetching plugin blacklist")
|
||||
return []
|
||||
|
||||
def format_blacklist(entries):
|
||||
format_entry = lambda x: "{} ({})".format(x[0], x[1]) if isinstance(x, (list, tuple)) and len(x) == 2 \
|
||||
else "{} (any)".format(x)
|
||||
return ", ".join(map(format_entry, entries))
|
||||
|
||||
def process_blacklist(entries):
|
||||
result = []
|
||||
|
||||
if not isinstance(entries, list):
|
||||
return result
|
||||
|
||||
for entry in entries:
|
||||
if not "plugin" in entry:
|
||||
continue
|
||||
|
||||
if "octoversions" in entry and not is_octoprint_compatible(*entry["octoversions"]):
|
||||
continue
|
||||
|
||||
if "version" in entry:
|
||||
logger.debug("Blacklisted plugin: {}, version: {}".format(entry["plugin"], entry["version"]))
|
||||
result.append((entry["plugin"], entry["version"]))
|
||||
elif "versions" in entry:
|
||||
logger.debug("Blacklisted plugin: {}, versions: {}".format(entry["plugin"], ", ".join(entry["versions"])))
|
||||
for version in entry["versions"]:
|
||||
result.append((entry["plugin"], version))
|
||||
else:
|
||||
logger.debug("Blacklisted plugin: {}".format(entry["plugin"]))
|
||||
result.append(entry["key"])
|
||||
|
||||
return result
|
||||
|
||||
def fetch_blacklist_from_cache(path, ttl):
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
|
||||
if os.stat(path).st_mtime + ttl < time.time():
|
||||
return None
|
||||
|
||||
with bom_aware_open(path, encoding="utf-8", mode="r") as f:
|
||||
result = yaml.safe_load(f)
|
||||
|
||||
if isinstance(result, list):
|
||||
return result
|
||||
|
||||
def fetch_blacklist_from_url(url, timeout=3, cache=None):
|
||||
result = []
|
||||
try:
|
||||
r = requests.get(url, timeout=timeout)
|
||||
result = process_blacklist(r.json())
|
||||
|
||||
if cache is not None:
|
||||
try:
|
||||
with bom_aware_open(cache, encoding="utf-8", mode="w") as f:
|
||||
yaml.safe_dump(result, f)
|
||||
except:
|
||||
logger.info("Fetched plugin blacklist but couldn't write it to its cache file.")
|
||||
except:
|
||||
logger.info("Unable to fetch plugin blacklist from {}, proceeding without it.".format(url))
|
||||
return result
|
||||
|
||||
try:
|
||||
# first attempt to fetch from cache
|
||||
cache_path = os.path.join(settings.getBaseFolder("data"), "plugin_blacklist.yaml")
|
||||
ttl = settings.getInt(["server", "pluginBlacklist", "ttl"])
|
||||
blacklist = fetch_blacklist_from_cache(cache_path, ttl)
|
||||
|
||||
if blacklist is None:
|
||||
# no result from the cache, let's fetch it fresh
|
||||
url = settings.get(["server", "pluginBlacklist", "url"])
|
||||
timeout = settings.getFloat(["server", "pluginBlacklist", "timeout"])
|
||||
blacklist = fetch_blacklist_from_url(url, timeout=timeout, cache=cache_path)
|
||||
|
||||
if blacklist is None:
|
||||
# still now result, so no blacklist
|
||||
blacklist = []
|
||||
|
||||
if blacklist:
|
||||
logger.info("Blacklist processing done, "
|
||||
"adding {} blacklisted plugin versions: {}".format(len(blacklist),
|
||||
format_blacklist(blacklist)))
|
||||
else:
|
||||
logger.info("Blacklist processing done")
|
||||
|
||||
return blacklist
|
||||
except:
|
||||
logger.exception("Something went wrong while processing the plugin blacklist. Proceeding without it.")
|
||||
|
||||
|
||||
def init_event_manager(settings):
|
||||
from octoprint.events import eventManager
|
||||
return eventManager()
|
||||
|
||||
|
||||
def init_connectivity_checker(settings, event_manager):
|
||||
from octoprint.events import Events
|
||||
from octoprint.util import ConnectivityChecker
|
||||
|
||||
# start regular check if we are connected to the internet
|
||||
connectivityEnabled = settings.getBoolean(["server", "onlineCheck", "enabled"])
|
||||
connectivityInterval = settings.getInt(["server", "onlineCheck", "interval"])
|
||||
connectivityHost = settings.get(["server", "onlineCheck", "host"])
|
||||
connectivityPort = settings.getInt(["server", "onlineCheck", "port"])
|
||||
|
||||
def on_connectivity_change(old_value, new_value):
|
||||
event_manager.fire(Events.CONNECTIVITY_CHANGED, payload=dict(old=old_value, new=new_value))
|
||||
|
||||
connectivityChecker = ConnectivityChecker(connectivityInterval,
|
||||
connectivityHost,
|
||||
port=connectivityPort,
|
||||
enabled=connectivityEnabled,
|
||||
on_change=on_connectivity_change)
|
||||
|
||||
return connectivityChecker
|
||||
|
||||
|
||||
#~~ server main method
|
||||
|
||||
def main():
|
||||
|
|
|
|||
|
|
@ -106,9 +106,9 @@ def standard_options(hidden=False):
|
|||
factory("--config", "-c", "configfile", type=click.Path(), callback=set_ctx_obj_option, is_eager=True, expose_value=False,
|
||||
help="Specify the config file to use."),
|
||||
factory("--verbose", "-v", "verbosity", count=True, callback=set_ctx_obj_option, is_eager=True, expose_value=False,
|
||||
help="Increase logging verbosity"),
|
||||
help="Increase logging verbosity."),
|
||||
factory("--safe", "safe_mode", is_flag=True, callback=set_ctx_obj_option, is_eager=True, expose_value=False,
|
||||
help="Enable safe mode; disables all third party plugins")
|
||||
help="Enable safe mode; disables all third party plugins.")
|
||||
]
|
||||
|
||||
return bulk_options(options)
|
||||
|
|
@ -123,6 +123,7 @@ legacy_options = bulk_options([
|
|||
hidden_option("--daemon", type=click.Choice(["start", "stop", "restart"]), callback=set_ctx_obj_option),
|
||||
hidden_option("--pid", type=click.Path(), default="/tmp/octoprint.pid", callback=set_ctx_obj_option),
|
||||
hidden_option("--iknowwhatimdoing", "allow_root", is_flag=True, callback=set_ctx_obj_option),
|
||||
hidden_option("--ignore-blacklist", "ignore_blacklist", is_flag=True, callback=set_ctx_obj_option)
|
||||
])
|
||||
"""Legacy options available directly on the "octoprint" command in earlier versions.
|
||||
Kept available for reasons of backwards compatibility, but hidden from the
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class OctoPrintPluginCommands(click.MultiCommand):
|
|||
if self._initialized:
|
||||
return
|
||||
|
||||
click.echo("Initializing settings & plugin subsystem...")
|
||||
if ctx.obj is None:
|
||||
ctx.obj = OctoPrintContext()
|
||||
|
||||
|
|
@ -51,7 +52,8 @@ class OctoPrintPluginCommands(click.MultiCommand):
|
|||
from octoprint import init_settings, init_pluginsystem, FatalStartupError
|
||||
try:
|
||||
self.settings = init_settings(get_ctx_obj_option(ctx, "basedir", None), get_ctx_obj_option(ctx, "configfile", None))
|
||||
self.plugin_manager = init_pluginsystem(self.settings, safe_mode=get_ctx_obj_option(ctx, "safe_mode", False))
|
||||
self.plugin_manager = init_pluginsystem(self.settings,
|
||||
safe_mode=get_ctx_obj_option(ctx, "safe_mode", False))
|
||||
except FatalStartupError as e:
|
||||
click.echo(e.message, err=True)
|
||||
click.echo("There was a fatal error initializing the settings or the plugin system.", err=True)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import sys
|
|||
|
||||
from octoprint.cli import bulk_options, standard_options, set_ctx_obj_option, get_ctx_obj_option
|
||||
|
||||
def run_server(basedir, configfile, host, port, debug, allow_root, logging_config, verbosity, safe_mode, octoprint_daemon=None):
|
||||
def run_server(basedir, configfile, host, port, debug, allow_root, logging_config, verbosity, safe_mode,
|
||||
ignore_blacklist, octoprint_daemon=None):
|
||||
"""Initializes the environment and starts up the server."""
|
||||
|
||||
from octoprint import init_platform, __display_version__, FatalStartupError
|
||||
|
|
@ -19,7 +20,7 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi
|
|||
def log_startup(recorder=None, safe_mode=None, **kwargs):
|
||||
from octoprint.logging import get_divider_line
|
||||
|
||||
logger = logging.getLogger("octoprint.server")
|
||||
logger = logging.getLogger("octoprint.startup")
|
||||
|
||||
logger.info(get_divider_line("*"))
|
||||
logger.info("Starting OctoPrint {}".format(__display_version__))
|
||||
|
|
@ -71,15 +72,17 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi
|
|||
OctoPrintLogHandler.registerRolloverCallback(rollover_callback)
|
||||
|
||||
try:
|
||||
settings, _, safe_mode, plugin_manager = init_platform(basedir,
|
||||
configfile,
|
||||
logging_file=logging_config,
|
||||
debug=debug,
|
||||
verbosity=verbosity,
|
||||
uncaught_logger=__name__,
|
||||
safe_mode=safe_mode,
|
||||
after_safe_mode=log_startup,
|
||||
after_plugin_manager=log_register_rollover)
|
||||
settings, _, safe_mode, event_manager, \
|
||||
connectivity_checker, plugin_manager = init_platform(basedir,
|
||||
configfile,
|
||||
logging_file=logging_config,
|
||||
debug=debug,
|
||||
verbosity=verbosity,
|
||||
uncaught_logger=__name__,
|
||||
safe_mode=safe_mode,
|
||||
ignore_blacklist=ignore_blacklist,
|
||||
after_safe_mode=log_startup,
|
||||
after_plugin_manager=log_register_rollover)
|
||||
except FatalStartupError as e:
|
||||
click.echo(e.message, err=True)
|
||||
click.echo("There was a fatal error starting up OctoPrint.", err=True)
|
||||
|
|
@ -87,6 +90,8 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi
|
|||
from octoprint.server import Server
|
||||
octoprint_server = Server(settings=settings,
|
||||
plugin_manager=plugin_manager,
|
||||
event_manager=event_manager,
|
||||
connectivity_checker=connectivity_checker,
|
||||
host=host,
|
||||
port=port,
|
||||
debug=debug,
|
||||
|
|
@ -107,7 +112,9 @@ server_options = bulk_options([
|
|||
click.option("--iknowwhatimdoing", "allow_root", is_flag=True, callback=set_ctx_obj_option,
|
||||
help="Allow OctoPrint to run as user root."),
|
||||
click.option("--debug", is_flag=True, callback=set_ctx_obj_option,
|
||||
help="Enable debug mode.")
|
||||
help="Enable debug mode."),
|
||||
click.option("--ignore-blacklist", "ignore_blacklist", is_flag=True, callback=set_ctx_obj_option,
|
||||
help="Disable processing of the plugin blacklist.")
|
||||
])
|
||||
"""Decorator to add the options shared among the server commands: ``--host``, ``--port``,
|
||||
``--logging``, ``--iknowwhatimdoing`` and ``--debug``."""
|
||||
|
|
@ -126,7 +133,7 @@ def server_commands():
|
|||
|
||||
|
||||
@server_commands.command(name="safemode")
|
||||
@standard_options(hidden=True)
|
||||
@standard_options()
|
||||
@click.pass_context
|
||||
def enable_safemode(ctx, **kwargs):
|
||||
"""Sets the safe mode flag for the next start."""
|
||||
|
|
@ -147,8 +154,8 @@ def enable_safemode(ctx, **kwargs):
|
|||
|
||||
|
||||
@server_commands.command(name="serve")
|
||||
@standard_options()
|
||||
@server_options
|
||||
@standard_options(hidden=True)
|
||||
@click.pass_context
|
||||
def serve_command(ctx, **kwargs):
|
||||
"""Starts the OctoPrint server."""
|
||||
|
|
@ -166,18 +173,20 @@ def serve_command(ctx, **kwargs):
|
|||
configfile = get_value("configfile")
|
||||
verbosity = get_value("verbosity")
|
||||
safe_mode = get_value("safe_mode")
|
||||
ignore_blacklist = get_value("ignore_blacklist")
|
||||
|
||||
run_server(basedir, configfile, host, port, debug,
|
||||
allow_root, logging, verbosity, safe_mode)
|
||||
allow_root, logging, verbosity, safe_mode,
|
||||
ignore_blacklist)
|
||||
|
||||
|
||||
if sys.platform != "win32" and sys.platform != "darwin":
|
||||
# we do not support daemon mode under windows or macosx
|
||||
|
||||
@server_commands.command(name="daemon")
|
||||
@standard_options()
|
||||
@server_options
|
||||
@daemon_options
|
||||
@standard_options(hidden=True)
|
||||
@click.argument("command", type=click.Choice(["start", "stop", "restart", "status"]),
|
||||
metavar="start|stop|restart|status")
|
||||
@click.pass_context
|
||||
|
|
@ -185,7 +194,7 @@ if sys.platform != "win32" and sys.platform != "darwin":
|
|||
"""
|
||||
Starts, stops or restarts in daemon mode.
|
||||
|
||||
Please note that daemon mode is only supported under Linux right now.
|
||||
Please note that daemon mode is not supported under Windows and MacOSX right now.
|
||||
"""
|
||||
|
||||
def get_value(key):
|
||||
|
|
@ -202,6 +211,7 @@ if sys.platform != "win32" and sys.platform != "darwin":
|
|||
configfile = get_value("configfile")
|
||||
verbosity = get_value("verbosity")
|
||||
safe_mode = get_value("safe_mode")
|
||||
ignore_blacklist = get_value("ignore_blacklist")
|
||||
|
||||
if pid is None:
|
||||
click.echo("No path to a pidfile set",
|
||||
|
|
@ -210,7 +220,8 @@ if sys.platform != "win32" and sys.platform != "darwin":
|
|||
|
||||
from octoprint.daemon import Daemon
|
||||
class OctoPrintDaemon(Daemon):
|
||||
def __init__(self, pidfile, basedir, configfile, host, port, debug, allow_root, logging_config, verbosity, safe_mode):
|
||||
def __init__(self, pidfile, basedir, configfile, host, port, debug, allow_root, logging_config, verbosity,
|
||||
safe_mode, ignore_blacklist):
|
||||
Daemon.__init__(self, pidfile)
|
||||
|
||||
self._basedir = basedir
|
||||
|
|
@ -222,14 +233,15 @@ if sys.platform != "win32" and sys.platform != "darwin":
|
|||
self._logging_config = logging_config
|
||||
self._verbosity = verbosity
|
||||
self._safe_mode = safe_mode
|
||||
self._ignore_blacklist = ignore_blacklist
|
||||
|
||||
def run(self):
|
||||
run_server(self._basedir, self._configfile, self._host, self._port, self._debug,
|
||||
self._allow_root, self._logging_config, self._verbosity, self._safe_mode,
|
||||
octoprint_daemon=self)
|
||||
self._ignore_blacklist, octoprint_daemon=self)
|
||||
|
||||
octoprint_daemon = OctoPrintDaemon(pid, basedir, configfile, host, port, debug, allow_root, logging, verbosity,
|
||||
safe_mode)
|
||||
safe_mode, ignore_blacklist)
|
||||
|
||||
if command == "start":
|
||||
octoprint_daemon.start()
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ def _validate_plugin(phase, plugin_info):
|
|||
return True
|
||||
|
||||
def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_entry_points=None, plugin_disabled_list=None,
|
||||
plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, plugin_validators=None):
|
||||
plugin_blacklist=None, plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None,
|
||||
plugin_validators=None):
|
||||
"""
|
||||
Factory method for initially constructing and consecutively retrieving the :class:`~octoprint.plugin.core.PluginManager`
|
||||
singleton.
|
||||
|
|
@ -65,6 +66,7 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
|
|||
this defaults to the entry point ``octoprint.plugin``.
|
||||
plugin_disabled_list (list): A list of plugin identifiers that are currently disabled. If not provided this
|
||||
defaults to all plugins for which ``enabled`` is set to ``False`` in the settings.
|
||||
plugin_blacklist (list): A list of plugin identifiers/identifier-version tuples that are currently blacklisted.
|
||||
plugin_restart_needing_hooks (list): A list of hook namespaces which cause a plugin to need a restart in order
|
||||
be enabled/disabled. Does not have to contain full hook identifiers, will be matched with startswith similar
|
||||
to logging handlers
|
||||
|
|
@ -120,6 +122,7 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
|
|||
plugin_entry_points,
|
||||
logging_prefix="octoprint.plugins.",
|
||||
plugin_disabled_list=plugin_disabled_list,
|
||||
plugin_blacklist=plugin_blacklist,
|
||||
plugin_restart_needing_hooks=plugin_restart_needing_hooks,
|
||||
plugin_obsolete_hooks=plugin_obsolete_hooks,
|
||||
plugin_validators=plugin_validators)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ import fnmatch
|
|||
import pkg_resources
|
||||
import pkginfo
|
||||
|
||||
from past.builtins import basestring
|
||||
|
||||
try:
|
||||
from os import scandir
|
||||
except ImportError:
|
||||
|
|
@ -57,7 +59,7 @@ class PluginInfo(object):
|
|||
Arguments:
|
||||
key (str): Identifier of the plugin
|
||||
location (str): Installation folder of the plugin
|
||||
instance (module): Plugin module instance
|
||||
instance (module): Plugin module instance - this may be ``None`` if the plugin has been blacklisted!
|
||||
name (str): Human readable name of the plugin
|
||||
version (str): Version of the plugin
|
||||
description (str): Description of the plugin
|
||||
|
|
@ -140,6 +142,7 @@ class PluginInfo(object):
|
|||
self.instance = instance
|
||||
self.origin = None
|
||||
self.enabled = True
|
||||
self.blacklisted = False
|
||||
self.bundled = False
|
||||
self.loaded = False
|
||||
self.managable = True
|
||||
|
|
@ -201,7 +204,7 @@ class PluginInfo(object):
|
|||
|
||||
def long_str(self, show_bundled=False, bundled_strs=(" [B]", ""),
|
||||
show_location=False, location_str=" - {location}",
|
||||
show_enabled=False, enabled_strs=("* ", " ")):
|
||||
show_enabled=False, enabled_strs=("* ", " ", "X ")):
|
||||
"""
|
||||
Long string representation of the plugin's information. Will return a string of the format ``<enabled><str(self)><bundled><location>``.
|
||||
|
||||
|
|
@ -209,8 +212,8 @@ class PluginInfo(object):
|
|||
The will be filled from ``enabled_str``, ``bundled_str`` and ``location_str`` as follows:
|
||||
|
||||
``enabled_str``
|
||||
a 2-tuple, the first entry being the string to insert when the plugin is enabled, the second
|
||||
entry the string to insert when it is not.
|
||||
a 3-tuple, the first entry being the string to insert when the plugin is enabled, the second
|
||||
entry the string to insert when it is not, the third entry the string when it is blacklisted.
|
||||
``bundled_str``
|
||||
a 2-tuple, the first entry being the string to insert when the plugin is bundled, the second
|
||||
entry the string to insert when it is not.
|
||||
|
|
@ -230,7 +233,7 @@ class PluginInfo(object):
|
|||
str: The long string representation of the plugin as described above
|
||||
"""
|
||||
if show_enabled:
|
||||
ret = enabled_strs[0] if self.enabled else enabled_strs[1]
|
||||
ret = enabled_strs[2] if self.blacklisted else (enabled_strs[0] if self.enabled else enabled_strs[1])
|
||||
else:
|
||||
ret = ""
|
||||
|
||||
|
|
@ -445,7 +448,7 @@ class PluginInfo(object):
|
|||
return self._get_instance_attribute(self.__class__.attr_disable, default=lambda: True)
|
||||
|
||||
def _get_instance_attribute(self, attr, default=None, defaults=None):
|
||||
if not hasattr(self.instance, attr):
|
||||
if self.instance is None or not hasattr(self.instance, attr):
|
||||
if defaults is not None:
|
||||
for value in defaults:
|
||||
if value is not None:
|
||||
|
|
@ -463,8 +466,8 @@ class PluginManager(object):
|
|||
"""
|
||||
|
||||
def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None,
|
||||
plugin_disabled_list=None, plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None,
|
||||
plugin_validators=None):
|
||||
plugin_disabled_list=None, plugin_blacklist=None, plugin_restart_needing_hooks=None,
|
||||
plugin_obsolete_hooks=None, plugin_validators=None):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
if logging_prefix is None:
|
||||
|
|
@ -477,11 +480,14 @@ class PluginManager(object):
|
|||
plugin_entry_points = []
|
||||
if plugin_disabled_list is None:
|
||||
plugin_disabled_list = []
|
||||
if plugin_blacklist is None:
|
||||
plugin_blacklist = []
|
||||
|
||||
self.plugin_folders = plugin_folders
|
||||
self.plugin_types = plugin_types
|
||||
self.plugin_entry_points = plugin_entry_points
|
||||
self.plugin_disabled_list = plugin_disabled_list
|
||||
self.plugin_blacklist = plugin_blacklist
|
||||
self.plugin_restart_needing_hooks = plugin_restart_needing_hooks
|
||||
self.plugin_obsolete_hooks = plugin_obsolete_hooks
|
||||
self.plugin_validators = plugin_validators
|
||||
|
|
@ -543,7 +549,7 @@ class PluginManager(object):
|
|||
if existing is None:
|
||||
existing = dict(self.plugins)
|
||||
|
||||
result = dict()
|
||||
result = OrderedDict()
|
||||
if self.plugin_folders:
|
||||
result.update(self._find_plugins_from_folders(self.plugin_folders, existing, ignored_uninstalled=ignore_uninstalled))
|
||||
if self.plugin_entry_points:
|
||||
|
|
@ -552,7 +558,7 @@ class PluginManager(object):
|
|||
return result
|
||||
|
||||
def _find_plugins_from_folders(self, folders, existing, ignored_uninstalled=True):
|
||||
result = dict()
|
||||
result = OrderedDict()
|
||||
|
||||
for folder in folders:
|
||||
flagged_readonly = False
|
||||
|
|
@ -596,7 +602,7 @@ class PluginManager(object):
|
|||
return result
|
||||
|
||||
def _find_plugins_from_entry_points(self, groups, existing, ignore_uninstalled=True):
|
||||
result = dict()
|
||||
result = OrderedDict()
|
||||
|
||||
# let's make sure we have a current working set ...
|
||||
working_set = pkg_resources.WorkingSet()
|
||||
|
|
@ -674,6 +680,12 @@ class PluginManager(object):
|
|||
self.logger.warn("Could not locate plugin {key}".format(key=key))
|
||||
return None
|
||||
|
||||
if self._is_plugin_blacklisted(key) or (version is not None and self._is_plugin_version_blacklisted(key, version)):
|
||||
plugin = PluginInfo(key, module[1], None, name=name, version=version, description=summary, author=author, url=url, license=license)
|
||||
plugin.blacklisted = True
|
||||
self.logger.warn("Plugin {} is blacklisted. Not importing it, only registering a dummy entry.".format(plugin))
|
||||
return plugin
|
||||
|
||||
plugin = self._import_plugin(key, *module, name=name, version=version, summary=summary, author=author, url=url, license=license)
|
||||
if plugin is None:
|
||||
return None
|
||||
|
|
@ -696,6 +708,19 @@ class PluginManager(object):
|
|||
def _is_plugin_disabled(self, key):
|
||||
return key in self.plugin_disabled_list or key.endswith('disabled')
|
||||
|
||||
def _is_plugin_blacklisted(self, key):
|
||||
return key in self.plugin_blacklist
|
||||
|
||||
def _is_plugin_version_blacklisted(self, key, version):
|
||||
def matches_plugin(entry):
|
||||
if isinstance(entry, (tuple, list)) and len(entry) == 2:
|
||||
entry_key, entry_version = entry
|
||||
return entry_key == key and entry_version == version
|
||||
return False
|
||||
|
||||
return any(map(lambda entry: matches_plugin(entry),
|
||||
self.plugin_blacklist))
|
||||
|
||||
def reload_plugins(self, startup=False, initialize_implementations=True, force_reload=None):
|
||||
self.logger.info("Loading plugins from {folders} and installed plugin packages...".format(
|
||||
folders=", ".join(map(lambda x: x[0] if isinstance(x, tuple) else str(x), self.plugin_folders))
|
||||
|
|
@ -710,7 +735,8 @@ class PluginManager(object):
|
|||
# 1st pass: loading the plugins
|
||||
for name, plugin in plugins.items():
|
||||
try:
|
||||
self.load_plugin(name, plugin, startup=startup, initialize_implementation=initialize_implementations)
|
||||
if not plugin.blacklisted:
|
||||
self.load_plugin(name, plugin, startup=startup, initialize_implementation=initialize_implementations)
|
||||
except PluginNeedsRestart:
|
||||
pass
|
||||
except PluginLifecycleException as e:
|
||||
|
|
@ -724,6 +750,9 @@ class PluginManager(object):
|
|||
for name, plugin in plugins.items():
|
||||
try:
|
||||
if plugin.loaded and not self._is_plugin_disabled(name):
|
||||
if plugin.blacklisted:
|
||||
self.logger.warn("Plugin {} is blacklisted. Not enabling it.".format(plugin))
|
||||
continue
|
||||
self.enable_plugin(name, plugin=plugin, initialize_implementation=initialize_implementations, startup=startup)
|
||||
except PluginNeedsRestart:
|
||||
pass
|
||||
|
|
@ -780,6 +809,11 @@ class PluginManager(object):
|
|||
|
||||
plugin.loaded = True
|
||||
|
||||
# we might only now have a version, so check again if we are blacklisted
|
||||
if not plugin.blacklisted and plugin.version and self._is_plugin_version_blacklisted(plugin.key,
|
||||
plugin.version):
|
||||
plugin.blacklisted = True
|
||||
|
||||
self.logger.debug("Loaded plugin {name}: {plugin}".format(**locals()))
|
||||
except PluginLifecycleException as e:
|
||||
raise e
|
||||
|
|
@ -1146,7 +1180,7 @@ class PluginManager(object):
|
|||
|
||||
|
||||
def log_all_plugins(self, show_bundled=True, bundled_str=(" (bundled)", ""), show_location=True,
|
||||
location_str=" = {location}", show_enabled=True, enabled_str=(" ", "!"),
|
||||
location_str=" = {location}", show_enabled=True, enabled_str=(" ", "!", "#"),
|
||||
only_to_handler=None):
|
||||
all_plugins = self.enabled_plugins.values() + self.disabled_plugins.values()
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ class CoreWizardPlugin(octoprint.plugin.AssetPlugin,
|
|||
def get_assets(self):
|
||||
if self.is_wizard_required():
|
||||
return dict(
|
||||
js=["js/corewizard.js"]
|
||||
js=["js/corewizard.js"],
|
||||
css=["css/corewizard.css"]
|
||||
)
|
||||
else:
|
||||
return dict()
|
||||
|
|
@ -91,7 +92,7 @@ class CoreWizardPlugin(octoprint.plugin.AssetPlugin,
|
|||
return result
|
||||
|
||||
def get_wizard_version(self):
|
||||
return 1
|
||||
return 2
|
||||
|
||||
#~~ ACL subwizard
|
||||
|
||||
|
|
@ -190,6 +191,23 @@ class CoreWizardPlugin(octoprint.plugin.AssetPlugin,
|
|||
def _get_onlinecheck_additional_wizard_template_data(self):
|
||||
return dict(mandatory=self._is_onlinecheck_wizard_required())
|
||||
|
||||
#~~ Plugin blacklist subwizard
|
||||
|
||||
def _is_pluginblacklist_wizard_firstrunonly(self):
|
||||
return False
|
||||
|
||||
def _is_pluginblacklist_wizard_required(self):
|
||||
return self._settings.global_get(["server", "pluginBlacklist", "enabled"]) is None
|
||||
|
||||
def _get_pluginblacklist_wizard_details(self):
|
||||
return dict(required=self._is_pluginblacklist_wizard_required())
|
||||
|
||||
def _get_pluginblacklist_wizard_name(self):
|
||||
return gettext("Plugin blacklist")
|
||||
|
||||
def _get_pluginblacklist_additional_wizard_template_data(self):
|
||||
return dict(mandatory=self._is_pluginblacklist_wizard_required())
|
||||
|
||||
#~~ Printer profile subwizard
|
||||
|
||||
def _is_printerprofile_wizard_firstrunonly(self):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
#wizard_plugin_corewizard_pluginblacklist p {
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
|
@ -207,6 +207,85 @@ $(function() {
|
|||
|
||||
}
|
||||
|
||||
function CoreWizardPluginBlacklistViewModel(parameters) {
|
||||
var self = this;
|
||||
|
||||
self.settingsViewModel = parameters[0];
|
||||
|
||||
self.setup = ko.observable(false);
|
||||
|
||||
self.decision = ko.observable();
|
||||
self.required = false;
|
||||
self.active = false;
|
||||
|
||||
self.enablePluginBlacklist = function() {
|
||||
self.settingsViewModel.server_pluginBlacklist_enabled(true);
|
||||
self.decision(true);
|
||||
self._sendData();
|
||||
};
|
||||
|
||||
self.disablePluginBlacklist = function() {
|
||||
self.settingsViewModel.server_pluginBlacklist_enabled(false);
|
||||
self.decision(false);
|
||||
self._sendData();
|
||||
};
|
||||
|
||||
self.onBeforeWizardTabChange = function(next, current) {
|
||||
if (!self.required) return true;
|
||||
|
||||
if (!current || !_.startsWith(current, "wizard_plugin_corewizard_pluginblacklist_") || self.setup()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self._showDecisionNeededDialog();
|
||||
return false;
|
||||
};
|
||||
|
||||
self.onBeforeWizardFinish = function() {
|
||||
if (!self.required) return true;
|
||||
|
||||
if (self.setup()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self._showDecisionNeededDialog();
|
||||
return false;
|
||||
};
|
||||
|
||||
self.onWizardPreventSettingsRefreshDialog = function() {
|
||||
return self.active;
|
||||
};
|
||||
|
||||
self.onWizardDetails = function(response) {
|
||||
self.required = response && response.corewizard && response.corewizard.details && response.corewizard.details.pluginblacklist && response.corewizard.details.pluginblacklist.required;
|
||||
};
|
||||
|
||||
self._showDecisionNeededDialog = function() {
|
||||
showMessageDialog({
|
||||
title: gettext("Please set up the plugin blacklist processing"),
|
||||
message: gettext("You haven't yet decided on whether to enable or disable the plugin blacklist processing. You need to either enable or disable it before continuing.")
|
||||
});
|
||||
};
|
||||
|
||||
self._sendData = function() {
|
||||
var data = {
|
||||
server: {
|
||||
pluginBlacklist: {
|
||||
enabled: self.settingsViewModel.server_pluginBlacklist_enabled()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.active = true;
|
||||
self.settingsViewModel.saveData(data)
|
||||
.done(function() {
|
||||
self.setup(true);
|
||||
self.active = false;
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function CoreWizardPrinterProfileViewModel(parameters) {
|
||||
var self = this;
|
||||
|
||||
|
|
@ -258,6 +337,10 @@ $(function() {
|
|||
CoreWizardOnlineCheckViewModel,
|
||||
["settingsViewModel"],
|
||||
"#wizard_plugin_corewizard_onlinecheck"
|
||||
], [
|
||||
CoreWizardPluginBlacklistViewModel,
|
||||
["settingsViewModel"],
|
||||
"#wizard_plugin_corewizard_pluginblacklist"
|
||||
], [
|
||||
CoreWizardPrinterProfileViewModel,
|
||||
["printerProfilesViewModel"],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<h3>{{ _('Configure plugin blacklist processing') }}</h3>
|
||||
|
||||
<p>{% trans %}
|
||||
To protect against known severe issues with certain versions of third party plugins, OctoPrint supports the use
|
||||
of a centralized plugin version blacklist to automatically disable such plugin versions before they can interfere with
|
||||
normal operation, allowing you to uninstall or update them to a newer version.
|
||||
{% endtrans %}</p>
|
||||
|
||||
<p>{% trans %}
|
||||
Please decide whether to allow fetch and use of this centralized blacklist starting with the next server start.
|
||||
You may also change your decision at any time through Settings > Server right from within OctoPrint.
|
||||
{% endtrans %}</p>
|
||||
|
||||
<div class="row-fluid">
|
||||
<a href="#" class="btn btn-danger span6" data-bind="click: function() { if(!setup()){disablePluginBlacklist()}}, enable: !setup(), css: {disabled: setup()}">{{ _('Disable Plugin Blacklist Processing') }}</a>
|
||||
<a href="#" class="btn btn-primary span6" data-bind="click: function() { if(!setup()){enablePluginBlacklist()}}, enable: !setup(), css: {disabled: setup()}">{{ _('Enable Plugin Blacklist Processing') }}</a>
|
||||
</div>
|
||||
|
||||
<div class="pluginblacklist_decision" style="display: none" data-bind="visible: setup()">
|
||||
<div class="text-center" style="display: none" data-bind="visible: decision()">{% trans %}
|
||||
Plugin blacklist processing is <strong class="text-success">enabled</strong>.
|
||||
{% endtrans %}</div>
|
||||
<div class="text-center" style="display: none" data-bind="visible: !decision()">{% trans %}
|
||||
Plugin blacklist processing is <strong class="text-danger">disabled</strong>.
|
||||
{% endtrans %}</div>
|
||||
</div>
|
||||
|
|
@ -14,6 +14,7 @@ from octoprint.settings import valid_boolean_trues
|
|||
from octoprint.server.util.flask import restricted_access, with_revalidation_checking, check_etag
|
||||
from octoprint.server import admin_permission, VERSION
|
||||
from octoprint.util.pip import LocalPipCaller, UnknownPip
|
||||
from octoprint.util.version import get_octoprint_version_string, get_octoprint_version, is_octoprint_compatible
|
||||
|
||||
from flask import jsonify, make_response
|
||||
from flask.ext.babel import gettext
|
||||
|
|
@ -31,6 +32,8 @@ import dateutil.parser
|
|||
import time
|
||||
import threading
|
||||
|
||||
_DATA_FORMAT_VERSION = "v2"
|
||||
|
||||
class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
||||
octoprint.plugin.TemplatePlugin,
|
||||
octoprint.plugin.AssetPlugin,
|
||||
|
|
@ -239,7 +242,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
plugins=self._repository_plugins
|
||||
),
|
||||
os=self._get_os(),
|
||||
octoprint=self._get_octoprint_version_string(),
|
||||
octoprint=get_octoprint_version_string(),
|
||||
pip=dict(
|
||||
available=self._pip_caller.available,
|
||||
version=self._pip_caller.version_string,
|
||||
|
|
@ -262,6 +265,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
hash.update(repr(self._notices))
|
||||
hash.update(repr(safe_mode))
|
||||
hash.update(repr(self._connectivity_checker.online))
|
||||
hash.update(repr(_DATA_FORMAT_VERSION))
|
||||
return hash.hexdigest()
|
||||
|
||||
def condition():
|
||||
|
|
@ -526,7 +530,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
|
||||
if not needs_restart:
|
||||
try:
|
||||
self._plugin_manager.disable_plugin(plugin.key, plugin=plugin)
|
||||
if plugin.enabled:
|
||||
self._plugin_manager.disable_plugin(plugin.key, plugin=plugin)
|
||||
except octoprint.plugin.core.PluginLifecycleException as e:
|
||||
self._logger.exception(u"Problem disabling plugin {name}".format(name=plugin.key))
|
||||
result = dict(result=False, uninstalled=True, disabled=False, unloaded=False, reason=e.reason)
|
||||
|
|
@ -534,7 +539,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
return jsonify(result)
|
||||
|
||||
try:
|
||||
self._plugin_manager.unload_plugin(plugin.key)
|
||||
if plugin.loaded:
|
||||
self._plugin_manager.unload_plugin(plugin.key)
|
||||
except octoprint.plugin.core.PluginLifecycleException as e:
|
||||
self._logger.exception(u"Problem unloading plugin {name}".format(name=plugin.key))
|
||||
result = dict(result=False, uninstalled=True, disabled=True, unloaded=False, reason=e.reason)
|
||||
|
|
@ -754,7 +760,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
return False
|
||||
|
||||
current_os = self._get_os()
|
||||
octoprint_version = self._get_octoprint_version(base=True)
|
||||
octoprint_version = get_octoprint_version(base=True)
|
||||
|
||||
def map_repository_entry(entry):
|
||||
result = copy.deepcopy(entry)
|
||||
|
|
@ -769,7 +775,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
|
||||
if "compatibility" in entry:
|
||||
if "octoprint" in entry["compatibility"] and entry["compatibility"]["octoprint"] is not None and isinstance(entry["compatibility"]["octoprint"], (list, tuple)) and len(entry["compatibility"]["octoprint"]):
|
||||
result["is_compatible"]["octoprint"] = self._is_octoprint_compatible(octoprint_version, entry["compatibility"]["octoprint"])
|
||||
result["is_compatible"]["octoprint"] = is_octoprint_compatible(*entry["compatibility"]["octoprint"],
|
||||
octoprint_version=octoprint_version)
|
||||
|
||||
if "os" in entry["compatibility"] and entry["compatibility"]["os"] is not None and isinstance(entry["compatibility"]["os"], (list, tuple)) and len(entry["compatibility"]["os"]):
|
||||
result["is_compatible"]["os"] = self._is_os_compatible(current_os, entry["compatibility"]["os"])
|
||||
|
|
@ -847,26 +854,6 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
self._notices = notices
|
||||
return True
|
||||
|
||||
def _is_octoprint_compatible(self, octoprint_version, compatibility_entries):
|
||||
"""
|
||||
Tests if the current ``octoprint_version`` is compatible to any of the provided ``compatibility_entries``.
|
||||
"""
|
||||
|
||||
for octo_compat in compatibility_entries:
|
||||
try:
|
||||
if not any(octo_compat.startswith(c) for c in ("<", "<=", "!=", "==", ">=", ">", "~=", "===")):
|
||||
octo_compat = ">={}".format(octo_compat)
|
||||
|
||||
s = next(pkg_resources.parse_requirements("OctoPrint" + octo_compat))
|
||||
if octoprint_version in s:
|
||||
break
|
||||
except:
|
||||
self._logger.exception("Something is wrong with this compatibility string for OctoPrint: {}".format(octo_compat))
|
||||
else:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _is_os_compatible(current_os, compatibility_entries):
|
||||
"""
|
||||
|
|
@ -899,38 +886,6 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
else:
|
||||
return "unmapped"
|
||||
|
||||
def _get_octoprint_version_string(self):
|
||||
return VERSION
|
||||
|
||||
def _get_octoprint_version(self, base=False):
|
||||
octoprint_version_string = self._get_octoprint_version_string()
|
||||
|
||||
if "-" in octoprint_version_string:
|
||||
octoprint_version_string = octoprint_version_string[:octoprint_version_string.find("-")]
|
||||
|
||||
octoprint_version = pkg_resources.parse_version(octoprint_version_string)
|
||||
|
||||
# A leading v is common in github release tags and old setuptools doesn't remove it. While OctoPrint's
|
||||
# versions should never contains such a prefix, we'll make sure to have stuff behave the same
|
||||
# regardless of setuptools version anyhow.
|
||||
if octoprint_version and isinstance(octoprint_version, tuple) and octoprint_version[0].lower() == "*v":
|
||||
octoprint_version = octoprint_version[1:]
|
||||
|
||||
if base:
|
||||
if isinstance(octoprint_version, tuple):
|
||||
# old setuptools
|
||||
base_version = []
|
||||
for part in octoprint_version:
|
||||
if part.startswith("*"):
|
||||
break
|
||||
base_version.append(part)
|
||||
base_version.append("*final")
|
||||
octoprint_version = tuple(base_version)
|
||||
else:
|
||||
# new setuptools
|
||||
octoprint_version = pkg_resources.parse_version(octoprint_version.base_version)
|
||||
return octoprint_version
|
||||
|
||||
@property
|
||||
def _reconnect_hooks(self):
|
||||
reconnect_hooks = self.__class__.RECONNECT_HOOKS
|
||||
|
|
@ -972,6 +927,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
bundled=plugin.bundled,
|
||||
managable=plugin.managable,
|
||||
enabled=plugin.enabled,
|
||||
blacklisted=plugin.blacklisted,
|
||||
safe_mode_victim=getattr(plugin, "safe_mode_victim", False),
|
||||
safe_mode_enabled=getattr(plugin, "safe_mode_enabled", False),
|
||||
pending_enable=(not plugin.enabled and not getattr(plugin, "safe_mode_enabled", False) and plugin.key in self._pending_enable),
|
||||
|
|
@ -990,13 +946,14 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
if key not in self._notices:
|
||||
return
|
||||
|
||||
octoprint_version = self._get_octoprint_version(base=True)
|
||||
octoprint_version = get_octoprint_version(base=True)
|
||||
plugin_notifications = self._notices.get(key, [])
|
||||
|
||||
def filter_relevant(notification):
|
||||
return "text" in notification and "date" in notification and \
|
||||
("versions" not in notification or plugin.version in notification["versions"]) and \
|
||||
("octoversions" not in notification or self._is_octoprint_compatible(octoprint_version, notification["octoversions"]))
|
||||
("octoversions" not in notification or is_octoprint_compatible(*notification["octoversions"],
|
||||
octoprint_version=octoprint_version))
|
||||
|
||||
def map_notification(notification):
|
||||
return self._to_external_notification(key, notification)
|
||||
|
|
|
|||
|
|
@ -222,7 +222,9 @@ $(function() {
|
|||
|
||||
self.enableToggle = function(data) {
|
||||
var command = self._getToggleCommand(data);
|
||||
return self.enableManagement() && (command == "disable" || !data.safe_mode_victim || data.safe_mode_enabled) && data.key != 'pluginmanager';
|
||||
var not_safemode_victim = !data.safe_mode_victim || data.safe_mode_enabled;
|
||||
var not_blacklisted = !data.blacklisted;
|
||||
return self.enableManagement() && (command == "disable" || (not_safemode_victim && not_blacklisted)) && data.key != 'pluginmanager';
|
||||
};
|
||||
|
||||
self.enableUninstall = function(data) {
|
||||
|
|
@ -699,7 +701,7 @@ $(function() {
|
|||
return self.isCompatible(data) ? (self.installed(data) ? gettext("Reinstall") : gettext("Install")) : (data.disabled ? gettext("Disabled") : gettext("Incompatible"));
|
||||
};
|
||||
|
||||
self._displayNotification = function(response, titleSuccess, textSuccess, textRestart, textReload, textReconnect, titleError, textError) {
|
||||
self._displayNotification = function(response, action, titleSuccess, textSuccess, textRestart, textReload, textReconnect, titleError, textError) {
|
||||
var notification;
|
||||
|
||||
var beforeClose = function(notification) {
|
||||
|
|
@ -707,7 +709,17 @@ $(function() {
|
|||
};
|
||||
|
||||
if (response.result) {
|
||||
if (response.needs_restart) {
|
||||
if (action == "install" && response.plugin && response.plugin.blacklisted) {
|
||||
notification = new PNotify({
|
||||
title: titleSuccess,
|
||||
text: textSuccess,
|
||||
type: "warning",
|
||||
callbacks: {
|
||||
before_close: beforeClose
|
||||
},
|
||||
hide: false
|
||||
})
|
||||
} else if (response.needs_restart) {
|
||||
var options = {
|
||||
title: titleSuccess,
|
||||
text: textRestart,
|
||||
|
|
@ -859,7 +871,9 @@ $(function() {
|
|||
self.toggleButtonTitle = function(data) {
|
||||
var command = self._getToggleCommand(data);
|
||||
if (command == "enable") {
|
||||
if (data.safe_mode_victim && !data.safe_mode_enabled) {
|
||||
if (data.blacklisted) {
|
||||
return gettext("Blacklisted");
|
||||
} else if (data.safe_mode_victim && !data.safe_mode_enabled) {
|
||||
return gettext("Disabled due to active safe mode");
|
||||
} else {
|
||||
return gettext("Enable Plugin");
|
||||
|
|
@ -1121,6 +1135,17 @@ $(function() {
|
|||
textRestart = textSuccess;
|
||||
textReload = textSuccess;
|
||||
textReconnect = textSuccess;
|
||||
} else if (data.plugin && data.plugin.blacklisted) {
|
||||
if (data.was_reinstalled) {
|
||||
titleSuccess = _.sprintf(gettext("Plugin \"%(name)s\" reinstalled"), {name: name});
|
||||
textSuccess = gettext("The plugin was reinstalled successfully, however it is blacklisted and therefore won't be loaded.");
|
||||
} else {
|
||||
titleSuccess = _.sprintf(gettext("Plugin \"%(name)s\" installed"), {name: name});
|
||||
textSuccess = gettext("The plugin was installed successfully, however it is blacklisted and therefore won't be loaded.");
|
||||
}
|
||||
textRestart = textSuccess;
|
||||
textReload = textSuccess;
|
||||
textReconnect = textSuccess;
|
||||
} else if (data.was_reinstalled) {
|
||||
titleSuccess = _.sprintf(gettext("Plugin \"%(name)s\" reinstalled"), {name: name});
|
||||
textSuccess = gettext("The plugin was reinstalled successfully");
|
||||
|
|
@ -1233,7 +1258,7 @@ $(function() {
|
|||
return;
|
||||
}
|
||||
|
||||
self._displayNotification(data, titleSuccess, textSuccess, textRestart, textReload, textReconnect, titleError, textError);
|
||||
self._displayNotification(data, action, titleSuccess, textSuccess, textRestart, textReload, textReconnect, titleError, textError);
|
||||
self.requestData();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
<tbody data-bind="foreach: plugins.paginatedItems">
|
||||
<tr>
|
||||
<td class="settings_plugin_plugin_manager_plugins_name">
|
||||
<div data-bind="css: {muted: !enabled}"><span data-bind="text: name"></span> <span data-bind="visible: version">(<span data-bind="text: version"></span>)</span> <i title="{{ _('Bundled with OctoPrint') }}" class="fa fa-th-large" data-bind="visible: bundled"></i> <i class="fa fa-lock" title="{{ _('Cannot be uninstalled through OctoPrint') }}" data-bind="visible: !managable"></i> <i title="{{ _('Restart of OctoPrint needed for changes to take effect') }}" class="fa fa-refresh" data-bind="visible: pending_enable || pending_disable || pending_install || pending_uninstall"></i> <i title="{{ _('Pending install') }}" class="fa fa-plus" data-bind="visible: pending_install"></i> <i title="{{ _('Pending uninstall') }}" class="fa fa-minus" data-bind="visible: pending_uninstall"></i> <i title="{{ _('Disabled due to safe mode') }}" class="fa fa-medkit" data-bind="visible: safe_mode_victim"></i> <i class="fa fa-exclamation-triangle" title="{{ _('There are notices available regarding this plugin') }}" data-bind="visible: notifications && notifications.length"></i></div>
|
||||
<div data-bind="css: {muted: !enabled}"><span data-bind="text: name"></span> <span data-bind="visible: version">(<span data-bind="text: version"></span>)</span> <i title="{{ _('Bundled with OctoPrint') }}" class="fa fa-th-large" data-bind="visible: bundled"></i> <i class="fa fa-lock" title="{{ _('Cannot be uninstalled through OctoPrint') }}" data-bind="visible: !managable"></i> <i title="{{ _('Restart of OctoPrint needed for changes to take effect') }}" class="fa fa-refresh" data-bind="visible: pending_enable || pending_disable || pending_install || pending_uninstall"></i> <i title="{{ _('Pending install') }}" class="fa fa-plus" data-bind="visible: pending_install"></i> <i title="{{ _('Pending uninstall') }}" class="fa fa-minus" data-bind="visible: pending_uninstall"></i> <i title="{{ _('Disabled due to safe mode') }}" class="fa fa-medkit" data-bind="visible: safe_mode_victim"></i> <i class="fa fa-exclamation-triangle" title="{{ _('There are notices available regarding this plugin') }}" data-bind="visible: notifications && notifications.length"></i> <i class="fa fa-ban" title="{{ _('This plugin is blacklisted') }}" data-bind="visible: blacklisted"></i></div>
|
||||
<div data-bind="visible: notifications && notifications.length"><a href="javascript:void(0)" class="text-error" style="text-decoration: underline" data-bind="click: function() { $root.showPluginNotifications($data) }, text: $root.showPluginNotificationsLinkText($data)"></a></div>
|
||||
<div><small class="muted" data-bind="text: description"> </small></div>
|
||||
<div data-bind="css: {muted: !enabled}">
|
||||
|
|
|
|||
|
|
@ -146,9 +146,12 @@ def load_user(id):
|
|||
|
||||
|
||||
class Server(object):
|
||||
def __init__(self, settings=None, plugin_manager=None, host="0.0.0.0", port=5000, debug=False, safe_mode=False, allow_root=False, octoprint_daemon=None):
|
||||
def __init__(self, settings=None, plugin_manager=None, connectivity_checker=None, event_manager=None,
|
||||
host="0.0.0.0", port=5000, debug=False, safe_mode=False, allow_root=False, octoprint_daemon=None):
|
||||
self._settings = settings
|
||||
self._plugin_manager = plugin_manager
|
||||
self._connectivity_checker = connectivity_checker
|
||||
self._event_manager = event_manager
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._debug = debug
|
||||
|
|
@ -233,7 +236,7 @@ class Server(object):
|
|||
pluginManager.reload_plugins(startup=True, initialize_implementations=False)
|
||||
|
||||
printerProfileManager = PrinterProfileManager()
|
||||
eventManager = events.eventManager()
|
||||
eventManager = self._event_manager
|
||||
analysisQueue = octoprint.filemanager.analysis.AnalysisQueue()
|
||||
slicingManager = octoprint.slicing.SlicingManager(self._settings.getBaseFolder("slicingProfiles"), printerProfileManager)
|
||||
|
||||
|
|
@ -245,20 +248,7 @@ class Server(object):
|
|||
pluginLifecycleManager = LifecycleManager(pluginManager)
|
||||
preemptiveCache = PreemptiveCache(os.path.join(self._settings.getBaseFolder("data"), "preemptive_cache_config.yaml"))
|
||||
|
||||
# start regular check if we are connected to the internet
|
||||
connectivityEnabled = self._settings.getBoolean(["server", "onlineCheck", "enabled"])
|
||||
connectivityInterval = self._settings.getInt(["server", "onlineCheck", "interval"])
|
||||
connectivityHost = self._settings.get(["server", "onlineCheck", "host"])
|
||||
connectivityPort = self._settings.getInt(["server", "onlineCheck", "port"])
|
||||
|
||||
def on_connectivity_change(old_value, new_value):
|
||||
eventManager.fire(events.Events.CONNECTIVITY_CHANGED, payload=dict(old=old_value, new=new_value))
|
||||
|
||||
connectivityChecker = octoprint.util.ConnectivityChecker(connectivityInterval,
|
||||
connectivityHost,
|
||||
port=connectivityPort,
|
||||
enabled=connectivityEnabled,
|
||||
on_change=on_connectivity_change)
|
||||
connectivityChecker = self._connectivity_checker
|
||||
|
||||
def on_settings_update(*args, **kwargs):
|
||||
# make sure our connectivity checker runs with the latest settings
|
||||
|
|
|
|||
|
|
@ -208,6 +208,11 @@ def getSettings():
|
|||
"interval": int(s.getInt(["server", "onlineCheck", "interval"]) / 60),
|
||||
"host": s.get(["server", "onlineCheck", "host"]),
|
||||
"port": s.getInt(["server", "onlineCheck", "port"])
|
||||
},
|
||||
"pluginBlacklist": {
|
||||
"enabled": s.getBoolean(["server", "pluginBlacklist", "enabled"]),
|
||||
"url": s.get(["server", "pluginBlacklist", "url"]),
|
||||
"ttl": int(s.getInt(["server", "pluginBlacklist", "ttl"]) / 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -435,6 +440,15 @@ def _saveSettings(data):
|
|||
pass
|
||||
if "host" in data["server"]["onlineCheck"]: s.set(["server", "onlineCheck", "host"], data["server"]["onlineCheck"]["host"])
|
||||
if "port" in data["server"]["onlineCheck"]: s.setInt(["server", "onlineCheck", "port"], data["server"]["onlineCheck"]["port"])
|
||||
if "pluginBlacklist" in data["server"]:
|
||||
if "enabled" in data["server"]["pluginBlacklist"]: s.setBoolean(["server", "pluginBlacklist", "enabled"], data["server"]["pluginBlacklist"]["enabled"])
|
||||
if "url" in data["server"]["pluginBlacklist"]: s.set(["server", "pluginBlacklist", "url"], data["server"]["pluginBlacklist"]["url"])
|
||||
if "ttl" in data["server"]["pluginBlacklist"]:
|
||||
try:
|
||||
ttl = int(data["server"]["pluginBlacklist"]["ttl"])
|
||||
s.setInt(["server", "pluginBlacklist", "ttl"], ttl * 60)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if "plugins" in data:
|
||||
for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin):
|
||||
|
|
|
|||
|
|
@ -154,6 +154,11 @@ default_settings = {
|
|||
"host": "8.8.8.8",
|
||||
"port": 53
|
||||
},
|
||||
"pluginBlacklist": {
|
||||
"enabled": None,
|
||||
"url": "http://plugins.octoprint.org/blacklist.json",
|
||||
"ttl": 15 * 60 # 15 min
|
||||
},
|
||||
"diskspace": {
|
||||
"warning": 500 * 1024 * 1024, # 500 MB
|
||||
"critical": 200 * 1024 * 1024, # 200 MB
|
||||
|
|
|
|||
|
|
@ -218,6 +218,10 @@ $(function() {
|
|||
self.server_onlineCheck_host = ko.observable();
|
||||
self.server_onlineCheck_port = ko.observable();
|
||||
|
||||
self.server_pluginBlacklist_enabled = ko.observable();
|
||||
self.server_pluginBlacklist_url = ko.observable();
|
||||
self.server_pluginBlacklist_ttl = ko.observable();
|
||||
|
||||
self.settings = undefined;
|
||||
self.lastReceivedSettings = undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,4 +10,8 @@
|
|||
{% include "snippets/settings/server/serverOnlineCheckDescription.jinja2" %}
|
||||
{% include "snippets/settings/server/serverOnlineCheck.jinja2" %}
|
||||
|
||||
<h3>{{ _('Plugin blacklist processing') }}</h3>
|
||||
|
||||
{% include "snippets/settings/server/serverPluginBlacklistDescription.jinja2" %}
|
||||
{% include "snippets/settings/server/serverPluginBlacklist.jinja2" %}
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
{% include "snippets/settings/server/serverPluginBlacklistEnabled.jinja2" %}
|
||||
|
||||
<div data-bind="visible: server_pluginBlacklist_enabled">
|
||||
<div class="advanced_options">
|
||||
<div><small><a href="#" class="muted" data-bind="toggleContent: { class: 'fa-caret-right fa-caret-down', parent: '.advanced_options', container: '.hide' }"><i class="fa fa-caret-right"></i> {{ _('Advanced options') }}</a></small></div>
|
||||
<div class="hide">
|
||||
{% include "snippets/settings/server/serverPluginBlacklistUrl.jinja2" %}
|
||||
{% include "snippets/settings/server/serverPluginBlacklistTtl.jinja2" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<p>{% trans %}
|
||||
<strong>To protect against known severe issues with certain versions of third party plugins</strong>, OctoPrint supports
|
||||
the use of a centralized plugin version blacklist to automatically disable such plugins before they can interfere
|
||||
with normal operation.
|
||||
{% endtrans %}</p>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: server_pluginBlacklist_enabled" id="settings-serverPluginBlacklistEnabled"> {{ _('Enable plugin blacklist processing on startup') }}
|
||||
<span class="help-block">{{ _('Any changes take effect only on the next server start.') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<div class="control-group" title="{{ _('How long to cache the blacklist, in minutes. You should normally not have to change this.') }}">
|
||||
<label class="control-label">{{ _('Blacklist cache TTL') }}</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" class="input-mini" data-bind="value: server_pluginBlacklist_ttl">
|
||||
<span class="add-on">min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<div class="control-group" title="{{ _('Plugin blacklist URL. You should normally not have to change this.') }}">
|
||||
<label class="control-label" for="settings-serverPluginBlacklistUrl">{{ _('Blacklist URL') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: server_pluginBlacklist_url" id="settings-serverPluginBlacklistUrl">
|
||||
</div>
|
||||
</div>
|
||||
88
src/octoprint/util/version.py
Normal file
88
src/octoprint/util/version.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
This module provides a bunch of utility methods and helpers for version handling.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import pkg_resources
|
||||
import logging
|
||||
|
||||
from octoprint._version import get_versions
|
||||
|
||||
|
||||
_VERSION = get_versions()["version"]
|
||||
|
||||
|
||||
def get_octoprint_version_string():
|
||||
return _VERSION
|
||||
|
||||
|
||||
def get_octoprint_version(base=False):
|
||||
octoprint_version_string = get_octoprint_version_string()
|
||||
|
||||
if "-" in octoprint_version_string:
|
||||
octoprint_version_string = octoprint_version_string[:octoprint_version_string.find("-")]
|
||||
|
||||
octoprint_version = pkg_resources.parse_version(octoprint_version_string)
|
||||
|
||||
# A leading v is common in github release tags and old setuptools doesn't remove it. While OctoPrint's
|
||||
# versions should never contain such a prefix, we'll make sure to have stuff behave the same
|
||||
# regardless of setuptools version anyhow.
|
||||
if octoprint_version and isinstance(octoprint_version, tuple) and octoprint_version[0].lower() == "*v":
|
||||
octoprint_version = octoprint_version[1:]
|
||||
|
||||
if base:
|
||||
if isinstance(octoprint_version, tuple):
|
||||
# old setuptools
|
||||
base_version = []
|
||||
for part in octoprint_version:
|
||||
if part.startswith("*"):
|
||||
break
|
||||
base_version.append(part)
|
||||
base_version.append("*final")
|
||||
octoprint_version = tuple(base_version)
|
||||
else:
|
||||
# new setuptools
|
||||
octoprint_version = pkg_resources.parse_version(octoprint_version.base_version)
|
||||
return octoprint_version
|
||||
|
||||
|
||||
def is_octoprint_compatible(*compatibility_entries, **kwargs):
|
||||
"""
|
||||
Tests if the current ``octoprint_version`` is compatible to any of the provided ``compatibility_entries``.
|
||||
|
||||
Arguments:
|
||||
compatibility_entries (str): compatibility string(s) to test against, result will be `True` if any match
|
||||
is found
|
||||
octoprint_version (tuple or SetuptoolsVersion): optional OctoPrint version to match against, if not current
|
||||
base version will be determined via :func:`get_octoprint_version`.
|
||||
|
||||
Returns:
|
||||
(bool) ``True`` if any of the provided compatibility entries matches or there are no entries, else ``False``
|
||||
"""
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if not compatibility_entries:
|
||||
return True
|
||||
|
||||
octoprint_version = kwargs.get("octoprint_version")
|
||||
if octoprint_version is None:
|
||||
octoprint_version = get_octoprint_version(base=True)
|
||||
|
||||
for octo_compat in compatibility_entries:
|
||||
try:
|
||||
if not any(octo_compat.startswith(c) for c in ("<", "<=", "!=", "==", ">=", ">", "~=", "===")):
|
||||
octo_compat = ">={}".format(octo_compat)
|
||||
|
||||
s = next(pkg_resources.parse_requirements("OctoPrint" + octo_compat))
|
||||
if octoprint_version in s:
|
||||
break
|
||||
except:
|
||||
logger.exception("Something is wrong with this compatibility string for OctoPrint: {}".format(octo_compat))
|
||||
else:
|
||||
return False
|
||||
|
||||
return True
|
||||
Loading…
Reference in a new issue