366 lines
10 KiB
Perl
366 lines
10 KiB
Perl
# Bugzilla4Intranet Extension engine
|
|
# License: Dual-license GPL 3.0+ or MPL 1.1+
|
|
# Contributor(s): Vitaliy Filippov <vitalif@mail.ru>
|
|
|
|
# See POD documentation at the end of file
|
|
|
|
package Bugzilla::Extension;
|
|
|
|
use strict;
|
|
|
|
# Don't use any more Bugzilla modules here as Bugzilla::Extension
|
|
# could be used outside of normal running Bugzilla installation
|
|
# (i.e. in checksetup.pl)
|
|
|
|
use Bugzilla::Constants;
|
|
use Bugzilla::Util;
|
|
use Bugzilla::Hook;
|
|
|
|
use Cwd qw(abs_path);
|
|
use File::Basename;
|
|
use File::Spec::Functions;
|
|
|
|
use base 'Exporter';
|
|
our @EXPORT = qw(
|
|
set_hook
|
|
add_hook
|
|
clear_hooks
|
|
extension_info
|
|
required_modules
|
|
optional_modules
|
|
extension_version
|
|
extension_include
|
|
extension_template_dir
|
|
extension_code_dir
|
|
);
|
|
|
|
my $extensions = {
|
|
# name => {
|
|
# required_modules => [],
|
|
# optional_modules => [],
|
|
# version => '',
|
|
# loaded => boolean,
|
|
# inc => [ 'path1', 'path2' ],
|
|
# }
|
|
};
|
|
|
|
sub required_modules { setter('required_modules', @_) }
|
|
sub optional_modules { setter('optional_modules', @_) }
|
|
sub extension_version { setter('version', @_) }
|
|
|
|
sub extension_code_dir
|
|
{
|
|
my ($name, $new) = @_;
|
|
my $old = setter('code_dir', $name, $new);
|
|
return $old || catfile(bz_locations()->{extensionsdir}, $name, 'code');
|
|
}
|
|
|
|
sub extension_template_dir
|
|
{
|
|
my ($name, $new) = @_;
|
|
my $old = setter('template_dir', $name, $new);
|
|
return $old || catfile(bz_locations()->{extensionsdir}, $name, 'template');
|
|
}
|
|
|
|
# Getter/setter for extension include path (@INC)
|
|
sub extension_include
|
|
{
|
|
my ($name, $new) = @_;
|
|
if ($new)
|
|
{
|
|
if (ref $new && $new !~ /ARRAY/)
|
|
{
|
|
die __PACKAGE__."::extension_include('$name', '$new'): second argument should be an arrayref";
|
|
}
|
|
$new = [ $new ] if !ref $new;
|
|
$new = [ map { abs_path($_) } @$new ];
|
|
trick_taint($_) for @$new;
|
|
}
|
|
my $old = setter('inc', $name, $new);
|
|
return $old if !$new;
|
|
# update @INC
|
|
my $oh = { map { $_ => 1 } @$old };
|
|
for (my $i = $#INC; $i >= 0; $i--)
|
|
{
|
|
splice @INC, $i, 1 if $oh->{$INC[$i]};
|
|
}
|
|
unshift @INC, @$new if $new;
|
|
return $old;
|
|
}
|
|
|
|
# Generic getter/setter
|
|
sub setter
|
|
{
|
|
my ($key, $name, $value) = @_;
|
|
$extensions->{$name} ||= {};
|
|
my $old = $extensions->{$name}->{$key};
|
|
$extensions->{$name}->{$key} = $value if defined $value;
|
|
return $old;
|
|
}
|
|
|
|
sub available
|
|
{
|
|
my $dir = bz_locations()->{extensionsdir};
|
|
my @extension_items = glob(catfile($dir, '*'));
|
|
my @r;
|
|
foreach my $item (@extension_items)
|
|
{
|
|
my $basename = basename($item);
|
|
# Skip CVS directories and any hidden files/dirs.
|
|
next if $basename eq 'CVS' or $basename =~ /^\./;
|
|
if (-d $item)
|
|
{
|
|
if (!-e catfile($item, "disabled"))
|
|
{
|
|
trick_taint($basename);
|
|
push @r, $basename;
|
|
}
|
|
}
|
|
}
|
|
return @r;
|
|
}
|
|
|
|
sub loaded
|
|
{
|
|
return grep { $extensions->{$_}->{loaded} } keys %$extensions;
|
|
}
|
|
|
|
sub extension_info
|
|
{
|
|
shift if $_[0] eq __PACKAGE__ || ref $_[0];
|
|
my ($name) = @_;
|
|
return $extensions->{$name};
|
|
}
|
|
|
|
sub load_all
|
|
{
|
|
shift if $_[0] && ($_[0] eq __PACKAGE__ || ref $_[0]);
|
|
foreach (available())
|
|
{
|
|
load($_);
|
|
}
|
|
}
|
|
|
|
sub load
|
|
{
|
|
my ($name) = @_;
|
|
if ($extensions->{$name} && $extensions->{$name}->{loaded})
|
|
{
|
|
# Extension is already loaded
|
|
return;
|
|
}
|
|
|
|
my $dir = bz_locations()->{extensionsdir};
|
|
# Add default include path
|
|
extension_include($name, catfile($dir, $name, 'lib'));
|
|
|
|
# Load main extension file
|
|
my $file = catfile($dir, $name, "$name.pl");
|
|
if (-e $file)
|
|
{
|
|
trick_taint($file);
|
|
require $file;
|
|
}
|
|
|
|
# Support for old extension system
|
|
my $code_dir = extension_code_dir($name);
|
|
if (-d $code_dir)
|
|
{
|
|
my @hooks = glob(catfile($code_dir, '*.pl'));
|
|
my ($hook, $hook_sub);
|
|
foreach my $filename (@hooks)
|
|
{
|
|
trick_taint($filename);
|
|
$hook = basename($filename);
|
|
$hook =~ s/\.pl$//so;
|
|
if (!-r $filename)
|
|
{
|
|
warn __PACKAGE__."::load(): can't read $filename, skipping";
|
|
next;
|
|
}
|
|
set_hook($name, $hook, { type => 'file', filename => $filename });
|
|
}
|
|
}
|
|
|
|
$extensions->{$name}->{loaded} = 1;
|
|
}
|
|
|
|
1;
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
Bugzilla::Extension - core of Bugzilla4Intranet Extension engine,
|
|
backwards-compatible with old pre-3.6 Bugzilla extension engine.
|
|
|
|
=head1 USAGE
|
|
|
|
Extension engine was refactored by Bugzilla authors in 3.6.
|
|
Their new version was incompatible with old extensions, had some
|
|
restrictions and was just VERY inconvenient to use.
|
|
So, in Bugzilla4Intranet 3.6, I've created my own extension engine.
|
|
|
|
=head2 Directory layout
|
|
|
|
All Bugzilla extensions must go into 'extensions' subdirectory.
|
|
The basic directory layout for an extension is as follows:
|
|
|
|
extensions/
|
|
<name>/
|
|
<name>.pl --- Main extension file
|
|
disabled --- Extension disabled if this file is present
|
|
code/ --- Directory with old-style (pre-3.6) hooks
|
|
<hook_name>.pl
|
|
lib/ --- Extension library directory (with *.pm modules)
|
|
template/ --- Directory with extension templates and template hooks
|
|
en/
|
|
default/
|
|
<template_path>/
|
|
<template_filename>.tmpl
|
|
hook/
|
|
<template_path>/
|
|
<template_filename>-<hook_name>.tmpl
|
|
|
|
=head2 Extension main
|
|
|
|
Main extension file sets extension version, required and optional Perl modules,
|
|
and can also set code hooks. It can be omitted if hooks are set using files
|
|
(see below), and there is no need for required_modules and optional_modules.
|
|
|
|
The file typically looks like:
|
|
|
|
use strict;
|
|
use Bugzilla;
|
|
use Bugzilla::Extension;
|
|
|
|
my $REQUIRED_MODULES = [];
|
|
my $OPTIONAL_MODULES = [
|
|
{
|
|
package => 'Spreadsheet-ParseExcel',
|
|
module => 'Spreadsheet::ParseExcel',
|
|
version => '0.54',
|
|
feature => 'Import of binary Excel files (*.xls)',
|
|
},
|
|
];
|
|
|
|
required_modules('<extension name>', $REQUIRED_MODULES);
|
|
optional_modules('<extension name>', $OPTIONAL_MODULES);
|
|
extension_version('<extension name>', '1.02');
|
|
|
|
clear_hooks('<extension name>');
|
|
set_hook('<extension name>', '<hook name>', 'ExtensionPackage::sub_name');
|
|
add_hook('<extension name>', '<hook name>', 'ExtensionPackage::other_sub_name');
|
|
# other hooks...
|
|
|
|
1;
|
|
__END__
|
|
|
|
Note that main file must not 'use ExtensionPackage', just because the
|
|
extension library directory can be unknown at this point. Specify the
|
|
package name in a string, and it will be loaded automatically.
|
|
|
|
=head2 Hooks
|
|
|
|
A hook is a place in the code into which other code parts can be inserted.
|
|
In Bugzilla, there are code hooks and template hooks.
|
|
Extensions should use hooks for extending the functionality. The best
|
|
is if you use predefined hooks, but you can also add your own and publish
|
|
the patch which adds this hooks somewhere on L<http://wiki.4intra.net/>.
|
|
|
|
Hook functions always get arguments through single hashref parameter ($args).
|
|
Their return value is always a boolean value: when it's TRUE, other hooks
|
|
(set after this) are also called. When a hook returns FALSE, hook processing
|
|
is stopped.
|
|
|
|
set_hook($extension, $hook_name, $callable) resets $extension's $hook_name to
|
|
$callable. add_hook(...) does not reset, but adds an additional hook with the
|
|
same name. Try to use set_hook() as much as you can, because it allows for
|
|
correct run-time extension reloading support.
|
|
|
|
Code hooks can be also set using single files inside the extension code
|
|
directory, just as it was before Bugzilla 3.6. Such files must 'use Bugzilla'
|
|
and get arguments through 'Bugzilla->hook_args'. They also don't need to
|
|
return anything - Bugzilla thinks that they always "return true".
|
|
|
|
You can get the list of all available hooks using grep on Bugzilla code:
|
|
|
|
grep -r Bugzilla::Hook::process *.cgi *.pl *.pm Bugzilla/ extensions/
|
|
|
|
You can also see the list of documented hooks in F<Bugzilla::Hook>.
|
|
|
|
=head2 Template hooks
|
|
|
|
Template hooks are just evaluated in the place of corresponding hook call.
|
|
|
|
You can get the list of all available template hooks using grep:
|
|
|
|
grep -r Hook.process template/ extensions/
|
|
|
|
=head1 METHODS FOR EXTENSIONS
|
|
|
|
First of all, add
|
|
|
|
use Bugzilla::Extension;
|
|
|
|
to the top of your extension's main file to use these methods.
|
|
|
|
=head2 $arrayref = required_modules([$new_arrayref]) / optional_modules()
|
|
|
|
Getters/setters for REQUIRED_MODULES and OPTIONAL_MODULES. Perl modules
|
|
specified here are checked by checksetup.pl during installation.
|
|
If some of required modules are missing, the installation is aborted.
|
|
If some of optional modules are missing, there is a warning.
|
|
|
|
The format of this arrayref is:
|
|
|
|
[ {
|
|
package => 'Text-CSV',
|
|
module => 'Text::CSV',
|
|
version => '1.06',
|
|
feature => 'CSV Importing of test cases'
|
|
}, ... ]
|
|
|
|
=head2 $version = extension_version([$new_version])
|
|
|
|
Getter/setter for extension version.
|
|
|
|
=head2 $dir = extension_code_dir([$new_code_dir])
|
|
|
|
Getter/setter for extension code directory, i.e. directory which contains
|
|
individual hook .pl files, as it was old Bugzillas (< 3.6).
|
|
|
|
Default value for code directory is "extensions/<name>/code/".
|
|
|
|
=head2 $dir = extension_template_dir([$new_template_dir])
|
|
|
|
Getter/setter for extension template directory. Templates from this directory
|
|
will override Bugzilla's built-in ones.
|
|
|
|
Default value for template directory is "extensions/<name>/template/".
|
|
|
|
=head1 METHODS FOR BUGZILLA (INTERNAL USAGE)
|
|
|
|
=head2 @list = Bugzilla::Extension::available()
|
|
|
|
List all available extension names
|
|
|
|
=head2 @list = Bugzilla::Extension::loaded()
|
|
|
|
List all loaded extensions
|
|
|
|
=head2 $hashref = Bugzilla::Extension::extension_info()
|
|
|
|
Get extension information hashref
|
|
|
|
=head2 Bugzilla::Extension::load_all()
|
|
|
|
Loads all enabled extensions installed into Bugzilla.
|
|
|
|
=head2 Bugzilla::Extension::load($name)
|
|
|
|
Load one extension named $name.
|
|
|
|
=head1 SEE ALSO
|
|
|
|
F<Bugzilla::Hook>
|