diff --git a/src/octoprint/__init__.py b/src/octoprint/__init__.py index 785a5e5c..e2d32cfc 100644 --- a/src/octoprint/__init__.py +++ b/src/octoprint/__init__.py @@ -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(): diff --git a/src/octoprint/cli/__init__.py b/src/octoprint/cli/__init__.py index 9562b173..34ad3f30 100644 --- a/src/octoprint/cli/__init__.py +++ b/src/octoprint/cli/__init__.py @@ -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 diff --git a/src/octoprint/cli/plugins.py b/src/octoprint/cli/plugins.py index e650129b..facfc80c 100644 --- a/src/octoprint/cli/plugins.py +++ b/src/octoprint/cli/plugins.py @@ -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) diff --git a/src/octoprint/cli/server.py b/src/octoprint/cli/server.py index 5789af64..3adea9f5 100644 --- a/src/octoprint/cli/server.py +++ b/src/octoprint/cli/server.py @@ -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() diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 2f77e2c4..114d2fe6 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -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) diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 76d092e7..d8a58e9e 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -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 ````. @@ -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() diff --git a/src/octoprint/plugins/corewizard/__init__.py b/src/octoprint/plugins/corewizard/__init__.py index bfeb93e7..7beb50a8 100644 --- a/src/octoprint/plugins/corewizard/__init__.py +++ b/src/octoprint/plugins/corewizard/__init__.py @@ -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): diff --git a/src/octoprint/plugins/corewizard/static/css/corewizard.css b/src/octoprint/plugins/corewizard/static/css/corewizard.css new file mode 100644 index 00000000..2f15349d --- /dev/null +++ b/src/octoprint/plugins/corewizard/static/css/corewizard.css @@ -0,0 +1,4 @@ +#wizard_plugin_corewizard_pluginblacklist p { + line-height: 1.5; + margin-bottom: 1.5em; +} diff --git a/src/octoprint/plugins/corewizard/static/js/corewizard.js b/src/octoprint/plugins/corewizard/static/js/corewizard.js index 2a09a07b..b96d92e2 100644 --- a/src/octoprint/plugins/corewizard/static/js/corewizard.js +++ b/src/octoprint/plugins/corewizard/static/js/corewizard.js @@ -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"], diff --git a/src/octoprint/plugins/corewizard/templates/corewizard_pluginblacklist_wizard.jinja2 b/src/octoprint/plugins/corewizard/templates/corewizard_pluginblacklist_wizard.jinja2 new file mode 100644 index 00000000..f046ac2f --- /dev/null +++ b/src/octoprint/plugins/corewizard/templates/corewizard_pluginblacklist_wizard.jinja2 @@ -0,0 +1,26 @@ +

{{ _('Configure plugin blacklist processing') }}

+ +

{% 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 %}

+ +

{% 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 %}

+ + + + diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index a6806ac7..1892a0e8 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -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) diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 4d42feac..8675adda 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -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(); } }; diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index 8829e656..4dc8b2ae 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -47,7 +47,7 @@ -
()
+
()
 
diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 7363adfe..e792846a 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -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 diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 5112eca3..4223416e 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -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): diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index d28d352b..7926af1b 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -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 diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index d5607e71..c338aeda 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -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; diff --git a/src/octoprint/templates/dialogs/settings/server.jinja2 b/src/octoprint/templates/dialogs/settings/server.jinja2 index 8e3fd59e..b7f6c497 100644 --- a/src/octoprint/templates/dialogs/settings/server.jinja2 +++ b/src/octoprint/templates/dialogs/settings/server.jinja2 @@ -10,4 +10,8 @@ {% include "snippets/settings/server/serverOnlineCheckDescription.jinja2" %} {% include "snippets/settings/server/serverOnlineCheck.jinja2" %} +

{{ _('Plugin blacklist processing') }}

+ + {% include "snippets/settings/server/serverPluginBlacklistDescription.jinja2" %} + {% include "snippets/settings/server/serverPluginBlacklist.jinja2" %} diff --git a/src/octoprint/templates/snippets/settings/server/serverPluginBlacklist.jinja2 b/src/octoprint/templates/snippets/settings/server/serverPluginBlacklist.jinja2 new file mode 100644 index 00000000..02624d73 --- /dev/null +++ b/src/octoprint/templates/snippets/settings/server/serverPluginBlacklist.jinja2 @@ -0,0 +1,11 @@ +{% include "snippets/settings/server/serverPluginBlacklistEnabled.jinja2" %} + +
+
+ +
+ {% include "snippets/settings/server/serverPluginBlacklistUrl.jinja2" %} + {% include "snippets/settings/server/serverPluginBlacklistTtl.jinja2" %} +
+
+
diff --git a/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistDescription.jinja2 b/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistDescription.jinja2 new file mode 100644 index 00000000..d4b12013 --- /dev/null +++ b/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistDescription.jinja2 @@ -0,0 +1,5 @@ +

{% 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 plugins before they can interfere + with normal operation. +{% endtrans %}

diff --git a/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistEnabled.jinja2 b/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistEnabled.jinja2 new file mode 100644 index 00000000..91576aee --- /dev/null +++ b/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistEnabled.jinja2 @@ -0,0 +1,8 @@ +
+
+ +
+
diff --git a/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistTtl.jinja2 b/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistTtl.jinja2 new file mode 100644 index 00000000..a586bac8 --- /dev/null +++ b/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistTtl.jinja2 @@ -0,0 +1,9 @@ +
+ +
+
+ + min +
+
+
diff --git a/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistUrl.jinja2 b/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistUrl.jinja2 new file mode 100644 index 00000000..781b1846 --- /dev/null +++ b/src/octoprint/templates/snippets/settings/server/serverPluginBlacklistUrl.jinja2 @@ -0,0 +1,6 @@ +
+ +
+ +
+
diff --git a/src/octoprint/util/version.py b/src/octoprint/util/version.py new file mode 100644 index 00000000..0b153a3a --- /dev/null +++ b/src/octoprint/util/version.py @@ -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