3 # Copyright 2012 Kyle Hall
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use Array::Utils qw( array_minus );
24 use List::MoreUtils qw( any );
25 use Module::Load::Conditional qw( can_load );
27 use Module::Pluggable search_path => ['Koha::Plugin'], except => qr/::Edifact(|::Line|::Message|::Order|::Segment|::Transport)$/;
34 use Koha::Cache::Memory::Lite;
35 use Koha::Exceptions::Plugin;
36 use Koha::Plugins::Datas;
37 use Koha::Plugins::Methods;
39 use constant ENABLED_PLUGINS_CACHE_KEY => 'enabled_plugins';
42 my $pluginsdir = C4::Context->config("pluginsdir");
43 my @pluginsdir = ref($pluginsdir) eq 'ARRAY' ? @$pluginsdir : $pluginsdir;
44 push @INC, array_minus(@pluginsdir, @INC) ;
45 pop @INC if $INC[-1] eq '.';
50 Koha::Plugins - Module for loading and managing plugins.
59 my ( $class, $args ) = @_;
61 return unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} );
63 $args->{'pluginsdir'} = C4::Context->config("pluginsdir");
65 return bless( $args, $class );
70 Calls a plugin method for all enabled plugins
72 @responses = Koha::Plugins->call($method, @args)
74 Note: Pass your arguments as refs, when you want subsequent plugins to use the value
75 updated by preceding plugins, provided that these plugins support that.
80 my ($class, $method, @args) = @_;
82 return unless C4::Context->config('enable_plugins');
85 my @plugins = $class->get_enabled_plugins( { verbose => 0 } );
86 @plugins = grep { $_->can($method) } @plugins;
88 # TODO: Remove warn when after_hold_create is removed from the codebase
89 warn "after_hold_create is deprecated and will be removed soon. Contact the following plugin's authors: " . join( ', ', map {$_->{metadata}->{name}} @plugins)
90 if $method eq 'after_hold_create' and @plugins;
92 foreach my $plugin (@plugins) {
93 my $response = eval { $plugin->$method(@args) };
95 warn sprintf("Plugin error (%s): %s", $plugin->get_metadata->{name}, $@);
99 push @responses, $response;
105 =head2 get_enabled_plugins
107 Returns a list of enabled plugins.
109 @plugins = Koha::Plugins->get_enabled_plugins( [ verbose => 1 ] );
113 sub get_enabled_plugins {
114 my ( $class, $params ) = @_;
116 return unless C4::Context->config('enable_plugins');
118 my $enabled_plugins = Koha::Cache::Memory::Lite->get_from_cache(ENABLED_PLUGINS_CACHE_KEY);
119 unless ($enabled_plugins) {
120 my $verbose = $params->{verbose} // $class->_verbose;
121 $enabled_plugins = [];
125 my $rs = Koha::Plugins::Datas->search({ plugin_key => '__ENABLED__', plugin_value => 1 });
126 @plugin_classes = $rs->get_column('plugin_class');
131 foreach my $plugin_class (@plugin_classes) {
132 next unless can_load( modules => { $plugin_class => undef }, verbose => $verbose, nocache => 1 );
134 my $plugin = eval { $plugin_class->new() };
135 if ($@ || !$plugin) {
136 warn "Failed to instantiate plugin $plugin_class: $@";
140 push @$enabled_plugins, $plugin;
142 Koha::Cache::Memory::Lite->set_in_cache(ENABLED_PLUGINS_CACHE_KEY, $enabled_plugins);
145 return @$enabled_plugins;
150 # Return false when running unit tests
151 return exists $ENV{_} && $ENV{_} =~ /\/prove(\s|$)|\/koha-qa\.pl$|\.t$/ ? 0 : 1;
154 =head2 feature_enabled
156 Returns a boolean denoting whether a plugin based feature is enabled or not.
158 $enabled = Koha::Plugins->feature_enabled('method_name');
162 sub feature_enabled {
163 my ( $class, $method ) = @_;
165 return 0 unless C4::Context->config('enable_plugins');
167 my $key = "ENABLED_PLUGIN_FEATURE_" . $method;
168 my $feature = Koha::Cache::Memory::Lite->get_from_cache($key);
169 unless ( defined($feature) ) {
170 my @plugins = $class->get_enabled_plugins( { verbose => 0 } );
171 my $enabled = any { $_->can($method) } @plugins;
172 Koha::Cache::Memory::Lite->set_in_cache( $key, $enabled );
179 This will return a list of all available plugins, optionally limited by
180 method or metadata value.
182 my @plugins = Koha::Plugins::GetPlugins({
183 method => 'some_method',
184 metadata => { some_key => 'some_value' },
185 [ all => 1, errors => 1, verbose => 1 ],
188 The method and metadata parameters are optional.
189 If you pass multiple keys in the metadata hash, all keys must match.
191 If you pass errors (only used in plugins-home), we return two arrayrefs:
193 ( $good, $bad ) = Koha::Plugins::GetPlugins( { errors => 1 } );
195 If you pass verbose, you can enable or disable explicitly warnings
196 from Module::Load::Conditional. Disabled by default to not flood
202 my ( $self, $params ) = @_;
204 my $method = $params->{method};
205 my $req_metadata = $params->{metadata} // {};
206 my $errors = $params->{errors};
208 # By default dont warn here unless asked to do so.
209 my $verbose = $params->{verbose} // 0;
211 my $filter = ( $method ) ? { plugin_method => $method } : undef;
213 my $plugin_classes = Koha::Plugins::Methods->search(
215 { columns => 'plugin_class',
218 )->_resultset->get_column('plugin_class');
221 # Loop through all plugins that implement at least a method
222 my ( @plugins, @failing );
223 while ( my $plugin_class = $plugin_classes->next ) {
224 if ( can_load( modules => { $plugin_class => undef }, verbose => $verbose, nocache => 1 ) ) {
227 my $failed_instantiation;
230 $plugin = $plugin_class->new({
231 enable_plugins => $self->{'enable_plugins'}
232 # loads even if plugins are disabled
233 # FIXME: is this for testing without bothering to mock config?
238 $failed_instantiation = 1;
241 next if $failed_instantiation;
243 next unless $plugin->is_enabled or
244 defined($params->{all}) && $params->{all};
246 # filter the plugin out by metadata
247 my $plugin_metadata = $plugin->get_metadata;
251 and any { !$plugin_metadata->{$_} || $plugin_metadata->{$_} ne $req_metadata->{$_} } keys %$req_metadata;
253 push @plugins, $plugin;
255 push @failing, { error => 1, name => $plugin_class };
259 return $errors ? ( \@plugins, \@failing ) : @plugins;
262 =head2 InstallPlugins
264 Koha::Plugins::InstallPlugins( [ verbose => 1 ] )
266 This method iterates through all plugins physically present on a system.
267 For each plugin module found, it will test that the plugin can be loaded,
268 and if it can, will store its available methods in the plugin_methods table.
270 NOTE: We reload all plugins here as a protective measure in case someone
271 has removed a plugin directly from the system without using the UI
276 my ( $self, $params ) = @_;
277 my $verbose = $params->{verbose} // $self->_verbose;
279 my @plugin_classes = $self->plugins();
282 foreach my $plugin_class (@plugin_classes) {
283 if ( can_load( modules => { $plugin_class => undef }, verbose => $verbose, nocache => 1 ) ) {
284 next unless $plugin_class->isa('Koha::Plugins::Base');
287 my $failed_instantiation;
290 $plugin = $plugin_class->new({ enable_plugins => $self->{'enable_plugins'} });
294 $failed_instantiation = 1;
297 next if $failed_instantiation;
299 Koha::Plugins::Methods->search({ plugin_class => $plugin_class })->delete();
301 foreach my $method ( @{ Class::Inspector->methods( $plugin_class, 'public' ) } ) {
302 Koha::Plugins::Method->new(
304 plugin_class => $plugin_class,
305 plugin_method => $method,
310 push @plugins, $plugin;
314 Koha::Cache::Memory::Lite->clear_from_cache(ENABLED_PLUGINS_CACHE_KEY);
316 $self->_restart_after_change();
323 Koha::Plugins->RemovePlugins( {
324 [ plugin_class => MODULE_NAME, destructive => 1, disable => 1 ],
327 This is primarily for unit testing. Take care when you pass the
328 destructive flag (know what you are doing)!
330 The method removes records from plugin_methods for one or all plugins.
332 If you pass the destructive flag, it will remove records too from
333 plugin_data for one or all plugins. Destructive overrules disable.
335 If you pass disable, it will disable one or all plugins (in plugin_data).
337 If you do not pass destructive or disable, this method does not touch
338 records in plugin_data. The cache key for enabled plugins will be cleared
339 only if you pass disabled or destructive.
344 my ( $class, $params ) = @_;
347 $params->{plugin_class}
348 ? ( plugin_class => $params->{plugin_class} )
351 Koha::Plugins::Methods->search($cond)->delete;
352 if ( $params->{destructive} ) {
353 Koha::Plugins::Datas->search($cond)->delete;
354 Koha::Cache::Memory::Lite->clear_from_cache( Koha::Plugins->ENABLED_PLUGINS_CACHE_KEY );
355 } elsif ( $params->{disable} ) {
356 $cond->{plugin_key} = '__ENABLED__';
357 Koha::Plugins::Datas->search($cond)->update( { plugin_value => 0 } );
358 Koha::Cache::Memory::Lite->clear_from_cache( Koha::Plugins->ENABLED_PLUGINS_CACHE_KEY );
361 $class->_restart_after_change();
364 sub _restart_after_change {
365 my ( $class, $params ) = @_;
367 return unless ( C4::Context->config('plugins_restart') && C4::Context->psgi_env );
369 my $parent_pid = getppid();
371 # Send HUP signal to Plack parent process for graceful restart
372 kill 'HUP', $parent_pid;
380 Kyle M Hall <kyle.m.hall@gmail.com>