From 592cad5b99d106fd1e7fd0dcd8eeb37c350614ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 9 Jun 2015 18:50:55 +0200 Subject: [PATCH] Deprecated AppPlugin mixin and introduced octoprint.accesscontrol.appkey hook instead Plugins implementing the mixin will automatically be migrated to providing the single mixin method as hook handler. --- docs/plugins/hooks.rst | 25 +++++++++++ docs/plugins/mixins.rst | 8 ---- src/octoprint/plugin/__init__.py | 22 +++++++++- src/octoprint/plugin/core.py | 62 +++++++++++++++------------ src/octoprint/plugin/types.py | 22 +++------- src/octoprint/server/__init__.py | 10 ++++- src/octoprint/server/apps/__init__.py | 18 ++++++-- 7 files changed, 108 insertions(+), 59 deletions(-) diff --git a/docs/plugins/hooks.rst b/docs/plugins/hooks.rst index 6096fc86..36a6e2c3 100644 --- a/docs/plugins/hooks.rst +++ b/docs/plugins/hooks.rst @@ -12,6 +12,31 @@ Available plugin hooks .. contents:: :local: +.. _sec-plugins-hook-accesscontrol-appkey: + +octoprint.accesscontrol.appkey +------------------------------ + +.. py:function:: hook(*args, **kwargs) + + By handling this hook plugins may register additional :ref:`App session key providers ` + within the system. + + Overrides this to return your additional app information to be used for validating app session keys. You'll + need to return a list of 3-tuples of the format (id, version, public key). + + The ``id`` should be the (unique) identifier of the app. Using a domain prefix might make sense here, e.g. + ``org.octoprint.example.MyApp``. + + ``version`` should be a string specifying the version of the app for which the public key is valid. You can + provide the string ``any`` here, in which case the provided public key will be valid for all versions of the + app for which no specific public key is defined. + + Finally, the public key is expected to be provided as a PKCS1 string without newlines. + + :return: A list of 3-tuples as described above + :rtype: list + .. _sec-plugins-hook-comm-protocol-action: octoprint.comm.protocol.action diff --git a/docs/plugins/mixins.rst b/docs/plugins/mixins.rst index c414934d..b4063fd7 100644 --- a/docs/plugins/mixins.rst +++ b/docs/plugins/mixins.rst @@ -91,11 +91,3 @@ SlicerPlugin .. autoclass:: octoprint.plugin.SlicerPlugin :members: -.. _sec-plugins-mixins-appplugin: - -AppPlugin ---------- - -.. autoclass:: octoprint.plugin.AppPlugin - :members: - diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 6a43b723..064eb9b4 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -31,8 +31,20 @@ from octoprint.util import deprecated # singleton _instance = None +def _validate_plugin(phase, plugin_info): + if phase == "after_load": + if plugin_info.implementation is not None and isinstance(plugin_info.implementation, AppPlugin): + # transform app plugin into hook + import warnings + warnings.warn("{name} uses deprecated plugin mixin AppPlugin, use octoprint.accesscontrol.appkey hook instead".format(name=plugin_info.key), DeprecationWarning) + + hooks = plugin_info.hooks + if not "octoprint.accesscontrol.appkey" in hooks: + hooks["octoprint.accesscontrol.appkey"] = plugin_info.implementation.get_additional_apps + setattr(plugin_info.instance, PluginInfo.attr_hooks, hooks) + 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_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. @@ -57,6 +69,7 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en to logging handlers plugin_obsolete_hooks (list): A list of hooks that have been declared obsolete. Plugins implementing them will not be enabled since they might depend on functionality that is no longer available. + plugin_validators (list): A list of additional plugin validators through which to process each plugin. Returns: PluginManager: A fully initialized :class:`~octoprint.plugin.core.PluginManager` instance to be used for plugin @@ -103,6 +116,10 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en plugin_obsolete_hooks = [ "octoprint.comm.protocol.gcode" ] + if plugin_validators is None: + plugin_validators = [ + _validate_plugin + ] _instance = PluginManager(plugin_folders, plugin_types, @@ -110,7 +127,8 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en logging_prefix="octoprint.plugins.", plugin_disabled_list=plugin_disabled_list, plugin_restart_needing_hooks=plugin_restart_needing_hooks, - plugin_obsolete_hooks=plugin_obsolete_hooks) + plugin_obsolete_hooks=plugin_obsolete_hooks, + plugin_validators=plugin_validators) else: raise ValueError("Plugin Manager not initialized yet") return _instance diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index e20062d1..d5c59bd9 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -135,36 +135,42 @@ class PluginInfo(object): self._url = url self._license = license - def validate(self): - # if the plugin still uses __plugin_implementations__, log a deprecation warning and put the first - # item into __plugin_implementation__ - if hasattr(self.instance, self.__class__.attr_implementations): - if not hasattr(self.instance, self.__class__.attr_implementation): - # deprecation warning - import warnings - warnings.warn("{name} uses deprecated control property __plugin_implementations__, use __plugin_implementation__ instead - only the first implementation of {name} will be recognized".format(name=self.key), DeprecationWarning) + def validate(self, phase, additional_validators=None): + if phase == "before_load": + # if the plugin still uses __plugin_init__, log a deprecation warning and move it to __plugin_load__ + if hasattr(self.instance, self.__class__.attr_init): + if not hasattr(self.instance, self.__class__.attr_load): + # deprecation warning + import warnings + warnings.warn("{name} uses deprecated control property __plugin_init__, use __plugin_load__ instead".format(name=self.key), DeprecationWarning) - # put first item into __plugin_implementation__ - implementations = getattr(self.instance, self.__class__.attr_implementations) - if len(implementations) > 0: - setattr(self.instance, self.__class__.attr_implementation, implementations[0]) + # move it + init = getattr(self.instance, self.__class__.attr_init) + setattr(self.instance, self.__class__.attr_load, init) - # delete __plugin_implementations__ - delattr(self.instance, self.__class__.attr_implementations) + # delete __plugin_init__ + delattr(self.instance, self.__class__.attr_init) - # if the plugin still uses __plugin_init__, log a deprecation warning and move it to __plugin_load__ - if hasattr(self.instance, self.__class__.attr_init): - if not hasattr(self.instance, self.__class__.attr_load): - # deprecation warning - import warnings - warnings.warn("{name} uses deprecated control property __plugin_init__, use __plugin_load__ instead".format(name=self.key), DeprecationWarning) + elif phase == "after_load": + # if the plugin still uses __plugin_implementations__, log a deprecation warning and put the first + # item into __plugin_implementation__ + if hasattr(self.instance, self.__class__.attr_implementations): + if not hasattr(self.instance, self.__class__.attr_implementation): + # deprecation warning + import warnings + warnings.warn("{name} uses deprecated control property __plugin_implementations__, use __plugin_implementation__ instead - only the first implementation of {name} will be recognized".format(name=self.key), DeprecationWarning) - # move it - init = getattr(self.instance, self.__class__.attr_init) - setattr(self.instance, self.__class__.attr_load, init) + # put first item into __plugin_implementation__ + implementations = getattr(self.instance, self.__class__.attr_implementations) + if len(implementations) > 0: + setattr(self.instance, self.__class__.attr_implementation, implementations[0]) - # delete __plugin_init__ - delattr(self.instance, self.__class__.attr_init) + # delete __plugin_implementations__ + delattr(self.instance, self.__class__.attr_implementations) + + if additional_validators is not None: + for validator in additional_validators: + validator(phase, self) def __str__(self): if self.version: @@ -422,7 +428,7 @@ class PluginManager(object): It is able to discover plugins both through possible file system locations as well as customizable entry points. """ - 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): + 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): self.logger = logging.getLogger(__name__) if logging_prefix is None: @@ -436,6 +442,7 @@ class PluginManager(object): self.plugin_disabled_list = plugin_disabled_list self.plugin_restart_needing_hooks = plugin_restart_needing_hooks self.plugin_obsolete_hooks = plugin_obsolete_hooks + self.plugin_validators = plugin_validators self.logging_prefix = logging_prefix self.enabled_plugins = dict() @@ -645,8 +652,9 @@ class PluginManager(object): plugin = self.plugins[name] try: + plugin.validate("before_load", additional_validators=self.plugin_validators) plugin.load() - plugin.validate() + plugin.validate("after_load", additional_validators=self.plugin_validators) self.on_plugin_loaded(name, plugin) plugin.loaded = True diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 047ae337..842e68f9 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -1038,25 +1038,13 @@ class AppPlugin(OctoPrintPlugin): """ Using the :class:`AppPlugin mixin` plugins may register additional :ref:`App session key providers ` within the system. + + .. deprecated:: 1.2.0 + + Refer to the :ref:`octoprint.accesscontrol.appkey hook ` instead. + """ def get_additional_apps(self): - """ - Overrides this to return your additional app information to be used for validating app session keys. You'll - need to return a :class:`list` of 3-tuples of the format (id, version, public key). - - The ``id`` should be the (unique) identifier of the app. Using a domain prefix might make sense here, e.g. - ``org.octoprint.example.MyApp``. - - ``version`` should be a string specifying the version of the app for which the public key is valid. You can - provide the string ``any`` here, in which case the provided public key will be valid for all versions of the - app for which no specific public key is defined. - - Finally, the public key is expected to be provided as a PKCS1 string without newlines. - - Returns: - list: A list of 3-tuples as described above. - """ - return [] diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index e5f45000..e99ff06e 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -39,6 +39,7 @@ eventManager = None loginManager = None pluginManager = None appSessionManager = None +pluginLifecycleManager = None principals = Principal(app) admin_permission = Permission(RoleNeed("admin")) @@ -138,6 +139,7 @@ class Server(): global loginManager global pluginManager global appSessionManager + global pluginLifecycleManager global debug from tornado.ioloop import IOLoop @@ -606,7 +608,7 @@ class Server(): def _setup_blueprints(self): from octoprint.server.api import api - from octoprint.server.apps import apps + from octoprint.server.apps import apps, clear_registered_app import octoprint.server.views app.register_blueprint(api, url_prefix="/api") @@ -618,6 +620,12 @@ class Server(): # and register a blueprint for serving the static files of asset plugins which are not blueprint plugins themselves self._register_asset_plugins() + global pluginLifecycleManager + def clear_apps(name, plugin): + clear_registered_app() + pluginLifecycleManager.add_callback("enabled", clear_apps) + pluginLifecycleManager.add_callback("disabled", clear_apps) + def _register_blueprint_plugins(self): blueprint_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.BlueprintPlugin) for plugin in blueprint_plugins: diff --git a/src/octoprint/server/apps/__init__.py b/src/octoprint/server/apps/__init__.py index e3650d95..e1c7f509 100644 --- a/src/octoprint/server/apps/__init__.py +++ b/src/octoprint/server/apps/__init__.py @@ -86,9 +86,15 @@ def _get_registered_apps(): if not "enabled" in app_data: apps[app]["enabled"] = True - app_plugins = octoprint.server.pluginManager.get_implementations(octoprint.plugin.AppPlugin) - for plugin in app_plugins: - additional_apps = plugin.get_additional_apps() + hooks = octoprint.server.pluginManager.get_hooks("octoprint.accesscontrol.appkey") + for name, hook in hooks.items(): + try: + additional_apps = hook() + except: + import logging + logging.getLogger(__name__).exception("Error while retrieving additional appkeys from plugin {name}".format(**locals())) + continue + any_version_enabled = dict() for app_data in additional_apps: @@ -117,4 +123,8 @@ def _get_registered_apps(): ) __registered_apps = apps - return apps \ No newline at end of file + return apps + +def clear_registered_app(): + global __registered_apps + __registered_apps = None