From e881a0e6acccd87c2edc652cb21601898411efc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20=E2=80=9Etadzik=E2=80=9D=20So=C5=9Bnierz?= Date: Mon, 19 Jan 2026 15:39:38 +0100 Subject: [PATCH] Bug 41651: Allow plugins to specify dependencies, abort installation if unmet This unpacks uploaded plugin into a temporary directory where we check the prerequisites/conflicts in standard CPAN ways, and only continue with the installation if all the runtime requirements are met. Signed-off-by: Martin Renvoize --- Koha/Plugins.pm | 51 +++++++++++++++++++ .../prog/en/modules/plugins/plugins-upload.tt | 4 ++ plugins/plugins-upload.pl | 12 ++++- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/Koha/Plugins.pm b/Koha/Plugins.pm index ee5ddde144e..f3a9e849f2b 100644 --- a/Koha/Plugins.pm +++ b/Koha/Plugins.pm @@ -21,7 +21,14 @@ use Modern::Perl; use Array::Utils qw( array_minus ); use Class::Inspector; +use CPAN::Meta; +use CPAN::Meta::Check qw( verify_dependencies ); +use File::Basename qw( fileparse ); +use File::Copy qw( copy ); +use File::Find qw( find ); +use File::Path qw( make_path ); use List::MoreUtils qw( any none ); +use Module::CPANfile; use Module::Load::Conditional qw( can_load ); use Module::Load; use Module::Pluggable search_path => ['Koha::Plugin'], @@ -303,6 +310,27 @@ sub InstallPlugins { my ( $self, $params ) = @_; my $verbose = $params->{verbose} // $self->_verbose; + if ( $params->{from} ) { + # We have "staged" plugin(s) passed in; check the dependencies, + # copy to the regular location if we're happy + $self->_check_dependencies($params->{from}); + + my $plugins_dir = C4::Context->config("pluginsdir"); + $plugins_dir = ref($plugins_dir) eq 'ARRAY' ? $plugins_dir->[0] : $plugins_dir; + + find({ + wanted => sub { + return unless -f $_; + my ($basename) = fileparse($_); + return if $basename =~ /^cpanfile|META/; + my $destdir = $File::Find::dir =~ s/^$params->{from}//r; + $destdir = "$plugins_dir/$destdir"; + make_path($destdir); + copy($File::Find::name, $destdir); + }, + }, $params->{from}); + } + my @plugin_classes = $self->plugins(); my @plugins; @@ -464,6 +492,29 @@ sub get_valuebuilders_installed { return @valuebuilders; } +sub _check_dependencies { + my ($self, $path) = @_; + + my $prereqs; + + if ( -f "$path/cpanfile" ) { + my $cpanfile = Module::CPANfile->load("$path/cpanfile"); + $prereqs = $cpanfile->prereqs; + } else { + my @metafiles = qw( META.json META.yml ); + for (@metafiles) { + if ( -f "$path/$_" ) { + $prereqs = CPAN::Meta->load_file("$path/$_"); + } + } + } + + return unless $prereqs; + + # Good for both file variants: per the docs, "$meta should be a CPAN::Meta::Prereqs or CPAN::Meta object" + die "$_\n" for verify_dependencies($prereqs, 'runtime', 'requires'); +} + 1; __END__ diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-upload.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-upload.tt index 63916b2d6fb..5170463c32d 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-upload.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-upload.tt @@ -55,6 +55,10 @@
  • Cannot install plugin from unknown source whilst plugins_restricted is enabled.
  • + [% ELSIF ( ERROR.INSTALLFAIL ) %] +
  • Plugin installation failed:
    [% ERROR.INSTALLFAIL | html %]
  • [% ELSE %]
  • [% ERROR.CORERR | html %] An unknown error has occurred.
    Please review the error log for more details.
  • new( archive => $tempfile, type => 'zip' ); - unless ( $ae->extract( to => $plugins_dir ) ) { + unless ( $ae->extract( to => $dirname ) ) { warn "ERROR: " . $ae->error; $errors{'UZIPFAIL'} = $uploadfilename; $template->param( ERRORS => [ \%errors ] ); @@ -129,7 +130,14 @@ if ( ( $op eq 'cud-Upload' ) && ( $uploadfile || $uploadlocation ) ) { # Install plugins; verbose not needed, we redirect to plugins-home. # FIXME There is no good way to verify the install below; we need an # individual plugin install. - Koha::Plugins->new->InstallPlugins( { verbose => 0 } ); + try { + Koha::Plugins->new->InstallPlugins( { verbose => 0, from => $dirname } ); + } catch { + $errors{'INSTALLFAIL'} = $_; + $template->param( ERRORS => [ \%errors ] ); + output_html_with_http_headers $input, $cookie, $template->output; + exit; + }; } } elsif ( ( $op eq 'cud-Upload' ) && !$uploadfile && !$uploadlocation ) { warn "Problem uploading file or no file uploaded."; -- 2.52.0