@@ -, +, @@
/var/lib/koha/plugins
1"
to your koha-conf.xml file
Alias /plugin/ "/var/lib/koha/plugins/"
to your koha-httpd.conf file
---
C4/Auth.pm | 4 +-
C4/Installer/PerlDependencies.pm | 40 ++++
C4/Templates.pm | 4 +-
Koha/Plugins.pm | 89 +++++++++
Koha/Plugins/Base.pm | 194 ++++++++++++++++++++
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 | 3 +
.../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 | 97 ++++++++++
plugins/run.pl | 50 +++++
rewrite-config.PL | 3 +-
skel/var/lib/koha/plugins/README | 1 +
25 files changed, 933 insertions(+), 5 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
--- a/C4/Auth.pm
+++ a/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(
@@ -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 {
--- a/C4/Installer/PerlDependencies.pm
+++ a/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',
+ },
+ '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;
--- a/C4/Templates.pm
+++ a/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';
--- a/Koha/Plugins.pm
+++ a/Koha/Plugins.pm
@@ -0,0 +1,89 @@
+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 );
+}
+
+=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 @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__
+
+=back
+
+=head1 AUTHOR
+
+Kyle M Hall
+
+=cut
--- a/Koha/Plugins/Base.pm
+++ a/Koha/Plugins/Base.pm
@@ -0,0 +1,194 @@
+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;
+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 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
--- a/Koha/Plugins/Handler.pm
+++ a/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
--- a/Makefile.PL
+++ a/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
--- a/debian/templates/koha-conf-site.xml.in
+++ a/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
--- a/etc/koha-conf.xml
+++ a/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__
--- a/etc/koha-httpd.conf
+++ a/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"
--- a/installer/data/mysql/kohastructure.sql
+++ a/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 */;
--- a/installer/data/mysql/sysprefs.sql
+++ a/installer/data/mysql/sysprefs.sql
@@ -406,3 +406,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