diff --git a/doc/sources/admin/plugincustom.rst b/doc/sources/admin/plugincustom.rst index 77de976d8b2622439423fb7b1685ee54ce1f50cc..55dbc1c39884743cf0e36795623cdfc4d7ed79ab 100644 --- a/doc/sources/admin/plugincustom.rst +++ b/doc/sources/admin/plugincustom.rst @@ -223,6 +223,60 @@ This module must inherit from ``Lemonldap::NG::Portal::2F::Register::Base``. extends 'Lemonldap::NG::Portal::2F::Register::Base'; +Custom entry points +~~~~~~~~~~~~~~~~~~~ + +Entrypoints allow plugins to register custom actions when they are loaded. +LemonLDAP::NG comes with many pre-defined entrypoints already. But you may want +to add your own, especially if you are creating a plugin that can itself have +plugins. + +You can call the ``addEntryPoint`` method in your plugin's ``init`` to +create your custom entrypoints. + +.. note:: + + Only plugins loaded *after* registering the entry point will trigger it. Be + careful with the ordering of your ``customPlugins`` variable. + +Here are a few examples: + +.. code:: perl + + sub init { + my ($self) = @_; + + # ... + + # From now on, when loading a new plugin that inherits from + # My::Base::Class, run the provided code reference, with an + # instance of the plugin being loaded as first argument + $self->addEntryPoint( + isa => "My::Base::Class", + callback => sub { + my ($plugin) = @_; + $self->do_domething_with($plugin); + } + ); + + # From now on, when loading a new plugin that uses My::Role + # Call a particular method of a particular service, passing the new + # plugins instance and optional extra arguments. + # This will effectively run: + # $portal->getService('myservice') + # ->mymethod($plugin, "role_entry_point") + $self->addEntryPoint( + does => "My::Role", + service => "myservice", + method => "mymethod", + args => ["role_entry_point"], + ); + + # ... + return 1; + } + +See also ``Lemonldap::NG::Portal::Main::Plugin`` man page for full details. Example ------- diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm index 4614ed05bf45b00846841d83533b4ddea5e670f6..63f40d75e23cd211315e41aae9da018227065a39 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm @@ -213,6 +213,19 @@ sub init { '2fregisters' => 'restoreSession', [ 'GET', 'POST' ] ) if ( $self->conf->{sfRequired} ); + + $self->addEntryPoint( + isa => 'Lemonldap::NG::Portal::Main::SecondFactor', + service => "secondFactor", + method => "_enable_2f_module", + ); + + $self->addEntryPoint( + isa => 'Lemonldap::NG::Portal::2F::Register::Base', + service => "secondFactor", + method => "_enable_2f_register_module", + ); + return 1; } diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm index 8f703f4e915384f8a4625baf9be39e2ae6f2a3ed..6e47ff93dbf833d7390227166209e2cd6effd4ad 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm @@ -51,6 +51,10 @@ has _jsRedirect => ( is => 'rw' ); has trustedDomainsRe => ( is => 'rw' ); has additionalTrustedDomains => ( is => 'rw', default => sub { [] } ); +# Entrypoints +has _pluginEntryPoints => + ( is => 'rw', isa => 'ArrayRef', default => sub { [] } ); + # Lists to store plugins entry-points my @entryPoints; @@ -118,6 +122,7 @@ sub _resetPluginsAndServices { $self->spRules( {} ); $self->hook( {} ); $self->pluginSessionDataToRemember( {} ); + $self->_pluginEntryPoints( [] ); # Reinitialize arrays foreach ( qw(_macros _groups), @entryPoints ) { @@ -570,18 +575,6 @@ sub findEP { } } } - $self->logger->debug("Plugin $plugin initialized"); - - # Second factors - if ( $obj->isa("Lemonldap::NG::Portal::Main::SecondFactor") ) { - $self->_sfEngine->_enable_2f_module($obj); - $self->logger->debug("Loaded $plugin as a 2FA module"); - } - - if ( $obj->isa("Lemonldap::NG::Portal::2F::Register::Base") ) { - $self->_sfEngine->_enable_2f_register_module($obj); - $self->logger->debug("Loaded $plugin as a 2F Register module"); - } # Rules for menu if ( $obj->can('spRules') ) { @@ -592,6 +585,45 @@ sub findEP { $self->spRules->{$k} = $obj->spRules->{$k}; } } + + # Plugin entrypoints + for my $ep ( @{ $self->_pluginEntryPoints } ) { + if ( ( $ep->{can} and $obj->can( $ep->{can} ) ) + or ( $ep->{isa} and $obj->isa( $ep->{isa} ) ) + or ( $ep->{does} and $obj->does( $ep->{does} ) ) ) + { + my @args = @{ $ep->{args} || [] }; + if ( my $callback = $ep->{callback} ) { + $self->logger->debug( + "Invoking callback registered by $ep->{_pkg}"); + $callback->( $obj, @args ); + } + elsif ( $ep->{service} && $ep->{method} ) { + my $service = $self->getService( $ep->{service} ); + if ($service) { + if ( my $method = $service->can( $ep->{method} ) ) { + $self->logger->debug( + "Invoking $ep->{method} on $ep->{service}" + . " on behalf of $ep->{_pkg}" ); + $service->$method( $obj, @args ); + } + else { + $self->logger->warn( + "Service $ep->{service} has no $ep->{method} method" + . " in entrypoint added by $ep->{_pkg}" ); + } + } + else { + $self->logger->warn( + "Could not find service $ep->{service}" + . " in entrypoint added by $ep->{_pkg}" ); + } + } + } + } + + $self->logger->debug("Plugin $plugin initialized"); + return $obj; } @@ -669,4 +701,10 @@ sub addPasswordPolicyDisplay { $self->_ppRules->{$id} = {%$options}; } +sub _addPluginEntryPoint { + my ( $self, %entryPointDescription ) = @_; + push @{ $self->_pluginEntryPoints }, + { _pkg => "[unknown]", %entryPointDescription }; +} + 1; diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm index 1e41ecf2f937619d3c16301b4c1d7e04d1427209..8e8e7b4932e0e6b13069e866d37669b8ebbcfce5 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm @@ -120,6 +120,11 @@ sub addSessionDataToRemember { return; } +sub addEntryPoint { + my ( $self, %entryPointDescription ) = @_; + return $self->p->_addPluginEntryPoint(%entryPointDescription, _pkg => ref($self)); +} + 1; __END__ @@ -295,6 +300,42 @@ LemonLDAP::NG code. Example: =back +=head3 Registering new entrypoints + +Use the C method to register a new entrypoint for future plugin loads. +This can be useful if you would like to extend your plugin with other plugins, +or interact with existing LemonLDAP::NG services in some way. + +C takes a hash of options, for example: + + $self->addEntryPoint( + isa => "My::Base::Class", + callback => sub { + my ($plugin) = @_; + $self->do_domething_with($plugin); + } + ); + +=over + +=item C: trigger the entry point when the newly loaded plugin has a given method + +=item C: trigger the entry point when the newly loaded plugin consumes a given role + +=item C: trigger the entry point when the newly loaded plugin extends a given class + +=item C: when triggered, run the given code reference, with the new +plugin instance as first argument, and optional extra arguments + +=item C and C: when triggered, run the given method of the +given portal service, with the new plugin instance as first argument, and +optional extra arguments + +=item C: optional array reference of extra arguments to pass to the +callback or service + +=back + =head1 LOGGING Logging is provided by $self->logger and $self->userLogger. The following rules diff --git a/lemonldap-ng-portal/t/01-PluginEntrypoints.t b/lemonldap-ng-portal/t/01-PluginEntrypoints.t new file mode 100644 index 0000000000000000000000000000000000000000..e44be0a785e22d4d9afe4560fb723aabd2abc4d0 --- /dev/null +++ b/lemonldap-ng-portal/t/01-PluginEntrypoints.t @@ -0,0 +1,30 @@ +use warnings; +use strict; +use Test::More; + +require 't/test-lib.pm'; + +my $res; + +my $client = LLNG::Manager::Test->new( { + ini => { + customPlugins => + "t::PluginEntryPoints::Consumer t::PluginEntryPoints::Target" + } + } +); + +is_deeply( + $client->p->getService("MyListeningService")->get_log, + [ + [ 't::PluginEntryPoints::Target', 'param1' ], + [ 't::PluginEntryPoints::Target', 'param2' ], + [ 't::PluginEntryPoints::Target', 'param3' ] + ] + + , + "Check that entrypoints were called" + . " in the correct order with correct params" +); + +done_testing(); diff --git a/lemonldap-ng-portal/t/PluginEntryPoints/Consumer.pm b/lemonldap-ng-portal/t/PluginEntryPoints/Consumer.pm new file mode 100644 index 0000000000000000000000000000000000000000..48669d61197ca461f4ebf90ed850d6943c6f2ead --- /dev/null +++ b/lemonldap-ng-portal/t/PluginEntryPoints/Consumer.pm @@ -0,0 +1,56 @@ +package My::Base; +use Mouse; + +package My::Role; +use Mouse::Role; + +package t::PluginEntryPoints::Consumer; + +use strict; +use Mouse; +use Lemonldap::NG::Portal::Main::Constants qw( + PE_OK +); + +extends 'Lemonldap::NG::Portal::Main::Plugin'; + +has listeningService => ( is => 'rw' ); + +sub init { + my ($self) = @_; + + # Load listening service + $self->p->loadService( "MyListeningService", + "t::PluginEntryPoints::ListeningService" ); + $self->listeningService( $self->p->getService("MyListeningService") ); + + # Use a callback, match by implemented method + $self->addEntryPoint( + can => "my_entrypoint", + callback => sub { + my ( $plugin, @args ) = @_; + $self->listeningService->notify( $plugin, @args ); + }, + args => ["param1"] + ); + + # Use a service directly, match by superclass + $self->addEntryPoint( + isa => "My::Base", + service => "MyListeningService", + method => "notify", + args => ["param2"] + ); + + # Use a service directly, match by implemented role + $self->addEntryPoint( + does => "My::Role", + service => "MyListeningService", + method => "notify", + args => ["param3"] + ); + + return 1; +} + +1; diff --git a/lemonldap-ng-portal/t/PluginEntryPoints/ListeningService.pm b/lemonldap-ng-portal/t/PluginEntryPoints/ListeningService.pm new file mode 100644 index 0000000000000000000000000000000000000000..199ae5dfc643e816e0e7fff7a343de4140156c88 --- /dev/null +++ b/lemonldap-ng-portal/t/PluginEntryPoints/ListeningService.pm @@ -0,0 +1,32 @@ +package t::PluginEntryPoints::ListeningService; + +use strict; +use Mouse; +use Lemonldap::NG::Portal::Main::Constants qw( + PE_OK +); + +extends 'Lemonldap::NG::Portal::Main::Plugin'; + +has _log => ( is => 'rw', builder => sub { [] } ); + +sub init { + 1; +} + +sub notify { + my ( $self, @content ) = @_; + push @{ $self->_log }, [@content]; +} + +sub get_log { + my ($self) = @_; + return [ + map { + my $obj = shift @$_; + [ ref($obj), @$_ ] + } @{ $self->_log() } + ]; +} + +1; diff --git a/lemonldap-ng-portal/t/PluginEntryPoints/Target.pm b/lemonldap-ng-portal/t/PluginEntryPoints/Target.pm new file mode 100644 index 0000000000000000000000000000000000000000..8a9ed1585131d3475fdcb331802dbc6e602fcfc6 --- /dev/null +++ b/lemonldap-ng-portal/t/PluginEntryPoints/Target.pm @@ -0,0 +1,23 @@ +package t::PluginEntryPoints::Target; + +use strict; +use Mouse; +use Lemonldap::NG::Portal::Main::Constants qw( + PE_OK +); + +extends 'Lemonldap::NG::Portal::Main::Plugin', 'My::Base'; + +with 'My::Role'; + +sub init { + my ($self) = @_; + 1; +} + +sub my_entrypoint { + my ($self) = @_; + 1; +} + +1;