From 5eabc672fd818b312b3dbc8e2c19defcca97ed13 Mon Sep 17 00:00:00 2001 From: Kyle M Hall Date: Fri, 22 Jan 2010 16:34:52 +0000 Subject: [PATCH 1/6] Bug 7804 - Add Koha Plugin System Adds support for custom plugins. At the moment the Plugins feature supports two types of plugins, reports and tools. Plugins are installed by uploading KPZ ( Koha Plugin Zip ) packages. A KPZ file is just a zip file containing the perl files, template files, and any other files neccessary to make the plugin work. Test plan: 1) Apply patch 2) Run updatedatabase.pl 3) Create the directory /var/lib/koha/plugins 4) Add the lines /var/lib/koha/plugins 1" to your koha-conf.xml file 5) Add the line Alias /plugin/ "/var/lib/koha/plugins/" to your koha-httpd.conf file 6) Restart your webserver 7) Access the plugins system from the "More" pulldown 8) Upload the example plugin file provided here 9) Try it out! Signed-off-by: Bernardo Gonzalez Kriegel Signed-off-by: Jonathan Druart Signed-off-by: Kyle M Hall Signed-off-by: Jared Camins-Esakov --- C4/Auth.pm | 4 +- C4/Installer/PerlDependencies.pm | 40 ++++ C4/Templates.pm | 4 +- Koha/Plugins.pm | 87 ++++++++ Koha/Plugins/Base.pm | 190 ++++++++++++++++++ Koha/Plugins/Handler.pm | 100 +++++++++ Makefile.PL | 10 +- debian/templates/koha-conf-site.xml.in | 2 + etc/koha-conf.xml | 1 + etc/koha-httpd.conf | 1 + installer/data/mysql/kohastructure.sql | 11 + installer/data/mysql/sysprefs.sql | 1 + installer/data/mysql/updatedatabase.pl | 24 +++ .../intranet-tmpl/prog/en/includes/header.inc | 5 +- .../admin/preferences/enhanced_content.pref | 7 + .../prog/en/modules/plugins/plugins-home.tt | 116 +++++++++++ .../prog/en/modules/plugins/plugins-upload.tt | 55 +++++ .../prog/en/modules/reports/reports-home.tt | 11 +- .../prog/en/modules/tools/tools-home.tt | 5 + plugins/plugins-home.pl | 57 ++++++ plugins/plugins-uninstall.pl | 52 +++++ plugins/plugins-upload.pl | 98 +++++++++ plugins/run.pl | 50 +++++ rewrite-config.PL | 3 +- skel/var/lib/koha/plugins/README | 1 + 25 files changed, 929 insertions(+), 6 deletions(-) create mode 100644 Koha/Plugins.pm create mode 100644 Koha/Plugins/Base.pm create mode 100644 Koha/Plugins/Handler.pm create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-upload.tt create mode 100755 plugins/plugins-home.pl create mode 100755 plugins/plugins-uninstall.pl create mode 100755 plugins/plugins-upload.pl create mode 100755 plugins/run.pl create mode 100644 skel/var/lib/koha/plugins/README diff --git a/C4/Auth.pm b/C4/Auth.pm index 27aa39b217..2984570469 100644 --- a/C4/Auth.pm +++ b/C4/Auth.pm @@ -133,7 +133,7 @@ EOQ sub get_template_and_user { my $in = shift; my $template = - C4::Templates::gettemplate( $in->{'template_name'}, $in->{'type'}, $in->{'query'} ); + C4::Templates::gettemplate( $in->{'template_name'}, $in->{'type'}, $in->{'query'}, $in->{'is_plugin'} ); my ( $user, $cookie, $sessionID, $flags ); if ( $in->{'template_name'} !~m/maintenance/ ) { ( $user, $cookie, $sessionID, $flags ) = checkauth( @@ -202,6 +202,7 @@ sub get_template_and_user { $template->param( CAN_user_serials => 1 ); $template->param( CAN_user_reports => 1 ); $template->param( CAN_user_staffaccess => 1 ); + $template->param( CAN_user_plugins => 1 ); foreach my $module (keys %$all_perms) { foreach my $subperm (keys %{ $all_perms->{$module} }) { $template->param( "CAN_user_${module}_${subperm}" => 1 ); @@ -366,6 +367,7 @@ sub get_template_and_user { OPACLocalCoverImages => C4::Context->preference('OPACLocalCoverImages'), AllowMultipleCovers => C4::Context->preference('AllowMultipleCovers'), EnableBorrowerFiles => C4::Context->preference('EnableBorrowerFiles'), + UseKohaPlugins => C4::Context->preference('UseKohaPlugins'), ); } else { diff --git a/C4/Installer/PerlDependencies.pm b/C4/Installer/PerlDependencies.pm index 83ba962776..f240f25f20 100644 --- a/C4/Installer/PerlDependencies.pm +++ b/C4/Installer/PerlDependencies.pm @@ -634,6 +634,46 @@ our $PERL_DEPS = { 'required' => '1', 'min_ver' => '0.22', }, + 'File::Temp' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '0.22', + }, + 'File::Copy' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '2.08', + }, + 'File::Path' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '2.07', + }, + 'Archive::Extract' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '0.60', + }, + 'Archive::Zip' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '1.30', + }, + 'Module::Load::Conditional' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '0.38', + }, + 'Module::Bundled::Files' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '0.03', + }, + 'Module::Pluggable' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '3.9', + }, }; 1; diff --git a/C4/Templates.pm b/C4/Templates.pm index 78dcdddd03..88b9b01687 100644 --- a/C4/Templates.pm +++ b/C4/Templates.pm @@ -228,13 +228,15 @@ sub _get_template_file { sub gettemplate { - my ( $tmplbase, $interface, $query ) = @_; + my ( $tmplbase, $interface, $query, $is_plugin ) = @_; ($query) or warn "no query in gettemplate"; my $path = C4::Context->preference('intranet_includes') || 'includes'; $tmplbase =~ s/\.tmpl$/.tt/; my ($htdocs, $theme, $lang, $filename) = _get_template_file($tmplbase, $interface, $query); + $filename = $tmplbase if ( $is_plugin ); my $template = C4::Templates->new($interface, $filename, $tmplbase, $query); + # NOTE: Commenting these out rather than deleting them so that those who need # to know how we previously shimmed these directories will be able to understand. # my $is_intranet = $interface eq 'intranet'; diff --git a/Koha/Plugins.pm b/Koha/Plugins.pm new file mode 100644 index 0000000000..797f60012c --- /dev/null +++ b/Koha/Plugins.pm @@ -0,0 +1,87 @@ +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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Module::Load::Conditional qw(can_load); +use Module::Pluggable search_path => ['Koha::Plugin']; + +use C4::Context; +use C4::Output; + +BEGIN { + die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") ); + + push @INC, C4::Context->config("pluginsdir"); +} + +=head1 NAME + +Koha::Plugins - Module for loading and managing plugins. + +=cut + +sub new { + my ( $class, $args ) = @_; + + $args->{'pluginsdir'} = C4::Context->config("pluginsdir"); + + return bless( $args, $class ); +} + +=head2 GetPlugins() + +This will return a list of all the available plugins of the passed type. + +Usage: my @plugins = C4::Plugins::GetPlugins( $method ); + +At the moment, the available types are 'report' and 'tool'. +=cut + +sub GetPlugins { + my $self = shift; + my $method = shift; + + my @plugin_classes = $self->plugins(); + my @plugins; + + foreach my $plugin_class (@plugin_classes) { + if ( can_load( modules => { $plugin_class => undef } ) ) { + my $plugin = $plugin_class->new(); + + if ($method) { + if ( $plugin->can($method) ) { + push( @plugins, $plugin ); + } + } else { + push( @plugins, $plugin ); + } + } + } + return @plugins; +} + +1; +__END__ + +=head1 AUTHOR + +Kyle M Hall + +=cut diff --git a/Koha/Plugins/Base.pm b/Koha/Plugins/Base.pm new file mode 100644 index 0000000000..f1cff3b9cb --- /dev/null +++ b/Koha/Plugins/Base.pm @@ -0,0 +1,190 @@ +package Koha::Plugins::Base; + +# 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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Module::Pluggable require => 1; + +use base qw{Module::Bundled::Files}; + +use C4::Context; +use C4::Auth; + +BEGIN { + die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") ); + + push @INC, C4::Context->config("pluginsdir"); +} + +=head1 NAME + +C4::Plugins::Base - Base Module for plugins + +=cut + +sub new { + my ( $class, $args ) = @_; + + $args->{'class'} = $class; + $args->{'template'} = Template->new( { ABSOLUTE => 1 } ); + + my $self = bless( $args, $class ); + + ## Run the installation method if it exists and hasn't been run before + if ( $self->can('install') && !$self->retrieve_data('__INSTALLED__') ) { + if ( $self->install() ) { + $self->store_data( { '__INSTALLED__' => 1 } ); + } else { + warn "Plugin $class failed during installation!"; + } + } + + return $self; +} + +=head2 store_data + +set_data allows a plugin to store key value pairs in the database for future use. + +usage: $self->set_data({ param1 => 'param1val', param2 => 'param2value' }) + +=cut + +sub store_data { + my ( $self, $data ) = @_; + + my $dbh = C4::Context->dbh; + my $sql = "REPLACE INTO plugin_data SET plugin_class = ?, plugin_key = ?, plugin_value = ?"; + my $sth = $dbh->prepare($sql); + + foreach my $key ( keys %$data ) { + $sth->execute( $self->{'class'}, $key, $data->{$key} ); + } +} + +=head2 retrieve_data + +retrieve_data allows a plugin to read the values that were previously saved with store_data + +usage: my $value = $self->retrieve_data( $key ); + +=cut + +sub retrieve_data { + my ( $self, $key ) = @_; + + my $dbh = C4::Context->dbh; + my $sql = "SELECT plugin_value FROM plugin_data WHERE plugin_class = ? AND plugin_key = ?"; + my $sth = $dbh->prepare($sql); + $sth->execute( $self->{'class'}, $key ); + my $row = $sth->fetchrow_hashref(); + + return $row->{'plugin_value'}; +} + +=head2 get_template + +get_template returns a Template object. Eventually this will probably be calling +C4:Template, but at the moment, it does not. + +=cut + +sub get_template { + my ( $self, $args ) = @_; + + # my $template = + # C4::Templates->new( my $interface = 'intranet', my $filename = $self->mbf_path( $args->{'file'} ), my $tmplbase = '', my $query = $self->{'cgi'} ); + + my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { template_name => $self->mbf_path( $args->{'file'} ), + query => $self->{'cgi'}, + type => "intranet", + authnotrequired => 1, +# flagsrequired => { tools => '*' }, + is_plugin => 1, + } + ); + + $template->param( + CLASS => $self->{'class'}, + METHOD => $self->{'cgi'}->param('method'), + PLUGIN_PATH => $self->get_plugin_http_path(), + ); + + return $template; +} + +sub get_metadata { + my ( $self, $args ) = @_; + + return $self->{'metadata'}; +} + +=head2 get_qualified_table_name + +To avoid naming conflict, each plugins tables should use a fully qualified namespace. +To avoid hardcoding and make plugins more flexible, this method will return the proper +fully qualified table name. + +usage: my $table = $self->get_qualified_table_name( 'myTable' ); + +=cut + +sub get_qualified_table_name { + my ( $self, $table_name ) = @_; + + return lc( join( '_', split( '::', $self->{'class'} ), $table_name ) ); +} + +=head2 get_plugin_http_path + +To access a plugin's own resources ( images, js files, css files, etc... ) +a plugin will need to know what path to use in the template files. This +method returns that path. + +usage: my $path = $self->get_plugin_http_path(); + +=cut + +sub get_plugin_http_path { + my ($self) = @_; + + return "/plugin/" . join( '/', split( '::', $self->{'class'} ) ); +} + +=head2 go_home + + go_home is a quick redirect to the Koha plugins home page + +=cut + +sub go_home { + my ( $self, $params ) = @_; + + print $self->{'cgi'}->redirect("/cgi-bin/koha/plugins/plugins-home.pl"); +} + +1; +__END__ + +=head1 AUTHOR + +Kyle M Hall + +=cut diff --git a/Koha/Plugins/Handler.pm b/Koha/Plugins/Handler.pm new file mode 100644 index 0000000000..7ecdb793ef --- /dev/null +++ b/Koha/Plugins/Handler.pm @@ -0,0 +1,100 @@ +package Koha::Plugins::Handler; + +# 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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use File::Path qw(remove_tree); + +use Module::Load::Conditional qw(can_load); + +use C4::Context; + +BEGIN { + die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") ); + + push @INC, C4::Context->config("pluginsdir"); +} + +=head1 NAME + +C4::Plugins::Handler - Handler Module for running plugins + +=head1 SYNOPSIS + + Koha::Plugins::Handler->run({ class => $class, method => $method, cgi => $cgi }); + $p->run(); + +=over 2 + +=cut + +=item run + +Runs a plugin + +=cut + +sub run { + my ( $class, $args ) = @_; + my $plugin_class = $args->{'class'}; + my $plugin_method = $args->{'method'}; + my $cgi = $args->{'cgi'}; + + if ( can_load( modules => { $plugin_class => undef } ) ) { + my $plugin = $plugin_class->new( { cgi => $cgi } ); + if ( $plugin->can($plugin_method) ) { + $plugin->$plugin_method(); + } else { + warn "Plugin does not have method $plugin_method"; + } + } else { + warn "Plugin $plugin_class cannot be loaded"; + } +} + +=item delete + +Deletes a plugin + +=cut + +sub delete { + my ( $class, $args ) = @_; + my $plugin_class = $args->{'class'}; + my $plugin_dir = C4::Context->config("pluginsdir"); + my $plugin_path = "$plugin_dir/" . join( '/', split( '::', $args->{'class'} ) ); + + Koha::Plugins::Handler->run( { class => $plugin_class, method => 'uninstall' } ); + + C4::Context->dbh->do( "DELETE FROM plugin_data WHERE plugin_class = ?", undef, ($plugin_class) ); + + unlink("$plugin_path.pm"); + remove_tree($plugin_path); +} + +1; +__END__ + +=back + +=head1 AUTHOR + +Kyle M Hall + +=cut diff --git a/Makefile.PL b/Makefile.PL index 02e96890cc..b69dfad35a 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -230,6 +230,10 @@ Directory for Apache and Zebra logs produced by Koha. Directory for backup files produced by Koha. +=item PLUGINS_DIR + +Directory for external Koha plugins. + =item PAZPAR2_CONF_DIR Directory for PazPar2 configuration files. @@ -310,6 +314,7 @@ my $target_map = { './skel/var/lib/koha/zebradb/biblios/register' => { target => 'ZEBRA_DATA_DIR', trimdir => 6 }, './skel/var/lib/koha/zebradb/biblios/shadow' => { target => 'ZEBRA_DATA_DIR', trimdir => 6 }, './skel/var/lib/koha/zebradb/biblios/tmp' => { target => 'ZEBRA_DATA_DIR', trimdir => 6 }, + './skel/var/lib/koha/plugins' => { target => 'PLUGINS_DIR', trimdir => 6 }, './sms' => 'INTRANET_CGI_DIR', './suggestion' => 'INTRANET_CGI_DIR', './svc' => 'INTRANET_CGI_DIR', @@ -1235,6 +1240,7 @@ sub get_target_directories { $dirmap{'ZEBRA_LOCK_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'lock', 'zebradb'); $dirmap{'LOG_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'log'); $dirmap{'BACKUP_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'spool'); + $dirmap{'PLUGINS_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'lib', 'koha', 'plugins'); $dirmap{'ZEBRA_DATA_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'lib', 'zebradb'); $dirmap{'ZEBRA_RUN_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'run', 'zebradb'); } elsif ($mode eq 'dev') { @@ -1265,6 +1271,7 @@ sub get_target_directories { $dirmap{'ZEBRA_LOCK_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'lock', 'zebradb'); $dirmap{'LOG_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'log'); $dirmap{'BACKUP_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'spool'); + $dirmap{'PLUGINS_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'lib', 'plugins'); $dirmap{'ZEBRA_DATA_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'lib', 'zebradb'); $dirmap{'ZEBRA_RUN_DIR'} = File::Spec->catdir(@basedir, $package, 'var', 'run', 'zebradb'); } else { @@ -1287,6 +1294,7 @@ sub get_target_directories { $dirmap{'ZEBRA_LOCK_DIR'} = File::Spec->catdir(File::Spec->rootdir(), 'var', 'lock', $package, 'zebradb'); $dirmap{'LOG_DIR'} = File::Spec->catdir(File::Spec->rootdir(), 'var', 'log', $package); $dirmap{'BACKUP_DIR'} = File::Spec->catdir(File::Spec->rootdir(), 'var', 'spool', $package); + $dirmap{'PLUGINS_DIR'} = File::Spec->catdir(File::Spec->rootdir(), 'var', 'lib', $package, 'plugins'); $dirmap{'ZEBRA_DATA_DIR'} = File::Spec->catdir(File::Spec->rootdir(), 'var', 'lib', $package, 'zebradb'); $dirmap{'ZEBRA_RUN_DIR'} = File::Spec->catdir(File::Spec->rootdir(), 'var', 'run', $package, 'zebradb'); } @@ -1560,7 +1568,7 @@ make_upgrade_backup :: \t\$(NOECHO) umask 022; \$(MOD_BACKUP) \\ /; foreach my $key (qw/KOHA_CONF_DIR INTRANET_TMPL_DIR INTRANET_WWW_DIR OPAC_TMPL_DIR OPAC_WWW_DIR - PAZPAR2_CONF_DIR ZEBRA_CONF_DIR/) { + PAZPAR2_CONF_DIR ZEBRA_CONF_DIR PLUGINS_DIR/) { $upgrade .= "\t\t\$(KOHA_INST_$key) \$(KOHA_DEST_$key) \\\n" unless ($config{'INSTALL_ZEBRA'} ne "yes" and $key =~ /ZEBRA/) or exists $skip_directories->{$key} or diff --git a/debian/templates/koha-conf-site.xml.in b/debian/templates/koha-conf-site.xml.in index 2d7d12416c..f3da6ec421 100644 --- a/debian/templates/koha-conf-site.xml.in +++ b/debian/templates/koha-conf-site.xml.in @@ -257,6 +257,8 @@ 1 authorities 1 + __PLUGINS_DIR__ + 0 /usr/share/koha/intranet/cgi-bin /usr/share/koha/opac/cgi-bin/opac /usr/share/koha/opac/htdocs/opac-tmpl diff --git a/etc/koha-conf.xml b/etc/koha-conf.xml index ad7bce706c..73d67bf7eb 100644 --- a/etc/koha-conf.xml +++ b/etc/koha-conf.xml @@ -276,6 +276,7 @@ __PAZPAR2_TOGGLE_XML_POST__ 1 authorities 1 + __PLUGINS_DIR__ __INTRANET_CGI_DIR__ __OPAC_CGI_DIR__/opac __OPAC_TMPL_DIR__ diff --git a/etc/koha-httpd.conf b/etc/koha-httpd.conf index dc82d08a39..0e7af51d85 100644 --- a/etc/koha-httpd.conf +++ b/etc/koha-httpd.conf @@ -105,6 +105,7 @@ ScriptAlias /cgi-bin/koha/ "__INTRANET_CGI_DIR__/" ScriptAlias /index.html "__INTRANET_CGI_DIR__/mainpage.pl" ScriptAlias /search "__INTRANET_CGI_DIR__/search.pl" + Alias /plugin/ "__PLUGINS_DIR__/" ErrorLog __LOG_DIR__/koha-error_log # TransferLog __LOG_DIR__/koha-access.log SetEnv KOHA_CONF "__KOHA_CONF_DIR__/koha-conf.xml" diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 4872a1f491..c74c39bbc5 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -3063,6 +3063,17 @@ CREATE TABLE linktracker ( KEY dateidx (timeclicked) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- +-- Table structure for table 'plugin_data' +-- + +CREATE TABLE IF NOT EXISTS plugin_data ( + plugin_class varchar(255) NOT NULL, + plugin_key varchar(255) NOT NULL, + plugin_value text, + PRIMARY KEY (plugin_class,plugin_key) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index 0189c6b5f7..df821c0ffe 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -418,3 +418,4 @@ INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) V INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('UseQueryParser', '0', 'If enabled, try to use QueryParser for queries.', NULL, 'YesNo'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('FinesIncludeGracePeriod','1','If enabled, fines calculations will include the grace period.',NULL,'YesNo'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ('UNIMARCAuthorsFacetsSeparator',', ', 'UNIMARC authors facets separator', NULL, 'short'); +INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('UseKohaPlugins','1','Enable or disable the ability to use Koha Plugins.','','YesNo'); diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl index d6c18d1864..6c576fa540 100755 --- a/installer/data/mysql/updatedatabase.pl +++ b/installer/data/mysql/updatedatabase.pl @@ -6631,6 +6631,30 @@ if ( CheckVersion($DBversion) ) { } +$DBversion = "3.11.00.XXX"; +if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { + $dbh->do("INSERT INTO userflags (bit, flag, flagdesc, defaulton) VALUES ('19', 'plugins', 'Koha plugins', '0')"); + $dbh->do("INSERT INTO permissions (module_bit, code, description) VALUES + ('19', 'manage', 'Manage plugins ( install / uninstall )'), + ('19', 'tool', 'Use tool plugins'), + ('19', 'report', 'Use report plugins'), + ('19', 'configure', 'Configure plugins') + "); + $dbh->do("INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('UseKohaPlugins','1','Enable or disable the ability to use Koha Plugins.','','YesNo')"); + + $dbh->do(" + CREATE TABLE IF NOT EXISTS plugin_data ( + plugin_class varchar(255) NOT NULL, + plugin_key varchar(255) NOT NULL, + plugin_value text, + PRIMARY KEY (plugin_class,plugin_key) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + "); + + print "Upgrade to $DBversion done (Bug 7804: Added plugin system.)\n"; + SetVersion($DBversion); +} + =head1 FUNCTIONS =head2 TableExists($table) diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/header.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/header.inc index f4ab3f2d96..d75b36e131 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/header.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/header.inc @@ -30,6 +30,9 @@ [% IF ( CAN_user_tools ) %]
  • Tools
  • [% END %] + [% IF ( UseKohaPlugins && CAN_user_plugins ) %] +
  • Plugins
  • + [% END %] [% IF ( CAN_user_parameters ) %]
  • Administration
  • [% END %] @@ -93,4 +96,4 @@ [% IF ( intranetbookbag ) %]
    Your cart is empty.
    [% END %] - \ No newline at end of file + diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/enhanced_content.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/enhanced_content.pref index 74e6553c14..ba6a6acf96 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/enhanced_content.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/enhanced_content.pref @@ -319,3 +319,10 @@ Enhanced Content: - pref: HTML5MediaExtensions class: multi - (separated with |). + Plugins: + - + - pref: UseKohaPlugins + choices: + yes: Enable + no: "Don't enable" + - the ability to use Koha Plugins. Note, the plugin system must also be enabled in the Koha configuration file to be fully enabled. diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt new file mode 100644 index 0000000000..7f9a5814b7 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt @@ -0,0 +1,116 @@ +[% USE KohaDates %] +[% INCLUDE 'doc-head-open.inc' %] +Koha › Tools › Plugins +[% INCLUDE 'doc-head-close.inc' %] +[% INCLUDE 'calendar.inc' %] + + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'circ-search.inc' %] + + + +
    +
    +
    +
    +
    +

    Plugins

    + + [% UNLESS ( plugins ) %] + [% UNLESS ( method ) %] +

    No plugins installed

    + [% ELSE %] + [% IF method == 'tool' %] +

    No plugins that can be used as a tool are installed

    + [% ELSIF method == 'report' %] +

    No plugins that can create a report are installed

    + [% ELSE %] +

    Unknown plugin type [% method %] + [% END %] + [% END %] + [% ELSE %] + + + + + + + + + + + [% IF ( CAN_user_plugins_configure ) %][% END %] + [% IF ( CAN_user_plugins_manage ) %][% END %] + + + [% FOREACH plugin IN plugins %] + + + + + + + + + + [% IF ( CAN_user_plugins_configure ) %] + + [% END %] + [% IF ( CAN_user_plugins_manage ) %] + + [% END %] + [% END %] +
    Name DescriptionAuthorPlugin VersionMinimum Koha VersionMaximum Koha VersionLast UpdatedConfigureUninstall
    [% plugin.metadata.name %] + [% IF ( CAN_user_plugins_report ) %] + [% IF plugin.can('report') %] +

    Run report

    + [% END %] + [% END %] + + [% IF ( CAN_user_plugins_tool ) %] + [% IF plugin.can('tool') %] +

    Run tool

    + [% END %] + [% END %] +
    + [% plugin.metadata.description %] + + [% IF ( plugin.metadata.minimum_version && koha_version < plugin.metadata.minimum_version ) %] +
    Warning: This report was written for a newer version of Koha. Run at your own risk.
    + [% END %] + + [% IF ( plugin.metadata.maximum_version && koha_version > plugin.metadata.maximum_version ) %] +
    Warning: This report was written for an older version of Koha. Run at your own risk.
    + [% END %] +
    [% plugin.metadata.author %][% plugin.metadata.version %][% plugin.metadata.minimum_version %][% plugin.metadata.maximum_version %][% plugin.metadata.date_updated | $KohaDates %] + [% IF plugin.can('configure') %] + Configure + [% END %] + + [% IF plugin.can('uninstall') %] + Uninstall + [% END %] +
    + [% END %] +

    +
    +
    + +
    + +
    +
    +
    + + +[% INCLUDE 'intranet-bottom.inc' %] 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 new file mode 100644 index 0000000000..94d33737ea --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-upload.tt @@ -0,0 +1,55 @@ +[% INCLUDE 'doc-head-open.inc' %] +Koha › Tools › Plugins › Upload Plugin + +[% INCLUDE 'doc-head-close.inc' %] +[% INCLUDE 'calendar.inc' %] + + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'circ-search.inc' %] + + + +
    +
    +
    +
    +
    +
    +

    Upload Koha Plugin

    + [% IF ( ERRORS ) %] +
    + [% FOREACH ERROR IN ERRORS %] + [% IF ( ERROR.NOTKPZ ) %]
  • The upload file does not appear to be a kpz file. The extention is not '.kpz'.
  • + [% ELSIF ( ERROR.NOWRITETEMP ) %]
  • This script is not able to create/write to the necessary temporary directory.
  • + [% ELSIF ( ERROR.EMPTYUPLOAD ) %]
  • The upload file appears to be empty.
  • + [% ELSIF ( ERROR.UZIPFAIL ) %]
  • [% ERROR.UZIPFAIL %] failed to unpack.
    Please verify the integrity of the zip file and retry.
  • + [% ELSIF ( ERROR.NOWRITEPLUGINS ) %]
  • Cannot unpack file to the plugins directory.
    Please verify that the Apache user can write to the plugins directory.
  • + [% ELSE %]
  • [% ERROR.CORERR %] An unknown error has occurred.
    Please review the error log for more details.
  • [% END %] + [% END %] +
    + [% END %] +
    +
    +
    NOTE: Only KPZ file format is supported.
    +
      +
    1. + +
    2. +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/reports-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/reports-home.tt index 32c1803ebc..c7744faeae 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/reports-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/reports-home.tt @@ -35,7 +35,16 @@
  • Circulation
  • Serials
  • Holds
  • -
    + + + [% IF UseKohaPlugins %] +

    Report Plugins

    + + [% END %] + +

    Top lists

      diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt index 31124a9cca..4fff72184c 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt @@ -97,6 +97,11 @@
      Quote editor for Quote-of-the-day feature in OPAC
      [% END %] + [% IF ( UseKohaPlugins && CAN_user_plugins_tool ) %] +
      Tool Plugins
      +
      Use tool plugins
      + [% END %] +
    diff --git a/plugins/plugins-home.pl b/plugins/plugins-home.pl new file mode 100755 index 0000000000..8b7a42adbc --- /dev/null +++ b/plugins/plugins-home.pl @@ -0,0 +1,57 @@ +#!/usr/bin/perl + +# Copyright 2010 Kyle M 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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use strict; +use warnings; + +use CGI; + +use Koha::Plugins; +use C4::Auth; +use C4::Output; +use C4::Dates; +use C4::Debug; +use C4::Context; + +die("Koha plugins are disabled!") + unless C4::Context->preference('UseKohaPlugins'); + +my $input = new CGI; +my $method = $input->param('method'); + +my ( $template, $borrowernumber, $cookie ) = get_template_and_user( + { template_name => "plugins/plugins-home.tmpl", + query => $input, + type => "intranet", + authnotrequired => 0, + flagsrequired => { plugins => '*' }, + debug => 1, + } +); + +$template->param( + koha_version => C4::Context->preference("Version"), + method => $method, +); + +my @plugins = Koha::Plugins->new()->GetPlugins($method); + +$template->param( plugins => \@plugins ); + +output_html_with_http_headers( $input, $cookie, $template->output ); diff --git a/plugins/plugins-uninstall.pl b/plugins/plugins-uninstall.pl new file mode 100755 index 0000000000..63240103e7 --- /dev/null +++ b/plugins/plugins-uninstall.pl @@ -0,0 +1,52 @@ +#!/usr/bin/perl +# +# 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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use strict; +use warnings; + +use Archive::Extract; +use File::Temp; +use File::Copy; +use CGI; + +use C4::Context; +use C4::Auth; +use C4::Output; +use C4::Members; +use C4::Debug; +use Koha::Plugins::Handler; + +die("Koha plugins are disabled!") + unless C4::Context->preference('UseKohaPlugins'); + +my $input = new CGI; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { template_name => "plugins/plugins-upload.tmpl", + query => $input, + type => "intranet", + authnotrequired => 0, + flagsrequired => { plugins => 'manage' }, + debug => 1, + } +); + +my $class = $input->param('class'); + +Koha::Plugins::Handler->delete( { class => $class } ); + +print $input->redirect("/cgi-bin/koha/plugins/plugins-home.pl"); diff --git a/plugins/plugins-upload.pl b/plugins/plugins-upload.pl new file mode 100755 index 0000000000..4548104410 --- /dev/null +++ b/plugins/plugins-upload.pl @@ -0,0 +1,98 @@ +#!/usr/bin/perl +# +# 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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use strict; +use warnings; + +use Archive::Extract; +use File::Temp; +use File::Copy; +use CGI; + +use C4::Context; +use C4::Auth; +use C4::Output; +use C4::Members; +use C4::Debug; +use Koha::Plugins; + +die("Koha plugins are disabled!") + unless C4::Context->preference('UseKohaPlugins'); + +my $input = new CGI; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { template_name => "plugins/plugins-upload.tmpl", + query => $input, + type => "intranet", + authnotrequired => 0, + flagsrequired => { plugins => 'manage' }, + debug => 1, + } +); + +my $uploadfilename = $input->param('uploadfile'); +my $uploadfile = $input->upload('uploadfile'); +my $op = $input->param('op'); + +my ( $total, $handled, @counts, $tempfile, $tfh ); + +my %errors; + +if ( ( $op eq 'Upload' ) && $uploadfile ) { + my $plugins_dir = C4::Context->config("pluginsdir"); + + my $dirname = File::Temp::tempdir( CLEANUP => 1 ); + $debug and warn "dirname = $dirname"; + + my $filesuffix; + $filesuffix = $1 if $uploadfilename =~ m/(\..+)$/i; + ( $tfh, $tempfile ) = File::Temp::tempfile( SUFFIX => $filesuffix, UNLINK => 1 ); + + $debug and warn "tempfile = $tempfile"; + + $errors{'NOTKPZ'} = 1 if ( $uploadfilename !~ /\.kpz$/i ); + $errors{'NOWRITETEMP'} = 1 unless ( -w $dirname ); + $errors{'NOWRITEPLUGINS'} = 1 unless ( -w $plugins_dir ); + $errors{'EMPTYUPLOAD'} = 1 unless ( length($uploadfile) > 0 ); + + if (%errors) { + $template->param( ERRORS => [ \%errors ] ); + } else { + while (<$uploadfile>) { + print $tfh $_; + } + close $tfh; + + my $ae = Archive::Extract->new( archive => $tempfile, type => 'zip' ); + unless ( $ae->extract( to => $plugins_dir ) ) { + warn "ERROR: " . $ae->error; + $errors{'UZIPFAIL'} = $uploadfilename; + $template->param( ERRORS => [ \%errors ] ); + output_html_with_http_headers $input, $cookie, $template->output; + exit; + } + } +} elsif ( ( $op eq 'Upload' ) && !$uploadfile ) { + warn "Problem uploading file or no file uploaded."; +} + +if ( $uploadfile && !%errors && !$template->param('ERRORS') ) { + print $input->redirect("/cgi-bin/koha/plugins/plugins-home.pl"); +} else { + output_html_with_http_headers $input, $cookie, $template->output; +} diff --git a/plugins/run.pl b/plugins/run.pl new file mode 100755 index 0000000000..6b8ef28def --- /dev/null +++ b/plugins/run.pl @@ -0,0 +1,50 @@ +#!/usr/bin/perl + +# Copyright 2010 Kyle M 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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use strict; +use warnings; + +use CGI; + +use Koha::Plugins::Handler; +use C4::Auth; +use C4::Output; +use C4::Dates; +use C4::Debug; +use C4::Context; + +die("Koha plugins are disabled!") + unless C4::Context->preference('UseKohaPlugins'); + +my $cgi = new CGI; + +my $class = $cgi->param('class'); +my $method = $cgi->param('method'); + +my ( $template, $borrowernumber, $cookie ) = get_template_and_user( + { template_name => "plugins/plugins-home.tmpl", + query => $cgi, + type => "intranet", + authnotrequired => 0, + flagsrequired => { plugins => $method }, + debug => 1, + } +); + +my $plugin = Koha::Plugins::Handler->run( { class => $class, method => $method, cgi => $cgi } ); diff --git a/rewrite-config.PL b/rewrite-config.PL index 02715ab48f..90fd5ff190 100644 --- a/rewrite-config.PL +++ b/rewrite-config.PL @@ -43,7 +43,7 @@ guesses worked out by the script. The following configuration keywords are available: PREFIX, -BASE_DIR, CGI_DIR, LOG_DIR, INSTALL_BASE, +BASE_DIR, CGI_DIR, LOG_DIR, PLUGINS_DIR, INSTALL_BASE, DB_TYPE, DB_HOST, DB_PORT, DB_NAME, DB_PASS, DB_USER, WEBMASTER_EMAIL, WEBSERVER_DOMAIN, WEBSERVER_HOST, WEBSERVER_IP, WEBSERVER_PORT, WEBSERVER_PORT_LIBRARIAN, ZEBRA_PASS, ZEBRA_USER @@ -82,6 +82,7 @@ $prefix = $ENV{'INSTALL_BASE'} || "/usr"; %configuration = ( "__KOHA_INSTALLED_VERSION__" => "no_version_found", "__LOG_DIR__" => "/var/log", + "__PLUGINS_DIR__" => "/var/lib/koha/plugins", "__DB_TYPE__" => "mysql", "__DB_NAME__" => "koha", "__DB_HOST__" => $myhost, diff --git a/skel/var/lib/koha/plugins/README b/skel/var/lib/koha/plugins/README new file mode 100644 index 0000000000..d4290eaac0 --- /dev/null +++ b/skel/var/lib/koha/plugins/README @@ -0,0 +1 @@ +plugins dir \ No newline at end of file From 74ea6209bc2adc26058d7f0a0ba2170e1ee4ad2e Mon Sep 17 00:00:00 2001 From: Kyle M Hall Date: Mon, 14 Jan 2013 08:23:46 -0500 Subject: [PATCH 2/6] Bug 7804 - Add Koha Plugin System - Unit Tests Tests Ok ~/kohaclone$ perl t/db_dependent/Plugins.t 1..15 ok 1 - use Koha::Plugins; ok 2 - use Koha::Plugins::Handler; ok 3 - use Koha::Plugins::Base; ok 4 - use Koha::Plugin::Test; ok 5 - Test can_load ok 6 - Test plugin class isa Koha::Plugin::Test ok 7 - Test plugin parent class isa Koha::Plugins::Base ok 8 - Test plugin can report ok 9 - Test plugin can tool ok 10 - Test plugin can configure ok 11 - Test plugin can install ok 12 - Test plugin can install ok 13 - Test $plugin->get_metadata() ok 14 - Test $plugin->get_qualified_table_name() ok 15 - Test $plugin->get_plugin_http_path() (and all others as well) Signed-off-by: Bernardo Gonzalez Kriegel Signed-off-by: Jonathan Druart Signed-off-by: Kyle M Hall Signed-off-by: Jared Camins-Esakov --- Koha/Plugins.pm | 4 +-- Koha/Plugins/Base.pm | 4 +-- Koha/Plugins/Handler.pm | 5 ++-- t/Koha/Plugin/Test.pm | 53 ++++++++++++++++++++++++++++++++++++++++ t/db_dependent/Plugins.t | 39 +++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 t/Koha/Plugin/Test.pm create mode 100755 t/db_dependent/Plugins.t diff --git a/Koha/Plugins.pm b/Koha/Plugins.pm index 797f60012c..bc4a3bbcd8 100644 --- a/Koha/Plugins.pm +++ b/Koha/Plugins.pm @@ -26,8 +26,6 @@ use C4::Context; use C4::Output; BEGIN { - die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") ); - push @INC, C4::Context->config("pluginsdir"); } @@ -40,6 +38,8 @@ Koha::Plugins - Module for loading and managing plugins. sub new { my ( $class, $args ) = @_; + die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} ); + $args->{'pluginsdir'} = C4::Context->config("pluginsdir"); return bless( $args, $class ); diff --git a/Koha/Plugins/Base.pm b/Koha/Plugins/Base.pm index f1cff3b9cb..0e1aedc125 100644 --- a/Koha/Plugins/Base.pm +++ b/Koha/Plugins/Base.pm @@ -27,8 +27,6 @@ use C4::Context; use C4::Auth; BEGIN { - die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") ); - push @INC, C4::Context->config("pluginsdir"); } @@ -41,6 +39,8 @@ C4::Plugins::Base - Base Module for plugins sub new { my ( $class, $args ) = @_; + die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} ); + $args->{'class'} = $class; $args->{'template'} = Template->new( { ABSOLUTE => 1 } ); diff --git a/Koha/Plugins/Handler.pm b/Koha/Plugins/Handler.pm index 7ecdb793ef..d2f324a3f8 100644 --- a/Koha/Plugins/Handler.pm +++ b/Koha/Plugins/Handler.pm @@ -26,8 +26,6 @@ use Module::Load::Conditional qw(can_load); use C4::Context; BEGIN { - die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") ); - push @INC, C4::Context->config("pluginsdir"); } @@ -52,6 +50,9 @@ Runs a plugin sub run { my ( $class, $args ) = @_; + + die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} ); + my $plugin_class = $args->{'class'}; my $plugin_method = $args->{'method'}; my $cgi = $args->{'cgi'}; diff --git a/t/Koha/Plugin/Test.pm b/t/Koha/Plugin/Test.pm new file mode 100644 index 0000000000..d34a6ebf46 --- /dev/null +++ b/t/Koha/Plugin/Test.pm @@ -0,0 +1,53 @@ +package Koha::Plugin::Test; + +## It's good practive to use Modern::Perl +use Modern::Perl; + +## Required for all plugins +use base qw(Koha::Plugins::Base); + +our $VERSION = 1.01; +our $metadata = { + name => 'Test Plugin', + author => 'Kyle M Hall', + description => 'Test plugin', + date_authored => '2013-01-14', + date_updated => '2013-01-14', + minimum_version => '3.11', + maximum_version => undef, + version => $VERSION, +}; + +## This is the minimum code required for a plugin's 'new' method +## More can be added, but none should be removed +sub new { + my ( $class, $args ) = @_; + $args->{'metadata'} = $metadata; + my $self = $class->SUPER::new($args); + return $self; +} + +sub report { + my ( $self, $args ) = @_; + return 1; +} + +sub tool { + my ( $self, $args ) = @_; + return 1; +} + +sub configure { + my ( $self, $args ) = @_; + return 1; +} + +sub install { + my ( $self, $args ) = @_; + return 1; +} + +sub uninstall { + my ( $self, $args ) = @_; + return 1; +} diff --git a/t/db_dependent/Plugins.t b/t/db_dependent/Plugins.t new file mode 100755 index 0000000000..52495ea841 --- /dev/null +++ b/t/db_dependent/Plugins.t @@ -0,0 +1,39 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More tests => 15; +use File::Basename; + +use Module::Load::Conditional qw(can_load); + +use C4::Context; + +BEGIN { + push( @INC, dirname(__FILE__) . '/..' ); + + use_ok('Koha::Plugins'); + use_ok('Koha::Plugins::Handler'); + use_ok('Koha::Plugins::Base'); + use_ok('Koha::Plugin::Test'); +} + +ok( can_load( modules => { "Koha::Plugin::Test" => undef } ), 'Test can_load' ); + +my $plugin = Koha::Plugin::Test->new({ enable_plugins => 1}); + +isa_ok( $plugin, "Koha::Plugin::Test", 'Test plugin class' ); +isa_ok( $plugin, "Koha::Plugins::Base", 'Test plugin parent class' ); + +ok( $plugin->can('report'), 'Test plugin can report' ); +ok( $plugin->can('tool'), 'Test plugin can tool' ); +ok( $plugin->can('configure'), 'Test plugin can configure' ); +ok( $plugin->can('install'), 'Test plugin can install' ); +ok( $plugin->can('uninstall'), 'Test plugin can install' ); + +my $metadata = $plugin->get_metadata(); +ok( $metadata->{'name'} eq 'Test Plugin', 'Test $plugin->get_metadata()' ); + +ok( $plugin->get_qualified_table_name('mytable') eq 'koha_plugin_test_mytable', 'Test $plugin->get_qualified_table_name()' ); +ok( $plugin->get_plugin_http_path() eq '/plugin/Koha/Plugin/Test', 'Test $plugin->get_plugin_http_path()' ); \ No newline at end of file From 171542059eb6bb734ec37f561d35d108e59c37a1 Mon Sep 17 00:00:00 2001 From: Kyle M Hall Date: Fri, 8 Feb 2013 09:48:38 -0500 Subject: [PATCH 3/6] Bug 7804 - Add Koha Plugin System - QA Followup - Add flags and permissions sql Signed-off-by: Jonathan Druart Signed-off-by: Kyle M Hall Signed-off-by: Jared Camins-Esakov --- installer/data/mysql/de-DE/mandatory/userflags.sql | 1 + installer/data/mysql/de-DE/mandatory/userpermissions.sql | 5 ++++- installer/data/mysql/en/mandatory/userflags.sql | 1 + installer/data/mysql/en/mandatory/userpermissions.sql | 6 +++++- installer/data/mysql/es-ES/mandatory/userflags.sql | 1 + installer/data/mysql/es-ES/mandatory/userpermissions.sql | 5 ++++- installer/data/mysql/fr-FR/1-Obligatoire/userflags.sql | 1 + .../data/mysql/fr-FR/1-Obligatoire/userpermissions.sql | 5 ++++- installer/data/mysql/it-IT/necessari/userflags.sql | 1 + installer/data/mysql/it-IT/necessari/userpermissions.sql | 5 ++++- installer/data/mysql/nb-NO/1-Obligatorisk/userflags.sql | 1 + .../data/mysql/nb-NO/1-Obligatorisk/userpermissions.sql | 5 ++++- installer/data/mysql/pl-PL/mandatory/userflags.sql | 1 + installer/data/mysql/pl-PL/mandatory/userpermissions.sql | 5 ++++- .../mysql/ru-RU/mandatory/permissions_and_user_flags.sql | 8 ++++++-- .../mysql/uk-UA/mandatory/permissions_and_user_flags.sql | 8 ++++++-- 16 files changed, 48 insertions(+), 11 deletions(-) diff --git a/installer/data/mysql/de-DE/mandatory/userflags.sql b/installer/data/mysql/de-DE/mandatory/userflags.sql index aeccf76cf6..50b567e59a 100644 --- a/installer/data/mysql/de-DE/mandatory/userflags.sql +++ b/installer/data/mysql/de-DE/mandatory/userflags.sql @@ -15,3 +15,4 @@ INSERT INTO `userflags` VALUES(14,'editauthorities','Normdaten ändern',0); INSERT INTO `userflags` VALUES(15,'serials','Zugang auf Abonnementverwaltung/Zeitschriftenmodul',0); INSERT INTO `userflags` VALUES(16,'reports','Zugang auf Reportmodul',0); INSERT INTO `userflags` VALUES(17,'staffaccess','Berechtigungen für Bibliotheksmitarbeiter vergeben',0); +INSERT INTO `userflags` VALUES(19, 'plugins', 'Koha plugins', '0'); diff --git a/installer/data/mysql/de-DE/mandatory/userpermissions.sql b/installer/data/mysql/de-DE/mandatory/userpermissions.sql index f203a8d3f0..dc281a9bef 100644 --- a/installer/data/mysql/de-DE/mandatory/userpermissions.sql +++ b/installer/data/mysql/de-DE/mandatory/userpermissions.sql @@ -52,5 +52,8 @@ INSERT INTO permissions (module_bit, code, description) VALUES (15, 'renew_subscription', 'Abonnements verlängern'), (15, 'routing', 'Umlauflisten verwalten'), (16, 'execute_reports', 'SQL-Reports ausführen'), - (16, 'create_reports', 'SQL-Reports erstellen') + (16, 'create_reports', 'SQL-Reports erstellen'), + (19, 'tool', 'Use tool plugins'), + (19, 'report', 'Use report plugins'), + (19, 'configure', 'Configure plugins') ; diff --git a/installer/data/mysql/en/mandatory/userflags.sql b/installer/data/mysql/en/mandatory/userflags.sql index 926e53b042..c098dd1278 100644 --- a/installer/data/mysql/en/mandatory/userflags.sql +++ b/installer/data/mysql/en/mandatory/userflags.sql @@ -15,3 +15,4 @@ INSERT INTO `userflags` VALUES(14,'editauthorities','Edit Authorities',0); INSERT INTO `userflags` VALUES(15,'serials','Manage serial subscriptions',0); INSERT INTO `userflags` VALUES(16,'reports','Allow access to the reports module',0); INSERT INTO `userflags` VALUES(17,'staffaccess','Allow staff members to modify permissions for other staff members',0); +INSERT INTO `userflags` VALUES(19, 'plugins', 'Koha plugins', '0'); \ No newline at end of file diff --git a/installer/data/mysql/en/mandatory/userpermissions.sql b/installer/data/mysql/en/mandatory/userpermissions.sql index 8577ef231e..124e03046c 100644 --- a/installer/data/mysql/en/mandatory/userpermissions.sql +++ b/installer/data/mysql/en/mandatory/userpermissions.sql @@ -52,5 +52,9 @@ INSERT INTO permissions (module_bit, code, description) VALUES (15, 'renew_subscription', 'Renew a subscription'), (15, 'routing', 'Routing'), (16, 'execute_reports', 'Execute SQL reports'), - (16, 'create_reports', 'Create SQL Reports') + (16, 'create_reports', 'Create SQL Reports'), + (19, 'manage', 'Manage plugins ( install / uninstall )'), + (19, 'tool', 'Use tool plugins'), + (19, 'report', 'Use report plugins'), + (19, 'configure', 'Configure plugins') ; diff --git a/installer/data/mysql/es-ES/mandatory/userflags.sql b/installer/data/mysql/es-ES/mandatory/userflags.sql index e9d9b2e62b..85b977a02f 100644 --- a/installer/data/mysql/es-ES/mandatory/userflags.sql +++ b/installer/data/mysql/es-ES/mandatory/userflags.sql @@ -15,3 +15,4 @@ INSERT INTO `userflags` VALUES(14,'editauthorities','Allow to edit authorities', INSERT INTO `userflags` VALUES(15,'serials','Allow to manage serials subscriptions',0); INSERT INTO `userflags` VALUES(16,'reports','Allow to access to the reports module',0); INSERT INTO `userflags` VALUES(17,'staffaccess','Modify login / permissions for staff users',0); +INSERT INTO `userflags` VALUES(19, 'plugins', 'Koha plugins', '0'); diff --git a/installer/data/mysql/es-ES/mandatory/userpermissions.sql b/installer/data/mysql/es-ES/mandatory/userpermissions.sql index 8577ef231e..f731eabcbf 100644 --- a/installer/data/mysql/es-ES/mandatory/userpermissions.sql +++ b/installer/data/mysql/es-ES/mandatory/userpermissions.sql @@ -52,5 +52,8 @@ INSERT INTO permissions (module_bit, code, description) VALUES (15, 'renew_subscription', 'Renew a subscription'), (15, 'routing', 'Routing'), (16, 'execute_reports', 'Execute SQL reports'), - (16, 'create_reports', 'Create SQL Reports') + (16, 'create_reports', 'Create SQL Reports'), + (19, 'tool', 'Use tool plugins'), + (19, 'report', 'Use report plugins'), + (19, 'configure', 'Configure plugins') ; diff --git a/installer/data/mysql/fr-FR/1-Obligatoire/userflags.sql b/installer/data/mysql/fr-FR/1-Obligatoire/userflags.sql index 68fea6e610..7c3c23398a 100644 --- a/installer/data/mysql/fr-FR/1-Obligatoire/userflags.sql +++ b/installer/data/mysql/fr-FR/1-Obligatoire/userflags.sql @@ -16,3 +16,4 @@ INSERT INTO `userflags` VALUES(13,'tools','Outils (export, import, impression de INSERT INTO `userflags` VALUES(14,'editauthorities','Gestion des autorités',0); INSERT INTO `userflags` VALUES(15,'serials','Gestion du module périodique',0); INSERT INTO `userflags` VALUES(16,'reports','Accès aux statistiques',0); +INSERT INTO `userflags` VALUES(19, 'plugins', 'Koha plugins', '0'); diff --git a/installer/data/mysql/fr-FR/1-Obligatoire/userpermissions.sql b/installer/data/mysql/fr-FR/1-Obligatoire/userpermissions.sql index 3c57a81e60..8edc356714 100644 --- a/installer/data/mysql/fr-FR/1-Obligatoire/userpermissions.sql +++ b/installer/data/mysql/fr-FR/1-Obligatoire/userpermissions.sql @@ -52,6 +52,9 @@ INSERT INTO permissions (module_bit, code, description) VALUES (15, 'renew_subscription', 'Renouveler les abonnements'), (15, 'routing', 'Mettre en circulation'), (16, 'execute_reports', 'Lancer les rapports SQL'), - (16, 'create_reports', 'Créer les rapports SQL Reports') + (16, 'create_reports', 'Créer les rapports SQL Reports'), + (19, 'tool', 'Use tool plugins'), + (19, 'report', 'Use report plugins'), + (19, 'configure', 'Configure plugins') ; diff --git a/installer/data/mysql/it-IT/necessari/userflags.sql b/installer/data/mysql/it-IT/necessari/userflags.sql index eaabb50dbf..6f551adfa2 100644 --- a/installer/data/mysql/it-IT/necessari/userflags.sql +++ b/installer/data/mysql/it-IT/necessari/userflags.sql @@ -17,5 +17,6 @@ INSERT INTO `userflags` VALUES(14,'editauthorities','autorizza la modifica delle INSERT INTO `userflags` VALUES(15,'serials','autorizza la gestione degli abbonamenti ai periodici',0); INSERT INTO `userflags` VALUES(16,'reports','autorizza accesso al modulo dei reports',0); INSERT INTO `userflags` VALUES(17,'staffaccess','modifica la login o i permessi degli staff users',0); +INSERT INTO `userflags` VALUES(19, 'plugins', 'Koha plugins', '0'); SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/installer/data/mysql/it-IT/necessari/userpermissions.sql b/installer/data/mysql/it-IT/necessari/userpermissions.sql index 31e6e18c29..96746ccc0c 100644 --- a/installer/data/mysql/it-IT/necessari/userpermissions.sql +++ b/installer/data/mysql/it-IT/necessari/userpermissions.sql @@ -54,6 +54,9 @@ INSERT INTO permissions (module_bit, code, description) VALUES (15, 'renew_subscription', 'Rinnova un abbonamento'), (15, 'routing', 'Crea/Manipola liste di distribuzione dei fascicoli ( routing list)'), (16, 'execute_reports', 'Esegui reports SQL'), - (16, 'create_reports', 'Crea reports SQL') + (16, 'create_reports', 'Crea reports SQL'), + (19, 'tool', 'Use tool plugins'), + (19, 'report', 'Use report plugins'), + (19, 'configure', 'Configure plugins') ; SET FOREIGN_KEY_CHECKS=1; diff --git a/installer/data/mysql/nb-NO/1-Obligatorisk/userflags.sql b/installer/data/mysql/nb-NO/1-Obligatorisk/userflags.sql index 2e73c7f4b0..c9062fa5f1 100644 --- a/installer/data/mysql/nb-NO/1-Obligatorisk/userflags.sql +++ b/installer/data/mysql/nb-NO/1-Obligatorisk/userflags.sql @@ -36,3 +36,4 @@ INSERT INTO `userflags` VALUES(14,'editauthorities','Tilgang til å endre autori INSERT INTO `userflags` VALUES(15,'serials','Tilgang til å endre abonnementer',0); INSERT INTO `userflags` VALUES(16,'reports','Tilgang til rapportmodulen',0); INSERT INTO `userflags` VALUES(17,'staffaccess','Endre innlogging og rettigheter for bibliotekansatte',0); +INSERT INTO `userflags` VALUES(19, 'plugins', 'Koha plugins', '0'); diff --git a/installer/data/mysql/nb-NO/1-Obligatorisk/userpermissions.sql b/installer/data/mysql/nb-NO/1-Obligatorisk/userpermissions.sql index 79778656de..ba56e3a0bf 100644 --- a/installer/data/mysql/nb-NO/1-Obligatorisk/userpermissions.sql +++ b/installer/data/mysql/nb-NO/1-Obligatorisk/userpermissions.sql @@ -72,5 +72,8 @@ INSERT INTO permissions (module_bit, code, description) VALUES (15, 'renew_subscription', 'Fornye abonnementer'), (15, 'routing', 'Sirkulasjon'), (16, 'execute_reports', 'Kjøre SQL-rapporter'), - (16, 'create_reports', 'Opprette SQL-rapporter') + (16, 'create_reports', 'Opprette SQL-rapporter'), + (19, 'tool', 'Use tool plugins'), + (19, 'report', 'Use report plugins'), + (19, 'configure', 'Configure plugins') ; diff --git a/installer/data/mysql/pl-PL/mandatory/userflags.sql b/installer/data/mysql/pl-PL/mandatory/userflags.sql index e9d9b2e62b..85b977a02f 100644 --- a/installer/data/mysql/pl-PL/mandatory/userflags.sql +++ b/installer/data/mysql/pl-PL/mandatory/userflags.sql @@ -15,3 +15,4 @@ INSERT INTO `userflags` VALUES(14,'editauthorities','Allow to edit authorities', INSERT INTO `userflags` VALUES(15,'serials','Allow to manage serials subscriptions',0); INSERT INTO `userflags` VALUES(16,'reports','Allow to access to the reports module',0); INSERT INTO `userflags` VALUES(17,'staffaccess','Modify login / permissions for staff users',0); +INSERT INTO `userflags` VALUES(19, 'plugins', 'Koha plugins', '0'); diff --git a/installer/data/mysql/pl-PL/mandatory/userpermissions.sql b/installer/data/mysql/pl-PL/mandatory/userpermissions.sql index 02b25517b2..5659abadf7 100644 --- a/installer/data/mysql/pl-PL/mandatory/userpermissions.sql +++ b/installer/data/mysql/pl-PL/mandatory/userpermissions.sql @@ -53,5 +53,8 @@ INSERT INTO permissions (module_bit, code, description) VALUES (15, 'renew_subscription', 'Renew a subscription'), (15, 'routing', 'Routing'), (16, 'execute_reports', 'Execute SQL reports'), - (16, 'create_reports', 'Create SQL Reports') + (16, 'create_reports', 'Create SQL Reports'), + (19, 'tool', 'Use tool plugins'), + (19, 'report', 'Use report plugins'), + (19, 'configure', 'Configure plugins') ; diff --git a/installer/data/mysql/ru-RU/mandatory/permissions_and_user_flags.sql b/installer/data/mysql/ru-RU/mandatory/permissions_and_user_flags.sql index 868c3c03d2..a7123ba3d5 100644 --- a/installer/data/mysql/ru-RU/mandatory/permissions_and_user_flags.sql +++ b/installer/data/mysql/ru-RU/mandatory/permissions_and_user_flags.sql @@ -17,7 +17,8 @@ INSERT INTO userflags (bit, flag, flagdesc, defaulton) VALUES (14,'editauthorities', 'Разрешение на изменение авторитетных источников',0), (15,'serials', 'Разрешение на управление подпиской периодических изданий',0), (16,'reports', 'Разрешение на доступ к модулю отчетов',0), - (17,'staffaccess', 'Смена имени(логина)/привилегий для работников библиотеки',0) + (17,'staffaccess', 'Смена имени(логина)/привилегий для работников библиотеки',0), + (19,'plugins', 'Koha plugins', '0') ; TRUNCATE permissions; @@ -76,6 +77,9 @@ INSERT INTO permissions (module_bit, code, description) VALUES (15, 'renew_subscription', 'Renew a subscription'), (15, 'routing', 'Routing'), (16, 'execute_reports', 'Execute SQL reports'), - (16, 'create_reports', 'Create SQL Reports') + (16, 'create_reports', 'Create SQL Reports'), + (19, 'tool', 'Use tool plugins'), + (19, 'report', 'Use report plugins'), + (19, 'configure', 'Configure plugins') ; diff --git a/installer/data/mysql/uk-UA/mandatory/permissions_and_user_flags.sql b/installer/data/mysql/uk-UA/mandatory/permissions_and_user_flags.sql index 8d0126b399..366764c0b7 100644 --- a/installer/data/mysql/uk-UA/mandatory/permissions_and_user_flags.sql +++ b/installer/data/mysql/uk-UA/mandatory/permissions_and_user_flags.sql @@ -17,7 +17,8 @@ INSERT INTO userflags (bit, flag, flagdesc, defaulton) VALUES (14,'editauthorities', 'Дозвіл на редагування авторитетних джерел',0), (15,'serials', 'Дозвіл на керування підпискою періодичних видань',0), (16,'reports', 'Дозвіл на доступ до модуля звітів',0), - (17,'staffaccess', 'Зміна імені(логіну)/привілеїв для працівників бібліотеки',0) + (17,'staffaccess', 'Зміна імені(логіну)/привілеїв для працівників бібліотеки',0), + (19,'plugins', 'Koha plugins', '0') ; TRUNCATE permissions; @@ -76,6 +77,9 @@ INSERT INTO permissions (module_bit, code, description) VALUES (15, 'renew_subscription', 'Renew a subscription'), (15, 'routing', 'Routing'), (16, 'execute_reports', 'Execute SQL reports'), - (16, 'create_reports', 'Create SQL Reports') + (16, 'create_reports', 'Create SQL Reports'), + (19, 'tool', 'Use tool plugins'), + (19, 'report', 'Use report plugins'), + (19, 'configure', 'Configure plugins') ; From 58cc246e930cb1e9c1f45b5c6dd87302eb9ce3a8 Mon Sep 17 00:00:00 2001 From: Kyle M Hall Date: Wed, 20 Feb 2013 13:32:25 -0500 Subject: [PATCH 4/6] Bug 7804 - Add Koha Plugin System - QA Followup 2 * Add "Plugins disabled" screen instead of error * Allow plugins to return a value, add a test run that checks the return value Signed-off-by: Kyle M Hall Signed-off-by: Jared Camins-Esakov --- Koha/Plugins.pm | 2 +- Koha/Plugins/Base.pm | 2 +- Koha/Plugins/Handler.pm | 4 +- .../en/modules/plugins/plugins-disabled.tt | 30 +++++++ plugins/plugins-home.pl | 25 +++--- plugins/plugins-upload.pl | 81 ++++++++++--------- plugins/run.pl | 11 ++- t/Koha/Plugin/Test.pm | 10 +-- t/db_dependent/Plugins.t | 4 +- 9 files changed, 106 insertions(+), 63 deletions(-) create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-disabled.tt diff --git a/Koha/Plugins.pm b/Koha/Plugins.pm index bc4a3bbcd8..1360afa174 100644 --- a/Koha/Plugins.pm +++ b/Koha/Plugins.pm @@ -38,7 +38,7 @@ Koha::Plugins - Module for loading and managing plugins. sub new { my ( $class, $args ) = @_; - die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} ); + return unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} ); $args->{'pluginsdir'} = C4::Context->config("pluginsdir"); diff --git a/Koha/Plugins/Base.pm b/Koha/Plugins/Base.pm index 0e1aedc125..2681ee7076 100644 --- a/Koha/Plugins/Base.pm +++ b/Koha/Plugins/Base.pm @@ -39,7 +39,7 @@ C4::Plugins::Base - Base Module for plugins sub new { my ( $class, $args ) = @_; - die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} ); + return unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} ); $args->{'class'} = $class; $args->{'template'} = Template->new( { ABSOLUTE => 1 } ); diff --git a/Koha/Plugins/Handler.pm b/Koha/Plugins/Handler.pm index d2f324a3f8..18ceba120a 100644 --- a/Koha/Plugins/Handler.pm +++ b/Koha/Plugins/Handler.pm @@ -51,7 +51,7 @@ Runs a plugin sub run { my ( $class, $args ) = @_; - die('Plugins not enabled in config') unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} ); + return unless ( C4::Context->config("enable_plugins") || $args->{'enable_plugins'} ); my $plugin_class = $args->{'class'}; my $plugin_method = $args->{'method'}; @@ -60,7 +60,7 @@ sub run { if ( can_load( modules => { $plugin_class => undef } ) ) { my $plugin = $plugin_class->new( { cgi => $cgi } ); if ( $plugin->can($plugin_method) ) { - $plugin->$plugin_method(); + return $plugin->$plugin_method(); } else { warn "Plugin does not have method $plugin_method"; } diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-disabled.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-disabled.tt new file mode 100644 index 0000000000..4b100cd28e --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-disabled.tt @@ -0,0 +1,30 @@ +[% INCLUDE 'doc-head-open.inc' %] +Koha › Tools › Plugins › Upload Plugin + +[% INCLUDE 'doc-head-close.inc' %] +[% INCLUDE 'calendar.inc' %] + + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'circ-search.inc' %] + + + +
    +
    +
    +
    +
    +
    +

    Plugins disabled!

    + +

    To enable Koha plugins, the system preference UseKohaPlugins must be enabled, and the flag enable_plugins must be set in the Koha configuration file

    +
    +
    +
    +
    +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/plugins/plugins-home.pl b/plugins/plugins-home.pl index 8b7a42adbc..03095bb705 100755 --- a/plugins/plugins-home.pl +++ b/plugins/plugins-home.pl @@ -29,29 +29,32 @@ use C4::Dates; use C4::Debug; use C4::Context; -die("Koha plugins are disabled!") - unless C4::Context->preference('UseKohaPlugins'); +my $plugins_enabled = C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins"); my $input = new CGI; my $method = $input->param('method'); my ( $template, $borrowernumber, $cookie ) = get_template_and_user( - { template_name => "plugins/plugins-home.tmpl", - query => $input, - type => "intranet", + { template_name => ($plugins_enabled) ? "plugins/plugins-home.tt" : "plugins/plugins-disabled.tt", + query => $input, + type => "intranet", authnotrequired => 0, flagsrequired => { plugins => '*' }, debug => 1, } ); -$template->param( - koha_version => C4::Context->preference("Version"), - method => $method, -); +if ($plugins_enabled) { -my @plugins = Koha::Plugins->new()->GetPlugins($method); + $template->param( + koha_version => C4::Context->preference("Version"), + method => $method, + ); -$template->param( plugins => \@plugins ); + my @plugins = Koha::Plugins->new()->GetPlugins($method); + + $template->param( plugins => \@plugins, ); + +} output_html_with_http_headers( $input, $cookie, $template->output ); diff --git a/plugins/plugins-upload.pl b/plugins/plugins-upload.pl index 4548104410..28a647ae26 100755 --- a/plugins/plugins-upload.pl +++ b/plugins/plugins-upload.pl @@ -1,4 +1,5 @@ #!/usr/bin/perl + # # This file is part of Koha. # @@ -30,15 +31,14 @@ use C4::Members; use C4::Debug; use Koha::Plugins; -die("Koha plugins are disabled!") - unless C4::Context->preference('UseKohaPlugins'); +my $plugins_enabled = C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins"); my $input = new CGI; my ( $template, $loggedinuser, $cookie ) = get_template_and_user( - { template_name => "plugins/plugins-upload.tmpl", - query => $input, - type => "intranet", + { template_name => ($plugins_enabled) ? "plugins/plugins-upload.tmpl" : "plugins/plugins-disabled.tt", + query => $input, + type => "intranet", authnotrequired => 0, flagsrequired => { plugins => 'manage' }, debug => 1, @@ -53,46 +53,51 @@ my ( $total, $handled, @counts, $tempfile, $tfh ); my %errors; -if ( ( $op eq 'Upload' ) && $uploadfile ) { - my $plugins_dir = C4::Context->config("pluginsdir"); +if ($plugins_enabled) { + if ( ( $op eq 'Upload' ) && $uploadfile ) { + my $plugins_dir = C4::Context->config("pluginsdir"); - my $dirname = File::Temp::tempdir( CLEANUP => 1 ); - $debug and warn "dirname = $dirname"; + my $dirname = File::Temp::tempdir( CLEANUP => 1 ); + $debug and warn "dirname = $dirname"; - my $filesuffix; - $filesuffix = $1 if $uploadfilename =~ m/(\..+)$/i; - ( $tfh, $tempfile ) = File::Temp::tempfile( SUFFIX => $filesuffix, UNLINK => 1 ); + my $filesuffix; + $filesuffix = $1 if $uploadfilename =~ m/(\..+)$/i; + ( $tfh, $tempfile ) = File::Temp::tempfile( SUFFIX => $filesuffix, UNLINK => 1 ); - $debug and warn "tempfile = $tempfile"; + $debug and warn "tempfile = $tempfile"; - $errors{'NOTKPZ'} = 1 if ( $uploadfilename !~ /\.kpz$/i ); - $errors{'NOWRITETEMP'} = 1 unless ( -w $dirname ); - $errors{'NOWRITEPLUGINS'} = 1 unless ( -w $plugins_dir ); - $errors{'EMPTYUPLOAD'} = 1 unless ( length($uploadfile) > 0 ); + $errors{'NOTKPZ'} = 1 if ( $uploadfilename !~ /\.kpz$/i ); + $errors{'NOWRITETEMP'} = 1 unless ( -w $dirname ); + $errors{'NOWRITEPLUGINS'} = 1 unless ( -w $plugins_dir ); + $errors{'EMPTYUPLOAD'} = 1 unless ( length($uploadfile) > 0 ); - if (%errors) { - $template->param( ERRORS => [ \%errors ] ); - } else { - while (<$uploadfile>) { - print $tfh $_; - } - close $tfh; - - my $ae = Archive::Extract->new( archive => $tempfile, type => 'zip' ); - unless ( $ae->extract( to => $plugins_dir ) ) { - warn "ERROR: " . $ae->error; - $errors{'UZIPFAIL'} = $uploadfilename; + if (%errors) { $template->param( ERRORS => [ \%errors ] ); - output_html_with_http_headers $input, $cookie, $template->output; - exit; - } - } -} elsif ( ( $op eq 'Upload' ) && !$uploadfile ) { - warn "Problem uploading file or no file uploaded."; -} + } else { + while (<$uploadfile>) { + print $tfh $_; + } + close $tfh; + + my $ae = Archive::Extract->new( archive => $tempfile, type => 'zip' ); + unless ( $ae->extract( to => $plugins_dir ) ) { + warn "ERROR: " . $ae->error; + $errors{'UZIPFAIL'} = $uploadfilename; + $template->param( ERRORS => [ \%errors ] ); + output_html_with_http_headers $input, $cookie, $template->output; + exit; + } + } + } elsif ( ( $op eq 'Upload' ) && !$uploadfile ) { + warn "Problem uploading file or no file uploaded."; + } + + if ( $uploadfile && !%errors && !$template->param('ERRORS') ) { + print $input->redirect("/cgi-bin/koha/plugins/plugins-home.pl"); + } else { + output_html_with_http_headers $input, $cookie, $template->output; + } -if ( $uploadfile && !%errors && !$template->param('ERRORS') ) { - print $input->redirect("/cgi-bin/koha/plugins/plugins-home.pl"); } else { output_html_with_http_headers $input, $cookie, $template->output; } diff --git a/plugins/run.pl b/plugins/run.pl index 6b8ef28def..6c462ed26d 100755 --- a/plugins/run.pl +++ b/plugins/run.pl @@ -29,8 +29,7 @@ use C4::Dates; use C4::Debug; use C4::Context; -die("Koha plugins are disabled!") - unless C4::Context->preference('UseKohaPlugins'); +my $plugins_enabled = C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins"); my $cgi = new CGI; @@ -38,7 +37,7 @@ my $class = $cgi->param('class'); my $method = $cgi->param('method'); my ( $template, $borrowernumber, $cookie ) = get_template_and_user( - { template_name => "plugins/plugins-home.tmpl", + { template_name => "plugins/plugins-disabled.tmpl", query => $cgi, type => "intranet", authnotrequired => 0, @@ -47,4 +46,8 @@ my ( $template, $borrowernumber, $cookie ) = get_template_and_user( } ); -my $plugin = Koha::Plugins::Handler->run( { class => $class, method => $method, cgi => $cgi } ); +if ( $plugins_enabled ) { + my $plugin = Koha::Plugins::Handler->run( { class => $class, method => $method, cgi => $cgi } ); +} else { + output_html_with_http_headers( $cgi, $cookie, $template->output ); +} diff --git a/t/Koha/Plugin/Test.pm b/t/Koha/Plugin/Test.pm index d34a6ebf46..39d237390c 100644 --- a/t/Koha/Plugin/Test.pm +++ b/t/Koha/Plugin/Test.pm @@ -29,25 +29,25 @@ sub new { sub report { my ( $self, $args ) = @_; - return 1; + return "Koha::Plugin::Test::report"; } sub tool { my ( $self, $args ) = @_; - return 1; + return "Koha::Plugin::Test::tool"; } sub configure { my ( $self, $args ) = @_; - return 1; + return "Koha::Plugin::Test::configure";; } sub install { my ( $self, $args ) = @_; - return 1; + return "Koha::Plugin::Test::install"; } sub uninstall { my ( $self, $args ) = @_; - return 1; + return "Koha::Plugin::Test::uninstall"; } diff --git a/t/db_dependent/Plugins.t b/t/db_dependent/Plugins.t index 52495ea841..a36fe3cf83 100755 --- a/t/db_dependent/Plugins.t +++ b/t/db_dependent/Plugins.t @@ -3,7 +3,7 @@ use strict; use warnings; -use Test::More tests => 15; +use Test::More tests => 16; use File::Basename; use Module::Load::Conditional qw(can_load); @@ -32,6 +32,8 @@ ok( $plugin->can('configure'), 'Test plugin can configure' ); ok( $plugin->can('install'), 'Test plugin can install' ); ok( $plugin->can('uninstall'), 'Test plugin can install' ); +ok( Koha::Plugins::Handler->run({ class => "Koha::Plugin::Test", method => 'report' }) eq "Koha::Plugin::Test::report", 'Test run plugin report method' ); + my $metadata = $plugin->get_metadata(); ok( $metadata->{'name'} eq 'Test Plugin', 'Test $plugin->get_metadata()' ); From c332b372cab2191affe6f491173227a0ebcfe457 Mon Sep 17 00:00:00 2001 From: Kyle M Hall Date: Mon, 18 Mar 2013 09:57:09 -0700 Subject: [PATCH 5/6] Bug 7804 - Add Koha Plugin System - QA Followup 3 - Additional Unit Tests Signed-off-by: Jared Camins-Esakov --- Koha/Plugins/Base.pm | 7 ++----- t/db_dependent/KitchenSinkPlugin.kpz | Bin 0 -> 74771 bytes t/db_dependent/Plugins.t | 22 +++++++++++++++++++--- 3 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 t/db_dependent/KitchenSinkPlugin.kpz diff --git a/Koha/Plugins/Base.pm b/Koha/Plugins/Base.pm index 2681ee7076..3b0ab15747 100644 --- a/Koha/Plugins/Base.pm +++ b/Koha/Plugins/Base.pm @@ -60,9 +60,9 @@ sub new { =head2 store_data -set_data allows a plugin to store key value pairs in the database for future use. +store_data allows a plugin to store key value pairs in the database for future use. -usage: $self->set_data({ param1 => 'param1val', param2 => 'param2value' }) +usage: $self->store_data({ param1 => 'param1val', param2 => 'param2value' }) =cut @@ -108,9 +108,6 @@ C4:Template, but at the moment, it does not. sub get_template { my ( $self, $args ) = @_; - # my $template = - # C4::Templates->new( my $interface = 'intranet', my $filename = $self->mbf_path( $args->{'file'} ), my $tmplbase = '', my $query = $self->{'cgi'} ); - my ( $template, $loggedinuser, $cookie ) = get_template_and_user( { template_name => $self->mbf_path( $args->{'file'} ), query => $self->{'cgi'}, diff --git a/t/db_dependent/KitchenSinkPlugin.kpz b/t/db_dependent/KitchenSinkPlugin.kpz new file mode 100644 index 0000000000000000000000000000000000000000..97b88b6de1c34ed7df1e4ac2455108be62540aa8 GIT binary patch literal 74771 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 16; +use Test::More tests => 20; use File::Basename; - +use FindBin qw($Bin); +use Archive::Extract; use Module::Load::Conditional qw(can_load); use C4::Context; @@ -38,4 +39,19 @@ my $metadata = $plugin->get_metadata(); ok( $metadata->{'name'} eq 'Test Plugin', 'Test $plugin->get_metadata()' ); ok( $plugin->get_qualified_table_name('mytable') eq 'koha_plugin_test_mytable', 'Test $plugin->get_qualified_table_name()' ); -ok( $plugin->get_plugin_http_path() eq '/plugin/Koha/Plugin/Test', 'Test $plugin->get_plugin_http_path()' ); \ No newline at end of file +ok( $plugin->get_plugin_http_path() eq '/plugin/Koha/Plugin/Test', 'Test $plugin->get_plugin_http_path()' ); + +my @plugins = Koha::Plugins->new()->GetPlugins( 'report' ); +ok( $plugins[0]->get_metadata()->{'name'} eq 'Test Plugin', "Koha::Plugins::GetPlugins functions correctly" ); + +my $plugins_dir = C4::Context->config("pluginsdir"); +my $ae = Archive::Extract->new( archive => "$Bin/KitchenSinkPlugin.kpz", type => 'zip' ); +unless ( $ae->extract( to => $plugins_dir ) ) { + warn "ERROR: " . $ae->error; +} +use_ok('Koha::Plugin::Com::ByWaterSolutions::KitchenSink'); +$plugin = Koha::Plugin::Com::ByWaterSolutions::KitchenSink->new({ enable_plugins => 1}); + +ok( -f $plugins_dir . "/Koha/Plugin/Com/ByWaterSolutions/KitchenSink.pm", "KitchenSink plugin installed successfully" ); +Koha::Plugins::Handler->delete({ class => "Koha::Plugin::Com::ByWaterSolutions::KitchenSink" }); +ok( !( -f $plugins_dir . "/Koha/Plugin/Com/ByWaterSolutions/KitchenSink.pm" ), "Koha::Plugins::Handler::delete works correctly." ); From 0ab317559e3ef4e29ac3e192d59bff72460bb99d Mon Sep 17 00:00:00 2001 From: Jared Camins-Esakov Date: Wed, 20 Mar 2013 14:51:15 -0400 Subject: [PATCH 6/6] Bug 7804: Increment version number Database update for bug 7804 has been given version 3.11.00.106 --- installer/data/mysql/updatedatabase.pl | 2 +- kohaversion.pl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl index 6c576fa540..222428d688 100755 --- a/installer/data/mysql/updatedatabase.pl +++ b/installer/data/mysql/updatedatabase.pl @@ -6631,7 +6631,7 @@ if ( CheckVersion($DBversion) ) { } -$DBversion = "3.11.00.XXX"; +$DBversion = "3.11.00.106"; if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { $dbh->do("INSERT INTO userflags (bit, flag, flagdesc, defaulton) VALUES ('19', 'plugins', 'Koha plugins', '0')"); $dbh->do("INSERT INTO permissions (module_bit, code, description) VALUES diff --git a/kohaversion.pl b/kohaversion.pl index dce45b7ec6..4d8e9a1639 100644 --- a/kohaversion.pl +++ b/kohaversion.pl @@ -16,7 +16,7 @@ the kohaversion is divided in 4 parts : use strict; sub kohaversion { - our $VERSION = '3.11.00.105'; + our $VERSION = '3.11.00.106'; # version needs to be set this way # so that it can be picked up by Makefile.PL # during install