Feature Proposal: Plugins should be able to control which other plugins are enabled

Motivation

Plugins that export topics (e.g. PublishPlugin) should be able to restrict which plugins are enabled. For example, EditTablePlugin should not be enabled because the edit button is not appropriate when exporting topics.

Description and Documentation

PublishPlugin must be able to control the set of plugins that is enabled when publishing. PublishPlugin should also respect DISABLEDPLUGINS, which suggests that there should be two ways to control which plugins are enabled. If a plugin is disabled via either interface, then it should be disabled.

At present, the only interface for determining if a plugin is enabled is via the MyPluginEnabled context that is "entered" for each enabled plugin. Crawford said it would be better to use $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} instead. This would mean that $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} has one meaning in LocalSite.cfg and in configure (i.e. Foswiki may load the plugin) and a new, slightly different, meaning after a Foswiki session is created (i.e. the plugin is enabled). The old meaning would no longer be available after a Foswiki session is created.

I thought about using $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} for the "extra" interface needed by PublishPlugin. The Foswiki::Plugins class has to do a number of things to disable or enable a plugin. That class could provide a method to do that work, but then plugins must do two things (change $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} and then call a function) to enable or disable a plugin. This is problematic because the two things can be separated (which can make maintenance and debugging ... difficult). I also think this is overly complicated: if a plugin has to call a function anyway to make a change take effect, then there is little advantage to using $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} as part of the interface. However, my main reason for not using $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} is that I can't find a clean way (that is easy to document) to combine run-time changes to $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} with run-time changes to the DISABLEDPLUGINS preference. For example, plugin authors may think that setting $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} makes a plugin enabled, but I do not think it should do so if the plugin was disabled via the DISABLEDPLUGINS preference.

Foswiki tries to load all plugins (and dispatch their initPlugin) before it processes the DISABLEDPLUGINS preference. This should not change.

Foswiki does not dispatch initPlugin again when plugins (or core) call pushTopicContext or popTopicContext. This should not change.

pushTopicContext or popTopicContext do not make Foswiki re-process the DISABLEDPLUGINS preference. This is a bug. This bug affects registered %TAG% handlers, handlers like commonTagsHandler and the MyPluginEnabled contexts .

This is how I think it should work, in the form of requirements:
  • Foswiki shall provide for two levels of “enabling” for plugins: loaded and enabled.
  • Foswiki shall try to load MyPlugin if-and-only-if $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} is true in LocalSite.cfg.
  • MyPlugin shall be considered “loaded” if-and-only-if all of the following are true:
    • Foswiki can eval “use $Foswiki::cfg{Plugins}{MyPlugin}{Module}” without errors
    • If the plugin has an earlyInitPlugin, then that function returned false when called
    • The plugin has an initPlugin function
    • The plugin’s initPlugin function returned false
  • The set of enabled plugins shall be a subset of the set of loaded plugins.
  • By default, loaded plugins shall also be enabled.
  • Plugins listed in the DISABLEDPLUGINS preference shall be disabled.
  • Foswiki shall provide an additional interface for disabling plugins.
  • After Foswiki has (tried to) load plugins, the following shall be true if-and-only-if MyPlugin is enabled:
    • $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} is true
    • The MyPluginEnabled context is “entered”
    • MyPlugin’s registered %TAG% handlers are invoked as necessary
    • MyPlugin’s non-initialisation handlers are invoked as necessary
  • The non-initialisation handlers are all except earlyInitPlugin, initPlugin, initializeUserHandler and finishPlugin
  • This must work in a persistent environment (e.g. modperl or fgci)
Interface requirements:
  • Foswiki::Func::pushTopicContext and Foswiki::Func::popTopicContext shall make Foswiki re-evaluate the DISABLEDPLUGINS preference and update (refresh) the “enabled” state of plugins accordingly.
  • Setting DISABLEDPLUGINS via Foswiki::Func::setPreferencesValue shall make Foswiki re-evaluate the DISABLEDPLUGINS preference and update (refresh) the “enabled” state of plugins accordingly.
  • The Foswiki::Plugins class shall provide a method for determining if a plugin is loaded
  • $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} shall be the formal interface for determining if a plugin is enabled.
  • Plugins should not modify $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} directly. If they do, they are responsible for restoring the original values.
  • Foswiki::Func shall provide a function for disabling a plugin
  • Foswiki::Func shall provide a function for permitting a plugin to be enabled.
    • This function could be combined with the previous function, if the function is named something like permitPluginToBeEnabled
    • Foswiki shall only enable the plugin if the plugin is not disabled for any other reason (i.e. the plugin is not loaded or the plugin is listed in DISABLEDPLUGINS).
