459 lines
13 KiB
Perl
459 lines
13 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 <https://www.gnu.org/licenses>.
|
|
|
|
use Modern::Perl;
|
|
|
|
use Array::Utils qw( array_minus );
|
|
use Class::Inspector;
|
|
use List::MoreUtils qw( any none );
|
|
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;
|
|
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 '.';
|
|
|
|
# Load plugins early to ensure they're available before any transactions begin
|
|
my $enable_plugins = C4::Context->config("enable_plugins") // 0;
|
|
|
|
if ($enable_plugins) {
|
|
require Koha::Plugins::Loader;
|
|
Koha::Plugins::Loader->get_enabled_plugins();
|
|
}
|
|
|
|
}
|
|
|
|
=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');
|
|
|
|
# Delegate to Koha::Plugins::Loader which handles caching and loading
|
|
require Koha::Plugins::Loader;
|
|
return Koha::Plugins::Loader->get_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
|
|
|
|
my $plugins = Koha::Plugins->new();
|
|
$plugins->InstallPlugins(
|
|
{
|
|
[ verbose => 1,
|
|
include => ( 'Koha::Plugin::A', ... ),
|
|
exclude => ( 'Koha::Plugin::X', ... ), ]
|
|
}
|
|
);
|
|
|
|
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.
|
|
|
|
Parameters:
|
|
|
|
=over 4
|
|
|
|
=item B<exclude>: A list of class names to exclude from the process.
|
|
|
|
=item B<include>: A list of class names to limit the process to.
|
|
|
|
=item B<verbose>: Print useful information.
|
|
|
|
=back
|
|
|
|
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;
|
|
|
|
Koha::Exceptions::BadParameter->throw("Only one of 'include' and 'exclude' can be passed")
|
|
if ( $params->{exclude} && $params->{include} );
|
|
|
|
if ( defined( $params->{include} ) || defined( $params->{exclude} ) ) {
|
|
my @classes_filters =
|
|
defined( $params->{include} )
|
|
? @{ $params->{include} }
|
|
: @{ $params->{exclude} };
|
|
|
|
# Warn user if the specified classes doesn't exist and return nothing
|
|
foreach my $class_name (@classes_filters) {
|
|
unless ( any { $class_name eq $_ } @plugin_classes ) {
|
|
Koha::Exceptions::BadParameter->throw("$class_name has not been found, try a different name");
|
|
}
|
|
}
|
|
|
|
# filter things
|
|
if ( $params->{include} ) {
|
|
@plugin_classes = grep {
|
|
my $plugin_class = $_;
|
|
any { $plugin_class eq $_ } @classes_filters
|
|
} @plugin_classes;
|
|
} else { # exclude
|
|
@plugin_classes = grep {
|
|
my $plugin_class = $_;
|
|
none { $plugin_class eq $_ } @classes_filters
|
|
} @plugin_classes;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
=head2 get_valuebuilders_installed
|
|
|
|
my @valuebuilders = Koha::Plugins->new->get_valuebuilders_installed();
|
|
|
|
Returns a list of all valuebuilder plugins provided by plugins.
|
|
|
|
=cut
|
|
|
|
sub get_valuebuilders_installed {
|
|
my ($self) = @_;
|
|
|
|
# Get ENABLED plugins
|
|
my @plugins = $self->get_enabled_plugins();
|
|
|
|
my @valuebuilders;
|
|
|
|
foreach my $plugin (@plugins) {
|
|
|
|
# Check if plugin implements get_valuebuilder method
|
|
if ( $plugin->can('get_valuebuilder') ) {
|
|
|
|
# Get the value builder from the plugin
|
|
my $valuebuilder = $plugin->get_valuebuilder();
|
|
|
|
if ($valuebuilder) {
|
|
push @valuebuilders, {
|
|
name => $valuebuilder,
|
|
plugin => $plugin,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return @valuebuilders;
|
|
}
|
|
|
|
1;
|
|
__END__
|
|
|
|
=head1 AUTHOR
|
|
|
|
Kyle M Hall <kyle.m.hall@gmail.com>
|
|
|
|
=cut
|