Bug 32610: (QA follow-up) Tidy Koha::Patron::Attribute
[koha.git] / Koha / Plugins.pm
1 package Koha::Plugins;
2
3 # Copyright 2012 Kyle Hall
4 #
5 # This file is part of Koha.
6 #
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.
11 #
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.
16 #
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>.
19
20 use Modern::Perl;
21
22 use Array::Utils qw( array_minus );
23 use Class::Inspector;
24 use List::MoreUtils qw( any );
25 use Module::Load::Conditional qw( can_load );
26 use Module::Load;
27 use Module::Pluggable search_path => ['Koha::Plugin'], except => qr/::Edifact(|::Line|::Message|::Order|::Segment|::Transport)$/;
28 use Try::Tiny;
29 use POSIX qw(getpid);
30
31 use C4::Context;
32 use C4::Output;
33
34 use Koha::Cache::Memory::Lite;
35 use Koha::Exceptions::Plugin;
36 use Koha::Plugins::Datas;
37 use Koha::Plugins::Methods;
38
39 use constant ENABLED_PLUGINS_CACHE_KEY => 'enabled_plugins';
40
41 BEGIN {
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 '.';
46 }
47
48 =head1 NAME
49
50 Koha::Plugins - Module for loading and managing plugins.
51
52 =head2 new
53
54 Constructor
55
56 =cut
57
58 sub new {
59     my ( $class, $args ) = @_;
60
61     return unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} );
62
63     $args->{'pluginsdir'} = C4::Context->config("pluginsdir");
64
65     return bless( $args, $class );
66 }
67
68 =head2 call
69
70 Calls a plugin method for all enabled plugins
71
72     @responses = Koha::Plugins->call($method, @args)
73
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.
76
77 =cut
78
79 sub call {
80     my ($class, $method, @args) = @_;
81
82     return unless C4::Context->config('enable_plugins');
83
84     my @responses;
85     my @plugins = $class->get_enabled_plugins( { verbose => 0 } );
86     @plugins = grep { $_->can($method) } @plugins;
87
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;
91
92     foreach my $plugin (@plugins) {
93         my $response = eval { $plugin->$method(@args) };
94         if ($@) {
95             warn sprintf("Plugin error (%s): %s", $plugin->get_metadata->{name}, $@);
96             next;
97         }
98
99         push @responses, $response;
100     }
101
102     return @responses;
103 }
104
105 =head2 get_enabled_plugins
106
107 Returns a list of enabled plugins.
108
109     @plugins = Koha::Plugins->get_enabled_plugins( [ verbose => 1 ] );
110
111 =cut
112
113 sub get_enabled_plugins {
114     my ( $class, $params ) = @_;
115
116     return unless C4::Context->config('enable_plugins');
117
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 = [];
122
123         my @plugin_classes;
124         try {
125             my $rs = Koha::Plugins::Datas->search({ plugin_key => '__ENABLED__', plugin_value => 1 });
126             @plugin_classes = $rs->get_column('plugin_class');
127         } catch {
128             warn "$_";
129         };
130
131         foreach my $plugin_class (@plugin_classes) {
132             next unless can_load( modules => { $plugin_class => undef }, verbose => $verbose, nocache => 1 );
133
134             my $plugin = eval { $plugin_class->new() };
135             if ($@ || !$plugin) {
136                 warn "Failed to instantiate plugin $plugin_class: $@";
137                 next;
138             }
139
140             push @$enabled_plugins, $plugin;
141         }
142         Koha::Cache::Memory::Lite->set_in_cache(ENABLED_PLUGINS_CACHE_KEY, $enabled_plugins);
143     }
144
145     return @$enabled_plugins;
146 }
147
148 sub _verbose {
149     my $class = shift;
150     # Return false when running unit tests
151     return exists $ENV{_} && $ENV{_} =~ /\/prove(\s|$)|\/koha-qa\.pl$|\.t$/ ? 0 : 1;
152 }
153
154 =head2 feature_enabled
155
156 Returns a boolean denoting whether a plugin based feature is enabled or not.
157
158     $enabled = Koha::Plugins->feature_enabled('method_name');
159
160 =cut
161
162 sub feature_enabled {
163     my ( $class, $method ) = @_;
164
165     return 0 unless C4::Context->config('enable_plugins');
166
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 );
173     }
174     return $feature;
175 }
176
177 =head2 GetPlugins
178
179 This will return a list of all available plugins, optionally limited by
180 method or metadata value.
181
182     my @plugins = Koha::Plugins::GetPlugins({
183         method => 'some_method',
184         metadata => { some_key => 'some_value' },
185         [ all => 1, errors => 1, verbose => 1 ],
186     });
187
188 The method and metadata parameters are optional.
189 If you pass multiple keys in the metadata hash, all keys must match.
190
191 If you pass errors (only used in plugins-home), we return two arrayrefs:
192
193     ( $good, $bad ) = Koha::Plugins::GetPlugins( { errors => 1 } );
194
195 If you pass verbose, you can enable or disable explicitly warnings
196 from Module::Load::Conditional. Disabled by default to not flood
197 the logs.
198
199 =cut
200
201 sub GetPlugins {
202     my ( $self, $params ) = @_;
203
204     my $method       = $params->{method};
205     my $req_metadata = $params->{metadata} // {};
206     my $errors       = $params->{errors};
207
208     # By default dont warn here unless asked to do so.
209     my $verbose      = $params->{verbose} // 0;
210
211     my $filter = ( $method ) ? { plugin_method => $method } : undef;
212
213     my $plugin_classes = Koha::Plugins::Methods->search(
214         $filter,
215         {   columns  => 'plugin_class',
216             distinct => 1
217         }
218     )->_resultset->get_column('plugin_class');
219
220
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 ) ) {
225
226             my $plugin;
227             my $failed_instantiation;
228
229             try {
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?
234                 });
235             }
236             catch {
237                 warn "$_";
238                 $failed_instantiation = 1;
239             };
240
241             next if $failed_instantiation;
242
243             next unless $plugin->is_enabled or
244                         defined($params->{all}) && $params->{all};
245
246             # filter the plugin out by metadata
247             my $plugin_metadata = $plugin->get_metadata;
248             next
249                 if $plugin_metadata
250                 and %$req_metadata
251                 and any { !$plugin_metadata->{$_} || $plugin_metadata->{$_} ne $req_metadata->{$_} } keys %$req_metadata;
252
253             push @plugins, $plugin;
254         } elsif( $errors ) {
255             push @failing, { error => 1, name => $plugin_class };
256         }
257     }
258
259     return $errors ? ( \@plugins, \@failing ) : @plugins;
260 }
261
262 =head2 InstallPlugins
263
264 Koha::Plugins::InstallPlugins( [ verbose => 1 ] )
265
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.
269
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
272
273 =cut
274
275 sub InstallPlugins {
276     my ( $self, $params ) = @_;
277     my $verbose = $params->{verbose} // $self->_verbose;
278
279     my @plugin_classes = $self->plugins();
280     my @plugins;
281
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');
285
286             my $plugin;
287             my $failed_instantiation;
288
289             try {
290                 $plugin = $plugin_class->new({ enable_plugins => $self->{'enable_plugins'} });
291             }
292             catch {
293                 warn "$_";
294                 $failed_instantiation = 1;
295             };
296
297             next if $failed_instantiation;
298
299             Koha::Plugins::Methods->search({ plugin_class => $plugin_class })->delete();
300
301             foreach my $method ( @{ Class::Inspector->methods( $plugin_class, 'public' ) } ) {
302                 Koha::Plugins::Method->new(
303                     {
304                         plugin_class  => $plugin_class,
305                         plugin_method => $method,
306                     }
307                 )->store();
308             }
309
310             push @plugins, $plugin;
311         }
312     }
313
314     Koha::Cache::Memory::Lite->clear_from_cache(ENABLED_PLUGINS_CACHE_KEY);
315
316     $self->_restart_after_change();
317
318     return @plugins;
319 }
320
321 =head2 RemovePlugins
322
323     Koha::Plugins->RemovePlugins( {
324         [ plugin_class => MODULE_NAME, destructive => 1, disable => 1 ],
325     } );
326
327     This is primarily for unit testing. Take care when you pass the
328     destructive flag (know what you are doing)!
329
330     The method removes records from plugin_methods for one or all plugins.
331
332     If you pass the destructive flag, it will remove records too from
333     plugin_data for one or all plugins. Destructive overrules disable.
334
335     If you pass disable, it will disable one or all plugins (in plugin_data).
336
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.
340
341 =cut
342
343 sub RemovePlugins {
344     my ( $class, $params ) = @_;
345
346     my $cond = {
347         $params->{plugin_class}
348         ? ( plugin_class => $params->{plugin_class} )
349         : ()
350     };
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 );
359     }
360
361     $class->_restart_after_change();
362 }
363
364 sub _restart_after_change {
365     my ( $class, $params ) = @_;
366
367     return unless ( C4::Context->config('plugins_restart') && C4::Context->psgi_env );
368
369     my $parent_pid = getppid();
370
371     # Send HUP signal to Plack parent process for graceful restart
372     kill 'HUP', $parent_pid;
373 }
374
375 1;
376 __END__
377
378 =head1 AUTHOR
379
380 Kyle M Hall <kyle.m.hall@gmail.com>
381
382 =cut