Selected implementation details:
  • Foswiki::Plugin::registerHandlers should be split into two parts – calling the plugin’s initPlugin, and registering the plugin’s handlers. initPlugin should be called once, when the Foswiki session is created. It should be possible to register handlers without calling initPlugin, so that the lists of registered handlers may be rebuilt as plugins are enabled or disabled e.g. with DISABLEDPLUGINS.
  • The Foswiki::Plugin class has a {disabled} property. It should be replaced by a “faulty” or “not-loaded” property. Foswiki::Plugins should maintain the “disabled” status separately.
  • Foswiki::Plugins::enable should be refactored so that registering of handlers may be repeated, e.g. if DISABLEDPLUGINS changes.
  • The Foswiki::Plugins object should update $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} and enter or leave the MyPluginEnabled context whenever the handlers are re-registered.
  • Foswiki::Func::registerTagHandler creates a closure that checks for the MyPluginEnabled context before calling the tag handler. The closure should check $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} instead. Alternatively, the tag registration process should record which plugin registers each tag handler, so that the tag handlers may be (temporarily) removed, as the list of enabled plugins changes.
  • The Foswiki::Plugins object should restore the original $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} values in the finish() method, so that it all still works in persistent environments.
  • setPreferencesValue, pushTopicContext and popTopicContext should trigger the re-registering of handlers if the DISABLEDPLUGINS preference changes.

Examples

Impact

%WHATDOESITAFFECT%
edit
The meaning of $Foswiki::cfg{Plugins}{MyPlugin}{Enabled} after a Foswiki session is created
Changes from "Foswiki may try to load MyPlugin" to "MyPlugin is enabled"
edit
Effect of change to DISABLEDPLUGINS preference via pushTopicContext, popTopicContext or setPreferencesValues
Changes from "no effect" (which I think is a bug) to "reprocessed"
edit
Foswiki::Func
New functions added
edit
Foswiki::Plugin's $plugin->{disabled} property removed
Replaced by a property which indicates if the plugin is loaded, and by a separate structure in the Foswiki::Plugins object.
edit

Implementation

-- Contributors: MichaelTempest - 14 Aug 2009

Discussion

I know that we are under feature-freeze for 1.1, so I do not anticipate that this would go into 1.1.

-- MichaelTempest - 14 Aug 2009

This proposal makes a lot of sense to me and the implementation details have some (aligned) overlap with FSAPluginHooks.

Let's mature the technical details and push this forward.

-- GilmarSantosJr - 15 Aug 2009

I am still not sure what kind of idiom to use for a new Foswiki::Func interface for enabling and disabling plugins. I do not like Foswiki::Func::enablePlugin('ThatOtherPlugin') because ThatOtherPlugin would only be enabled if nothing else had disabled it. I do not like "Lock" and "unlock, either, because "lock" is ambiguous. If a plugin is locked, does that mean that it cannot do anything, or does it mean that the plugin cannot be disabled?

I also think the interface should be generalised. If there can be two reasons for disabling a plugin, why not have n reasons?

I think adding and removing shackles could work. A plugin is enabled if it is not shackled, and there may be multiple shackles. The interface could be something like Foswiki::Func::addPluginShackle($pluginToShackle, $optionalShackleName) and Foswiki::Func::removePluginShackle($pluginToShackle, $optionalShackleName). The default value for the $optionalShackleName would be derived from the caller's package name. With this approach, 'DISABLEDPLUGINS' could be one type of shackle.

