Bug 35790: Remove Koha::Template::Plugin::Biblio::CanArticleRequest
[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
30 use C4::Context;
31 use C4::Output;
32
33 use Koha::Cache::Memory::Lite;
34 use Koha::Exceptions::Plugin;
35 use Koha::Plugins::Datas;
36 use Koha::Plugins::Methods;
37
38 use constant ENABLED_PLUGINS_CACHE_KEY => 'enabled_plugins';
39
40 BEGIN {
41     my $pluginsdir = C4::Context->config("pluginsdir");
42     my @pluginsdir = ref($pluginsdir) eq 'ARRAY' ? @$pluginsdir : $pluginsdir;
43     push @INC, array_minus(@pluginsdir, @INC) ;
44     pop @INC if $INC[-1] eq '.';
45 }
46
47 =head1 NAME
48
49 Koha::Plugins - Module for loading and managing plugins.
50
51 =head2 new
52
53 Constructor
54
55 =cut
56
57 sub new {
58     my ( $class, $args ) = @_;
59
60     return unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} );
61
62     $args->{'pluginsdir'} = C4::Context->config("pluginsdir");
63
64     return bless( $args, $class );
65 }
66
67 =head2 call
68
69 Calls a plugin method for all enabled plugins
70
71     @responses = Koha::Plugins->call($method, @args)
72
73 Note: Pass your arguments as refs, when you want subsequent plugins to use the value
74 updated by preceding plugins, provided that these plugins support that.
75
76 =cut
77
78 sub call {
79     my ($class, $method, @args) = @_;
80
81     return unless C4::Context->config('enable_plugins');
82
83     my @responses;
84     my @plugins = $class->get_enabled_plugins( { verbose => 0 } );
85     @plugins = grep { $_->can($method) } @plugins;
86
87     # TODO: Remove warn when after_hold_create is removed from the codebase
88     warn "after_hold_create is deprecated and will be removed soon. Contact the following plugin's authors: " . join( ', ', map {$_->{metadata}->{name}} @plugins)
89         if $method eq 'after_hold_create' and @plugins;
90
91     foreach my $plugin (@plugins) {
92         my $response = eval { $plugin->$method(@args) };
93         if ($@) {
94             warn sprintf("Plugin error (%s): %s", $plugin->get_metadata->{name}, $@);
95             next;
96         }
97
98         push @responses, $response;
99     }
100
101     return @responses;
102 }
103
104 =head2 get_enabled_plugins
105
106 Returns a list of enabled plugins.
107
108     @plugins = Koha::Plugins->get_enabled_plugins( [ verbose => 1 ] );
109
110 =cut
111
112 sub get_enabled_plugins {
113     my ( $class, $params ) = @_;
114
115     return unless C4::Context->config('enable_plugins');
116
117     my $enabled_plugins = Koha::Cache::Memory::Lite->get_from_cache(ENABLED_PLUGINS_CACHE_KEY);
118     unless ($enabled_plugins) {
119         my $verbose = $params->{verbose} // $class->_verbose;
120         $enabled_plugins = [];
121
122         my @plugin_classes;
123         try {
124             my $rs = Koha::Plugins::Datas->search({ plugin_key => '__ENABLED__', plugin_value => 1 });
125             @plugin_classes = $rs->get_column('plugin_class');
126         } catch {
127             warn "$_";
128         };
129
130         foreach my $plugin_class (@plugin_classes) {
131             next unless can_load( modules => { $plugin_class => undef }, verbose => $verbose, nocache => 1 );
132
133             my $plugin = eval { $plugin_class->new() };
134             if ($@ || !$plugin) {
135                 warn "Failed to instantiate plugin $plugin_class: $@";
136                 next;
137             }
138
139             push @$enabled_plugins, $plugin;
140         }
141         Koha::Cache::Memory::Lite->set_in_cache(ENABLED_PLUGINS_CACHE_KEY, $enabled_plugins);
142     }
143
144     return @$enabled_plugins;
145 }
146
147 sub _verbose {
148     my $class = shift;
149     # Return false when running unit tests
150     return exists $ENV{_} && $ENV{_} =~ /\/prove(\s|$)|\/koha-qa\.pl$|\.t$/ ? 0 : 1;
151 }
152
153 =head2 feature_enabled
154
155 Returns a boolean denoting whether a plugin based feature is enabled or not.
156
157     $enabled = Koha::Plugins->feature_enabled('method_name');
158
159 =cut
160
161 sub feature_enabled {
162     my ( $class, $method ) = @_;
163
164     return 0 unless C4::Context->config('enable_plugins');
165
166     my $key     = "ENABLED_PLUGIN_FEATURE_" . $method;
167     my $feature = Koha::Cache::Memory::Lite->get_from_cache($key);
168     unless ( defined($feature) ) {
169         my @plugins = $class->get_enabled_plugins( { verbose => 0 } );
170         my $enabled = any { $_->can($method) } @plugins;
171         Koha::Cache::Memory::Lite->set_in_cache( $key, $enabled );
172     }
173     return $feature;
174 }
175
176 =head2 GetPlugins
177
178 This will return a list of all available plugins, optionally limited by
179 method or metadata value.
180
181     my @plugins = Koha::Plugins::GetPlugins({
182         method => 'some_method',
183         metadata => { some_key => 'some_value' },
184         [ all => 1, errors => 1, verbose => 1 ],
185     });
186
187 The method and metadata parameters are optional.
188 If you pass multiple keys in the metadata hash, all keys must match.
189
190 If you pass errors (only used in plugins-home), we return two arrayrefs:
191
192     ( $good, $bad ) = Koha::Plugins::GetPlugins( { errors => 1 } );
193
194 If you pass verbose, you can enable or disable explicitly warnings
195 from Module::Load::Conditional. Disabled by default to not flood
196 the logs.
197
198 =cut
199
200 sub GetPlugins {
201     my ( $self, $params ) = @_;
202
203     my $method       = $params->{method};
204     my $req_metadata = $params->{metadata} // {};
205     my $errors       = $params->{errors};
206
207     # By default dont warn here unless asked to do so.
208     my $verbose      = $params->{verbose} // 0;
209
210     my $filter = ( $method ) ? { plugin_method => $method } : undef;
211
212     my $plugin_classes = Koha::Plugins::Methods->search(
213         $filter,
214         {   columns  => 'plugin_class',
215             distinct => 1
216         }
217     )->_resultset->get_column('plugin_class');
218
219
220     # Loop through all plugins that implement at least a method
221     my ( @plugins, @failing );
222     while ( my $plugin_class = $plugin_classes->next ) {
223         if ( can_load( modules => { $plugin_class => undef }, verbose => $verbose, nocache => 1 ) ) {
224
225             my $plugin;
226             my $failed_instantiation;
227
228             try {
229                 $plugin = $plugin_class->new({
230                     enable_plugins => $self->{'enable_plugins'}
231                         # loads even if plugins are disabled
232                         # FIXME: is this for testing without bothering to mock config?
233                 });
234             }
235             catch {
236                 warn "$_";
237                 $failed_instantiation = 1;
238             };
239
240             next if $failed_instantiation;
241
242             next unless $plugin->is_enabled or
243                         defined($params->{all}) && $params->{all};
244
245             # filter the plugin out by metadata
246             my $plugin_metadata = $plugin->get_metadata;
247             next
248                 if $plugin_metadata
249                 and %$req_metadata
250                 and any { !$plugin_metadata->{$_} || $plugin_metadata->{$_} ne $req_metadata->{$_} } keys %$req_metadata;
251
252             push @plugins, $plugin;
253         } elsif( $errors ) {
254             push @failing, { error => 1, name => $plugin_class };
255         }
256     }
257
258     return $errors ? ( \@plugins, \@failing ) : @plugins;
259 }
260
261 =head2 InstallPlugins
262
263 Koha::Plugins::InstallPlugins( [ verbose => 1 ] )
264
265 This method iterates through all plugins physically present on a system.
266 For each plugin module found, it will test that the plugin can be loaded,
267 and if it can, will store its available methods in the plugin_methods table.
268
269 NOTE: We reload all plugins here as a protective measure in case someone
270 has removed a plugin directly from the system without using the UI
271
272 =cut
273
274 sub InstallPlugins {
275     my ( $self, $params ) = @_;
276     my $verbose = $params->{verbose} // $self->_verbose;
277
278     my @plugin_classes = $self->plugins();
279     my @plugins;
280
281     foreach my $plugin_class (@plugin_classes) {
282         if ( can_load( modules => { $plugin_class => undef }, verbose => $verbose, nocache => 1 ) ) {
283             next unless $plugin_class->isa('Koha::Plugins::Base');
284
285             my $plugin;
286             my $failed_instantiation;
287
288             try {
289                 $plugin = $plugin_class->new({ enable_plugins => $self->{'enable_plugins'} });
290             }
291             catch {
292                 warn "$_";
293                 $failed_instantiation = 1;
294             };
295
296             next if $failed_instantiation;
297
298             Koha::Plugins::Methods->search({ plugin_class => $plugin_class })->delete();
299
300             foreach my $method ( @{ Class::Inspector->methods( $plugin_class, 'public' ) } ) {
301                 Koha::Plugins::Method->new(
302                     {
303                         plugin_class  => $plugin_class,
304                         plugin_method => $method,
305                     }
306                 )->store();
307             }
308
309             push @plugins, $plugin;
310         }
311     }
312
313     Koha::Cache::Memory::Lite->clear_from_cache(ENABLED_PLUGINS_CACHE_KEY);
314
315     return @plugins;
316 }
317
318 =head2 RemovePlugins
319
320     Koha::Plugins->RemovePlugins( {
321         [ plugin_class => MODULE_NAME, destructive => 1, disable => 1 ],
322     } );
323
324     This is primarily for unit testing. Take care when you pass the
325     destructive flag (know what you are doing)!
326
327     The method removes records from plugin_methods for one or all plugins.
328
329     If you pass the destructive flag, it will remove records too from
330     plugin_data for one or all plugins. Destructive overrules disable.
331
332     If you pass disable, it will disable one or all plugins (in plugin_data).
333
334     If you do not pass destructive or disable, this method does not touch
335     records in plugin_data. The cache key for enabled plugins will be cleared
336     only if you pass disabled or destructive.
337
338 =cut
339
340 sub RemovePlugins {
341     my ( $class, $params ) = @_;
342
343     my $cond = {
344         $params->{plugin_class}
345         ? ( plugin_class => $params->{plugin_class} )
346         : ()
347     };
348     Koha::Plugins::Methods->search($cond)->delete;
349     if ( $params->{destructive} ) {
350         Koha::Plugins::Datas->search($cond)->delete;
351         Koha::Cache::Memory::Lite->clear_from_cache( Koha::Plugins->ENABLED_PLUGINS_CACHE_KEY );
352     } elsif ( $params->{disable} ) {
353         $cond->{plugin_key} = '__ENABLED__';
354         Koha::Plugins::Datas->search($cond)->update( { plugin_value => 0 } );
355         Koha::Cache::Memory::Lite->clear_from_cache( Koha::Plugins->ENABLED_PLUGINS_CACHE_KEY );
356     }
357 }
358
359 1;
360 __END__
361
362 =head1 AUTHOR
363
364 Kyle M Hall <kyle.m.hall@gmail.com>
365
366 =cut