Koha/Koha/Plugins.pm
Martin Renvoize e4af79e513
Bug 30897: Remove recursive parent identification loop
We believe that plack will only spawn from one parent and children
cannot spawn further child processes.. as such we don't need to walk up
the tree.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
2024-04-11 16:53:43 +02:00

382 lines
11 KiB
Perl

package Koha::Plugins;
# Copyright 2012 Kyle Hall
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use Array::Utils qw( array_minus );
use Class::Inspector;
use List::MoreUtils qw( any );
use Module::Load::Conditional qw( can_load );
use Module::Load;
use Module::Pluggable search_path => ['Koha::Plugin'], except => qr/::Edifact(|::Line|::Message|::Order|::Segment|::Transport)$/;
use Try::Tiny;
use POSIX qw(getpid);
use C4::Context;
use C4::Output;
use Koha::Cache::Memory::Lite;
use Koha::Exceptions::Plugin;
use Koha::Plugins::Datas;
use Koha::Plugins::Methods;
use constant ENABLED_PLUGINS_CACHE_KEY => 'enabled_plugins';
BEGIN {
my $pluginsdir = C4::Context->config("pluginsdir");
my @pluginsdir = ref($pluginsdir) eq 'ARRAY' ? @$pluginsdir : $pluginsdir;
push @INC, array_minus(@pluginsdir, @INC) ;
pop @INC if $INC[-1] eq '.';
}
=head1 NAME
Koha::Plugins - Module for loading and managing plugins.
=head2 new
Constructor
=cut
sub new {
my ( $class, $args ) = @_;
return unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} );
$args->{'pluginsdir'} = C4::Context->config("pluginsdir");
return bless( $args, $class );
}
=head2 call
Calls a plugin method for all enabled plugins
@responses = Koha::Plugins->call($method, @args)
Note: Pass your arguments as refs, when you want subsequent plugins to use the value
updated by preceding plugins, provided that these plugins support that.
=cut
sub call {
my ($class, $method, @args) = @_;
return unless C4::Context->config('enable_plugins');
my @responses;
my @plugins = $class->get_enabled_plugins( { verbose => 0 } );
@plugins = grep { $_->can($method) } @plugins;
# TODO: Remove warn when after_hold_create is removed from the codebase
warn "after_hold_create is deprecated and will be removed soon. Contact the following plugin's authors: " . join( ', ', map {$_->{metadata}->{name}} @plugins)
if $method eq 'after_hold_create' and @plugins;
foreach my $plugin (@plugins) {
my $response = eval { $plugin->$method(@args) };
if ($@) {
warn sprintf("Plugin error (%s): %s", $plugin->get_metadata->{name}, $@);
next;
}
push @responses, $response;
}
return @responses;
}
=head2 get_enabled_plugins
Returns a list of enabled plugins.
@plugins = Koha::Plugins->get_enabled_plugins( [ verbose => 1 ] );
=cut
sub get_enabled_plugins {
my ( $class, $params ) = @_;
return unless C4::Context->config('enable_plugins');
my $enabled_plugins = Koha::Cache::Memory::Lite->get_from_cache(ENABLED_PLUGINS_CACHE_KEY);
unless ($enabled_plugins) {
my $verbose = $params->{verbose} // $class->_verbose;
$enabled_plugins = [];
my @plugin_classes;
try {
my $rs = Koha::Plugins::Datas->search({ plugin_key => '__ENABLED__', plugin_value => 1 });
@plugin_classes = $rs->get_column('plugin_class');
} catch {
warn "$_";
};
foreach my $plugin_class (@plugin_classes) {
next unless can_load( modules => { $plugin_class => undef }, verbose => $verbose, nocache => 1 );
my $plugin = eval { $plugin_class->new() };
if ($@ || !$plugin) {
warn "Failed to instantiate plugin $plugin_class: $@";
next;
}
push @$enabled_plugins, $plugin;
}
Koha::Cache::Memory::Lite->set_in_cache(ENABLED_PLUGINS_CACHE_KEY, $enabled_plugins);
}
return @$enabled_plugins;
}
sub _verbose {
my $class = shift;
# Return false when running unit tests
return exists $ENV{_} && $ENV{_} =~ /\/prove(\s|$)|\/koha-qa\.pl$|\.t$/ ? 0 : 1;
}
=head2 feature_enabled
Returns a boolean denoting whether a plugin based feature is enabled or not.
$enabled = Koha::Plugins->feature_enabled('method_name');
=cut
sub feature_enabled {
my ( $class, $method ) = @_;
return 0 unless C4::Context->config('enable_plugins');
my $key = "ENABLED_PLUGIN_FEATURE_" . $method;
my $feature = Koha::Cache::Memory::Lite->get_from_cache($key);
unless ( defined($feature) ) {
my @plugins = $class->get_enabled_plugins( { verbose => 0 } );
my $enabled = any { $_->can($method) } @plugins;
Koha::Cache::Memory::Lite->set_in_cache( $key, $enabled );
}
return $feature;
}
=head2 GetPlugins
This will return a list of all available plugins, optionally limited by
method or metadata value.
my @plugins = Koha::Plugins::GetPlugins({
method => 'some_method',
metadata => { some_key => 'some_value' },
[ all => 1, errors => 1, verbose => 1 ],
});
The method and metadata parameters are optional.
If you pass multiple keys in the metadata hash, all keys must match.
If you pass errors (only used in plugins-home), we return two arrayrefs:
( $good, $bad ) = Koha::Plugins::GetPlugins( { errors => 1 } );
If you pass verbose, you can enable or disable explicitly warnings
from Module::Load::Conditional. Disabled by default to not flood
the logs.
=cut
sub GetPlugins {
my ( $self, $params ) = @_;
my $method = $params->{method};
my $req_metadata = $params->{metadata} // {};
my $errors = $params->{errors};
# By default dont warn here unless asked to do so.
my $verbose = $params->{verbose} // 0;
my $filter = ( $method ) ? { plugin_method => $method } : undef;
my $plugin_classes = Koha::Plugins::Methods->search(
$filter,
{ columns => 'plugin_class',
distinct => 1
}
)->_resultset->get_column('plugin_class');
# Loop through all plugins that implement at least a method
my ( @plugins, @failing );
while ( my $plugin_class = $plugin_classes->next ) {
if ( can_load( modules => { $plugin_class => undef }, verbose => $verbose, nocache => 1 ) ) {
my $plugin;
my $failed_instantiation;
try {
$plugin = $plugin_class->new({
enable_plugins => $self->{'enable_plugins'}
# loads even if plugins are disabled
# FIXME: is this for testing without bothering to mock config?
});
}
catch {
warn "$_";
$failed_instantiation = 1;
};
next if $failed_instantiation;
next unless $plugin->is_enabled or
defined($params->{all}) && $params->{all};
# filter the plugin out by metadata
my $plugin_metadata = $plugin->get_metadata;
next
if $plugin_metadata
and %$req_metadata
and any { !$plugin_metadata->{$_} || $plugin_metadata->{$_} ne $req_metadata->{$_} } keys %$req_metadata;
push @plugins, $plugin;
} elsif( $errors ) {
push @failing, { error => 1, name => $plugin_class };
}
}
return $errors ? ( \@plugins, \@failing ) : @plugins;
}
=head2 InstallPlugins
Koha::Plugins::InstallPlugins( [ verbose => 1 ] )
This method iterates through all plugins physically present on a system.
For each plugin module found, it will test that the plugin can be loaded,
and if it can, will store its available methods in the plugin_methods table.
NOTE: We reload all plugins here as a protective measure in case someone
has removed a plugin directly from the system without using the UI
=cut
sub InstallPlugins {
my ( $self, $params ) = @_;
my $verbose = $params->{verbose} // $self->_verbose;
my @plugin_classes = $self->plugins();
my @plugins;
foreach my $plugin_class (@plugin_classes) {
if ( can_load( modules => { $plugin_class => undef }, verbose => $verbose, nocache => 1 ) ) {
next unless $plugin_class->isa('Koha::Plugins::Base');
my $plugin;
my $failed_instantiation;
try {
$plugin = $plugin_class->new({ enable_plugins => $self->{'enable_plugins'} });
}
catch {
warn "$_";
$failed_instantiation = 1;
};
next if $failed_instantiation;
Koha::Plugins::Methods->search({ plugin_class => $plugin_class })->delete();
foreach my $method ( @{ Class::Inspector->methods( $plugin_class, 'public' ) } ) {
Koha::Plugins::Method->new(
{
plugin_class => $plugin_class,
plugin_method => $method,
}
)->store();
}
push @plugins, $plugin;
}
}
Koha::Cache::Memory::Lite->clear_from_cache(ENABLED_PLUGINS_CACHE_KEY);
$self->_restart_after_change();
return @plugins;
}
=head2 RemovePlugins
Koha::Plugins->RemovePlugins( {
[ plugin_class => MODULE_NAME, destructive => 1, disable => 1 ],
} );
This is primarily for unit testing. Take care when you pass the
destructive flag (know what you are doing)!
The method removes records from plugin_methods for one or all plugins.
If you pass the destructive flag, it will remove records too from
plugin_data for one or all plugins. Destructive overrules disable.
If you pass disable, it will disable one or all plugins (in plugin_data).
If you do not pass destructive or disable, this method does not touch
records in plugin_data. The cache key for enabled plugins will be cleared
only if you pass disabled or destructive.
=cut
sub RemovePlugins {
my ( $class, $params ) = @_;
my $cond = {
$params->{plugin_class}
? ( plugin_class => $params->{plugin_class} )
: ()
};
Koha::Plugins::Methods->search($cond)->delete;
if ( $params->{destructive} ) {
Koha::Plugins::Datas->search($cond)->delete;
Koha::Cache::Memory::Lite->clear_from_cache( Koha::Plugins->ENABLED_PLUGINS_CACHE_KEY );
} elsif ( $params->{disable} ) {
$cond->{plugin_key} = '__ENABLED__';
Koha::Plugins::Datas->search($cond)->update( { plugin_value => 0 } );
Koha::Cache::Memory::Lite->clear_from_cache( Koha::Plugins->ENABLED_PLUGINS_CACHE_KEY );
}
$class->_restart_after_change();
}
sub _restart_after_change {
my ( $class, $params ) = @_;
return unless ( C4::Context->config('plugins_restart') && C4::Context->psgi_env );
my $parent_pid = getppid();
# Send HUP signal to Plack parent process for graceful restart
kill 'HUP', $parent_pid;
}
1;
__END__
=head1 AUTHOR
Kyle M Hall <kyle.m.hall@gmail.com>
=cut