-- MichaelTempest - 15 Aug 2009
[06:31] <CDot> it kinda depends how sophisticated we want to get.
[06:32] <CDot> For example, consider the ActionTrackerPlugin. During a pulbish, i want to disable the UI, but not the whole plugin
[06:32] <CDot> I want action searches to continue to work, for example
[06:33] <CDot> on the other hand, there may be a really crude plugin out there that doesn't understand shackles
[06:33] <CDot> and i want to disable the whole thing
[06:34] * MTempest looks at action searches
[06:34] <CDot> I can already "self-shackle" in the presence of another plugin, via the Context mechanism
[06:35] <CDot> but there is no standard for the context mechanism to say, for example, "disable UIs in this context"
[06:36] <CDot> so rather than explicitly shacking other plugins, I'd quite like to be able to say enterContext("UI free") and have other plugins self-shackle.
[06:37] <CDot> of course, that depends on those plugin authors understanding this, and doing somethnig about it, which is unlikely
[06:37] * MTempest was just thinking that...
[06:37] <CDot> so, how about a generic mechanism that lets me define (in preferences) plugins to be disabled in certain contexts
[06:38] <CDot> so you define the "UI_FREE" context, and somewhere say * Set PLUGIN_DISABLED_IN_CONTEXT = {UI_FREE => BlahBlahPlugin}
[06:39] <CDot> then, as a "smart" plugin author, i can write if (inContext("UI_FREE")) in my code, to self shackle
[06:39] * CDot is just brain dumping here
[06:40] * MTempest is listening
[06:40] <CDot> you then define a series of contexts, the first of which are already defined (e.g. command_line)
[06:41] <CDot> so I can say * Set DISABLE_IN_CONTEXT = {command_line => "UIByRESTPlugin"}
[06:41] <CDot> etc
[06:41] <CDot> that mechanism is pretty clunky; I'm sure you could come up with somethig better
[06:42] * CDot has emptied his brain
[06:45] <CDot> thinking more about it, this is really just a generalisation of DISABLED_PLUGINS
[06:46] <CDot> * Set DISABLE_IN_CONTEXT = {'*' => 'ThisPlugin'} is equvalent to * Set DISABLED_PLUGINS = ThisPlugin
[06:47] <MTempest> True. Whilst I think self-shackling is best, I also think there is a place for force-shackling not-so-smart plugins.
[06:47] <CDot> interesting problem; Plugin A sets context X. Plugin B sets context Y. * Set DISABLE_IN_CONTEXT = {X=>'A', Y=>'B'}. Who wins?
[06:48] <CDot> self-shackling is defintely best, but it depends on a taxonomy of contexts that is agreed
[06:49] <CDot> from experience, that's unlikely to happen, and if it does happen, it won't be right first time
[06:49] <CDot> so involving a third party (preferences) does make a certain amount of sense.
[06:51] <MTempest> [08:45] <CDot> interesting problem; Plugin A sets context X. Plugin B sets context Y. * Set DISABLE_IN_CONTEXT = {X=>'A', Y=>'B'}. Who wins?
[06:51] <MTempest> I would make that undefined, to start with.
[06:52] <MTempest> In other words - don't do that smile
[06:52] <CDot> I like your approach, of defining the rules. If there is documentation, it doesn't actually matter how that cycle is broken smile
[06:56] <MTempest> Hmm. Suppose we defined a "readonly" context, which is entered when the user does not have permission to change the current topic, then plugins could inhibit buttons etc that would change the current topic
[06:57] <MTempest> For the sake of an example, suppose EditTablePlugin did not respect that context, then I could * Set DISABLE_IN_CONTEXT = { readonly=>'EditTablePlugin' }
[07:00] <MTempest> PublishPlugin could enter the "readonly" context and presto! EditTablePlugin is disabled.
...
[07:14] <CDot> "readonly" context - I like that a lot

-- MichaelTempest - 15 Aug 2009

This has been implemented as "static" context. Setting to a DiscardedProposal as the implementation is somewhat different.

-- Main.GeorgeClark - 03 Nov 2015 - 16:15
Topic revision: r4 - 03 Nov 2015, GeorgeClark
The copyright of the content on this website is held by the contributing authors, except where stated elsewhere. See Copyright Statement. Creative Commons License    Legal Imprint    Privacy Policy