From 432686d85c5d2429074516734e04f58baddbf3b9 Mon Sep 17 00:00:00 2001 From: Kyle M Hall Date: Fri, 22 Jan 2010 16:34:52 +0000 Subject: [PATCH] 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 a plugin.ini file describing the plugin, as well as the perl file, template files, and any other files neccessary to make the plugin work. http://bugs.koha-community.org/show_bug.cgi?id=7804 --- C4/Auth.pm | 2 + C4/Installer/PerlDependencies.pm | 40 +++++ Koha/Plugins.pm | 96 ++++++++++++ Koha/Plugins/Base.pm | 156 ++++++++++++++++++++ Koha/Plugins/Handler.pm | 87 +++++++++++ Makefile.PL | 10 +- debian/templates/koha-conf-site.xml.in | 1 + etc/koha-conf.xml | 1 + install_misc/debian.packages | 1 + install_misc/ubuntu.12.04.packages | 1 + install_misc/ubuntu.packages | 1 + installer/data/mysql/kohastructure.sql | 11 ++ installer/data/mysql/sysprefs.sql | 2 + installer/data/mysql/updatedatabase.pl | 19 +++ .../intranet-tmpl/prog/en/includes/header.inc | 3 + .../admin/preferences/enhanced_content.pref | 7 + .../prog/en/modules/plugins/plugins-home.tt | 93 ++++++++++++ .../prog/en/modules/plugins/plugins-upload.tt | 55 +++++++ .../prog/en/modules/reports/report-plugins.tt | 54 +++++++ .../prog/en/modules/reports/reports-home.tt | 11 ++- .../prog/en/modules/tools/tool-plugins.tt | 54 +++++++ .../prog/en/modules/tools/tools-home.tt | 5 + plugins/plugins-home.pl | 51 +++++++ plugins/plugins-upload.pl | 97 ++++++++++++ plugins/run.pl | 49 ++++++ reports/report-plugins.pl | 47 ++++++ rewrite-config.PL | 3 +- skel/var/lib/koha/plugins/.htaccess | 2 + skel/var/lib/koha/plugins/README | 1 + tools/tool-plugins.pl | 47 ++++++ 30 files changed, 1004 insertions(+), 3 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 100644 koha-tmpl/intranet-tmpl/prog/en/modules/reports/report-plugins.tt create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/tool-plugins.tt create mode 100755 plugins/plugins-home.pl create mode 100755 plugins/plugins-upload.pl create mode 100755 plugins/run.pl create mode 100755 reports/report-plugins.pl create mode 100644 skel/var/lib/koha/plugins/.htaccess create mode 100644 skel/var/lib/koha/plugins/README create mode 100755 tools/tool-plugins.pl diff --git a/C4/Auth.pm b/C4/Auth.pm index 2eb63f5..b89beea 100644 --- a/C4/Auth.pm +++ b/C4/Auth.pm @@ -203,6 +203,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 ); @@ -364,6 +365,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 41fa8db..26279dc 100644 --- a/C4/Installer/PerlDependencies.pm +++ b/C4/Installer/PerlDependencies.pm @@ -634,6 +634,46 @@ our $PERL_DEPS = { 'required' => '0', 'min_ver' => '0.22', }, + 'File::Temp' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '0.22', + }, + 'File::Copy' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '2.08', + }, + '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', + }, + 'File::Find::Rule' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '0.32', + }, + 'Config::General' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '2.48', + }, + 'File::Spec' => { + 'usage' => 'Plugins', + 'required' => '0', + 'min_ver' => '3.30', + }, }; 1; diff --git a/Koha/Plugins.pm b/Koha/Plugins.pm new file mode 100644 index 0000000..88b5046 --- /dev/null +++ b/Koha/Plugins.pm @@ -0,0 +1,96 @@ +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 vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); + +use File::Find::Rule; +use Config::General qw(ParseConfig); +use Module::Load::Conditional qw(can_load); +use Module::Pluggable search_path => ['Koha::Plugin']; +use Module::Metadata; + +use C4::Context; +use C4::Output; + +BEGIN { + push @INC, C4::Context->config("pluginsdir"); +} + +=head1 NAME + +Koha::Plugins - Module for loading and managing plugins. + +=cut + +sub new { + my ( $class, $args ) = @_; + my $self = bless( {}, $class ); + + $args->{'pluginsdir'} = C4::Context->config("pluginsdir"); + + # TODO: Decide what metadata will be required ( $args->{metadata} ) and add a check for those keys + + return bless( $args, $class ); +} + +=item 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 $version = C4::Context->preference("Version"); + + 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 ( defined($method) && $plugin->can($method) ) { + push( @plugins, $plugin ); + } else { + push( @plugins, $plugin ); + } + } + } + + return @plugins; +} + +1; +__END__ + +=back + +=head1 AUTHOR + +Kyle M Hall + +=cut diff --git a/Koha/Plugins/Base.pm b/Koha/Plugins/Base.pm new file mode 100644 index 0000000..c638ae9 --- /dev/null +++ b/Koha/Plugins/Base.pm @@ -0,0 +1,156 @@ +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 Template; + +use C4::Context; + +BEGIN { + 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 ) = @_; + + return Template->new( { ABSOLUTE => 1 } ); +} + +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 + + 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__ + +=back + +=head1 AUTHOR + +Kyle M Hall + +=cut diff --git a/Koha/Plugins/Handler.pm b/Koha/Plugins/Handler.pm new file mode 100644 index 0000000..5e0f690 --- /dev/null +++ b/Koha/Plugins/Handler.pm @@ -0,0 +1,87 @@ +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 vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); + +use Module::Load::Conditional qw(can_load); + +use C4::Context; + +BEGIN { + 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 + +#sub new { +# my ( $class, %args ) = @_; +# my $self = bless( {}, $class ); +# +# $self->{'pluginsdir'} = C4::Context->config("pluginsdir"); +# $self->{'class'} = $args{'class'}; +# +# return $self; +#} + +=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 { + + #TODO: Add + } +} + +1; +__END__ + +=back + +=head1 AUTHOR + +Kyle M Hall + +=cut diff --git a/Makefile.PL b/Makefile.PL index 6c6e186..c0d5b90 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. @@ -309,6 +313,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 0858059..167b58f 100644 --- a/debian/templates/koha-conf-site.xml.in +++ b/debian/templates/koha-conf-site.xml.in @@ -257,6 +257,7 @@ 1 authorities 1 + __PLUGINS_DIR__ /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 53d7ce8..71eb05b 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/install_misc/debian.packages b/install_misc/debian.packages index 2460555..aee2cd2 100644 --- a/install_misc/debian.packages +++ b/install_misc/debian.packages @@ -137,3 +137,4 @@ make install mysql-server install yaz-doc install yaz install +libconfig-simple-perl install diff --git a/install_misc/ubuntu.12.04.packages b/install_misc/ubuntu.12.04.packages index c8e8d5f..5c60c76 100644 --- a/install_misc/ubuntu.12.04.packages +++ b/install_misc/ubuntu.12.04.packages @@ -156,3 +156,4 @@ libxml-simple-perl install libxml-xslt-perl install libyaml-perl install libyaml-syck-perl install +libconfig-simple-perl install diff --git a/install_misc/ubuntu.packages b/install_misc/ubuntu.packages index c8e8d5f..5c60c76 100644 --- a/install_misc/ubuntu.packages +++ b/install_misc/ubuntu.packages @@ -156,3 +156,4 @@ libxml-simple-perl install libxml-xslt-perl install libyaml-perl install libyaml-syck-perl install +libconfig-simple-perl install diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index ed673da..4da1fc3 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -3056,6 +3056,17 @@ CREATE TABLE IF NOT EXISTS `borrower_modifications` ( KEY `borrowernumber` (`borrowernumber`) ) 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 d5d15d7..d8993b2 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -371,6 +371,7 @@ INSERT INTO systempreferences (variable,value,options,explanation,type) VALUES ( INSERT INTO systempreferences (variable,value,explanation,type) VALUES('EnableBorrowerFiles','0','If enabled, allows librarians to upload and attach arbitrary files to a borrower record.','YesNo'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('UpdateTotalIssuesOnCirc','0','Whether to update the totalissues field in the biblio on each circ.',NULL,'YesNo'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('IntranetSlipPrinterJS','','Use this JavaScript for printing slips. Define at least function printThenClose(). For use e.g. with Firefox PlugIn jsPrintSetup, see http://jsprintsetup.mozdev.org/','','Free'); +<<<<<<< HEAD INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpacSuppressionByIPRange','','Restrict the suppression to IP adresses outside of the IP range','','free'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('PrefillItem','0','When a new item is added, should it be prefilled with last created item values?','','YesNo'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ('SubfieldsToUseWhenPrefill','','Define a list of subfields to use when prefilling items (separated by space)','','Free'); @@ -406,3 +407,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES(' INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('NotesBlacklist','','List of notes fields that should not appear in the title notes/description separator of details',NULL,'free'); INSERT INTO systempreferences (variable,value,options,explanation,type) VALUES ('SCOUserCSS', '', NULL, 'Add CSS to be included in the SCO module in an embedded