425 lines
14 KiB
Perl
425 lines
14 KiB
Perl
# ==================================================================
|
|
# Gossamer Threads Module Library - http://gossamer-threads.com/
|
|
#
|
|
# GT::Plugins
|
|
# Author : Alex Krohn
|
|
# CVS Info :
|
|
# $Id: Plugins.pm,v 1.55 2005/04/01 00:16:51 brewt Exp $
|
|
#
|
|
# Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
|
|
# ==================================================================
|
|
#
|
|
# Description: A plugin system for CGI scripts.
|
|
#
|
|
|
|
package GT::Plugins;
|
|
# ==================================================================
|
|
use strict;
|
|
# TODO: Eventually we want to get rid of the $ACTION global, but it would break
|
|
# rather a lot to do so.
|
|
use vars qw/$VERSION $DEBUG $ERRORS $ATTRIBS $ACTION $error @ISA $AUTOLOAD @EXPORT/;
|
|
use GT::Base;
|
|
use GT::Config;
|
|
use GT::AutoLoader;
|
|
|
|
@ISA = qw/GT::Base/;
|
|
$ERRORS = {
|
|
BADARGS => "Invalid arguments. Usage: %s",
|
|
CANTLOAD => "Unable to load plugin '%s': %s",
|
|
CANTOPEN => "Unable to open '%s': %s",
|
|
CANTDELETE => "Unable to remove plugin file '%s': %s",
|
|
CANTMOVE => "Unable to move plugin %s from '%s' to '%s': %s",
|
|
CANTREMOVE => "Unable to remove plugin file '%s': %s",
|
|
PLUGEXISTS => "The plugin '%s' already exists, unable to overwrite without confirmation",
|
|
NOINSTALL => "Unable to load install code in plugin '%s'. Missing Install.pm file.",
|
|
NOCODE => "Unable to load main code for plugin '%s' from tar file. Missing '%s.pm' file.",
|
|
NOPLUGINNAME => "Please name your plugin before calling save()",
|
|
NOPLUGIN => "There is no plugin named '%s' in the config file.",
|
|
CORRUPTCFG => "Syntax error in config file: %s",
|
|
PLUGINERR => "Error running plugin '%s' hook '%s': %s"
|
|
};
|
|
$ATTRIBS = { directory => undef, prefix => '' };
|
|
$DEBUG = 0;
|
|
$VERSION = sprintf "%d.%03d", q$Revision: 1.55 $ =~ /(\d+)\.(\d+)/;
|
|
|
|
# Actions that plugins can handle.
|
|
use constants
|
|
STOP => 1,
|
|
CONTINUE => 2,
|
|
|
|
NAME => 0,
|
|
TYPE => 1,
|
|
HOOK => 2,
|
|
ENABLED => 3;
|
|
|
|
@EXPORT = qw/STOP CONTINUE/;
|
|
|
|
sub init {
|
|
# -----------------------------------------------------------------
|
|
# Set our debug level and any extra options.
|
|
#
|
|
my $self = shift;
|
|
my @args = @_;
|
|
if (@args == 1 and not ref $args[0]) {
|
|
@args = (directory => @args);
|
|
}
|
|
|
|
$self->set(@args);
|
|
|
|
if ($self->{debug}) {
|
|
$self->{_debug} = delete $self->{debug};
|
|
}
|
|
|
|
$self->{directory} or $self->fatal(BADARGS => 'No directory passed to GT::Plugins->new()');
|
|
|
|
$self->load_cfg;
|
|
|
|
return $self;
|
|
}
|
|
|
|
sub active_plugins {
|
|
# -----------------------------------------------------------------------------
|
|
# Class/object method that returns a boolean value indicating whether or not
|
|
# the given argument (a plugin hook name) has any registered plugin hooks.
|
|
# Primarily designed for optimizations where a section of code isn't needed
|
|
# except for plugins.
|
|
#
|
|
my $self = shift;
|
|
my $config = ref $self ? $self->{config} : $self->load_cfg(shift);
|
|
|
|
my $hook_name = lc shift;
|
|
|
|
return (
|
|
exists $config->{_pre_hooks}->{$hook_name} and @{$config->{_pre_hooks}->{$hook_name}} or
|
|
exists $config->{_post_hooks}->{$hook_name} and @{$config->{_post_hooks}->{$hook_name}}
|
|
) ? 1 : undef;
|
|
}
|
|
|
|
sub dispatch {
|
|
# -----------------------------------------------------------------
|
|
# Class Method to Run plugins.
|
|
#
|
|
my $self = shift;
|
|
my $directory;
|
|
my $config = ref $self ? $self->{config} : $self->load_cfg($directory = shift);
|
|
my ($hook_name, $code, @args) = @_;
|
|
|
|
$hook_name = lc $hook_name;
|
|
|
|
# Run any pre hooks.
|
|
my @results;
|
|
my $debug = ref $self ? $self->{_debug} : $DEBUG;
|
|
|
|
if (exists $config->{_pre_hooks}->{$hook_name}) {
|
|
local $^W; no strict 'refs';
|
|
# Save our action in case plugins is called twice.
|
|
my $orig_action = $ACTION;
|
|
foreach my $hook (@{$config->{_pre_hooks}->{$hook_name}}) {
|
|
$self->debug("Plugin: pre $hook_name running => $hook") if $debug;
|
|
defined &{$hook} or $self->_load_hook($hook, 'PRE') or next;
|
|
$ACTION = CONTINUE;
|
|
@results = $hook->(@args);
|
|
if ($ACTION == STOP) {
|
|
$self->debug("Plugin pre hook $hook_name stopped further plugins.") if $debug;
|
|
last;
|
|
}
|
|
}
|
|
unless ($ACTION == STOP) {
|
|
@results = $code->(@args);
|
|
}
|
|
$ACTION = $orig_action;
|
|
}
|
|
else {
|
|
@results = $code->(@args);
|
|
}
|
|
|
|
# Run any post hooks.
|
|
if (exists $config->{_post_hooks}->{$hook_name}) {
|
|
local ($^W); no strict 'refs';
|
|
my $orig_action = $ACTION;
|
|
foreach my $hook (@{$config->{_post_hooks}->{$hook_name}}) {
|
|
$self->debug("Plugin: post $hook_name running => $hook") if $debug;
|
|
defined &{$hook} or $self->_load_hook($hook, 'POST') or next;
|
|
$ACTION = CONTINUE;
|
|
@results = $hook->(@results);
|
|
if ($ACTION == STOP) {
|
|
$self->debug("Plugin post hook $hook_name stopped further plugins.") if $debug;
|
|
last;
|
|
}
|
|
}
|
|
$ACTION = $orig_action;
|
|
}
|
|
|
|
# Must return as a list
|
|
return @results ? (@results)[0 .. $#results] : ();
|
|
}
|
|
|
|
sub dispatch_method {
|
|
# -----------------------------------------------------------------
|
|
# Class Method to Run plugins.
|
|
#
|
|
my $self = shift;
|
|
my $directory;
|
|
my $config = ref $self ? $self->{config} : $self->load_cfg($directory = shift);
|
|
my ($hook_name, $object, $method, @args) = @_;
|
|
$hook_name = lc $hook_name;
|
|
|
|
my $debug = ref $self ? $self->{_debug} : $DEBUG;
|
|
|
|
# Run any pre hooks.
|
|
my @results;
|
|
if (exists $config->{_pre_hooks}->{$hook_name}) {
|
|
local ($^W); no strict 'refs';
|
|
# Save our action in case plugins is called twice.
|
|
my $orig_action = $ACTION;
|
|
foreach my $hook (@{$config->{_pre_hooks}->{$hook_name}}) {
|
|
$self->debug("Plugin: pre $hook_name running => $hook") if $debug;
|
|
defined &{$hook} or $self->_load_hook($hook, 'PRE') or next;
|
|
$ACTION = CONTINUE;
|
|
@results = $hook->($object, @args);
|
|
$ACTION == STOP and last;
|
|
}
|
|
unless ($ACTION == STOP) {
|
|
@results = $object->$method(@args);
|
|
}
|
|
$ACTION = $orig_action;
|
|
}
|
|
else {
|
|
@results = $object->$method(@args);
|
|
}
|
|
|
|
# Run any post hooks.
|
|
if (exists $config->{_post_hooks}->{$hook_name}) {
|
|
local ($^W); no strict 'refs';
|
|
my $orig_action = $ACTION;
|
|
foreach my $hook (@{$config->{_post_hooks}->{$hook_name}}) {
|
|
$self->debug("Plugin: post $hook_name running => $hook") if $debug;
|
|
defined &{$hook} or $self->_load_hook($hook, 'POST') or next;
|
|
$ACTION = CONTINUE;
|
|
@results = $hook->($object, @results);
|
|
# If the post hook returned the object as the first return value
|
|
# that probably means it returned @_ unaltered, in which case we
|
|
# want to remove it so that @results doesn't end up with any number
|
|
# of objects stuck to the beginning of arguments/return values.
|
|
shift @results if ref $object and ref $results[0] and $object == $results[0];
|
|
|
|
$ACTION == STOP and last;
|
|
}
|
|
$ACTION = $orig_action;
|
|
}
|
|
|
|
# Must return as a list
|
|
return @results ? (@results)[0 .. $#results] : ();
|
|
}
|
|
|
|
sub load_cfg {
|
|
# -----------------------------------------------------------------
|
|
# Load the plugin config file.
|
|
#
|
|
my ($self, $directory) = @_;
|
|
$directory ||= ref $self ? $self->{directory} : '.';
|
|
|
|
my $cfg = GT::Config->load("$directory/plugin.cfg", { local => 0, inheritance => 0, create_ok => 1 });
|
|
|
|
if (!$cfg and ref $self ? $self->{_debug} : $DEBUG) {
|
|
$self->debug("Unable to load plugin config file '$directory/plugin.cfg': $GT::Config::error");
|
|
}
|
|
|
|
# Take care to delete _pre_hooks just in case the file was somehow saved
|
|
# with _pre_hooks in it.
|
|
delete $cfg->{_pre_hooks} if not $cfg->cache_hit;
|
|
|
|
# If _pre_hooks exists, the config was loaded from the cache, and the below
|
|
# has already been calculated.
|
|
unless ($cfg->{_pre_hooks}) {
|
|
$cfg->{_pre_hooks} = {};
|
|
$cfg->{_post_hooks} = {};
|
|
while (my ($plugin, $config) = each %$cfg) {
|
|
next if substr($plugin, 0, 1) eq '_' or ref $config->{hooks} ne 'ARRAY';
|
|
for my $hook (@{$config->{hooks}}) {
|
|
next unless $hook->[ENABLED] and ($hook->[TYPE] eq 'PRE' or $hook->[TYPE] eq 'POST');
|
|
push @{$cfg->{$hook->[TYPE] eq 'PRE' ? '_pre_hooks' : '_post_hooks'}->{lc $hook->[NAME]}}, $hook->[HOOK];
|
|
}
|
|
}
|
|
}
|
|
|
|
$self->{config} = $cfg if ref $self;
|
|
return $cfg;
|
|
}
|
|
|
|
$COMPILE{save_cfg} = __LINE__ . <<'END_OF_SUB';
|
|
sub save_cfg {
|
|
# -----------------------------------------------------------------
|
|
# Save the plugin cfg file. OO usage: $plugin_obj->save; Deprecated, non-OO
|
|
# usage: GT::Plugins->save_cfg($plugin_config_object); Also supported is:
|
|
# GT::Plugins->save_cfg($ignored_value, $plugin_config_object); for
|
|
# compatibility reasons. These are almost equivelant to
|
|
# $plugin_config_object->save, except that they remove the internal _pre_hooks
|
|
# and _post_hooks keys first, then restore them after saving.
|
|
#
|
|
my $self = shift;
|
|
my $config = ref $self ? $self->{config} : @_ > 1 ? $_[1] : $_[0];
|
|
|
|
my ($pre, $post) = delete @$config{qw/_pre_hooks _post_hooks/};
|
|
|
|
$config->save();
|
|
|
|
@$config{qw/_pre_hooks _post_hooks/} = ($pre, $post);
|
|
|
|
return 1;
|
|
}
|
|
END_OF_SUB
|
|
|
|
sub action {
|
|
# -------------------------------------------------------------------
|
|
# Sets the action the plugin wants.
|
|
#
|
|
$ACTION = $_[1];
|
|
}
|
|
|
|
$COMPILE{_load_hook} = __LINE__ . <<'END_OF_SUB';
|
|
sub _load_hook {
|
|
# -------------------------------------------------------------------
|
|
# Loads a module and checks for the hook.
|
|
#
|
|
my ($self, $hook, $stage) = @_;
|
|
my ($pkg) = $hook =~ /^(.*)::[^:]+$/ or return;
|
|
$pkg =~ s,::,/,g;
|
|
{
|
|
local $SIG{__DIE__};
|
|
eval { require "$pkg.pm" };
|
|
}
|
|
if ($@) {
|
|
return $self->error('PLUGINERR', 'FATAL', $stage, $hook, "$@");
|
|
}
|
|
if (! defined &{$hook}) {
|
|
return $self->error('PLUGINERR', 'FATAL', $stage, $hook, "$hook does not exist in $pkg");
|
|
}
|
|
return 1;
|
|
}
|
|
END_OF_SUB
|
|
|
|
$COMPILE{reset_env} = __LINE__ . <<'END_OF_SUB';
|
|
sub reset_env { }
|
|
END_OF_SUB
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
GT::Plugins - a plugin interface for Gossamer Threads products.
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use GT::Plugins;
|
|
$PLUGIN = GT::Plugins->new('/path/to/plugin/dir');
|
|
|
|
$PLUGIN->dispatch(hook_name => \&code_ref => @args);
|
|
$PLUGIN->dispatch_method(hook_name => $self => method => @args);
|
|
|
|
Old style, now deprecated in favour of the object approach above:
|
|
|
|
use GT::Plugins;
|
|
|
|
GT::Plugins->dispatch('/path/to/plugin/dir', hook_name => \&code_ref => @args);
|
|
GT::Plugins->dispatch_method('/path/to/plugin/dir', hook_name => $self => method => @args);
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
The plugin module supports two modes of use. The first mode involves creating
|
|
and using a GT::Plugins object upon which plugin dispatch methods may be called
|
|
to provide hooks. The second does not use the object, but instead uses class
|
|
methods with an extra argument of the plugin path preceding the other
|
|
->dispatch() arguments.
|
|
|
|
Of the two approaches, the object approach is recommended as it is a) faster,
|
|
and b) requires much less value duplication as the plugin directory needs to be
|
|
specified only once. The old, class-method-based plugin interface should be
|
|
considered deprecated, and all new code should attempt to use the object-based
|
|
system.
|
|
|
|
A dispatch with each of the two interfaces work as follows, with differences in
|
|
interfaces as noted:
|
|
|
|
=over 4
|
|
|
|
=item 1.
|
|
|
|
Loads the plugin config file. The actual file access and evaluation will be
|
|
cached, but a small amount of extra overhead is required on each dispatch.
|
|
This only applies to the deprecated class-method dispatch interface - the
|
|
preferred object interface loads the configuration file only once.
|
|
|
|
=item 2.
|
|
|
|
Runs any 'PRE' hooks registered in the config file. When using ->dispatch(),
|
|
each hook is passed the C<@args> arguments passed into ->dispatch. When using
|
|
->dispatch_method(), both the object ($self) and arguments (@args) are passed
|
|
to the hook.
|
|
|
|
Each plugin hook then has the ability to abort further plugins if desired by
|
|
calling C<$PLUGIN-E<gt>action(STOP)> (or C<GT::Plugins-E<gt>action(STOP)> for
|
|
the non-OO interface). STOP is exported by default from the GT::Plugins
|
|
module. Performing a STOP will skip both any further 'PRE' hooks and the
|
|
original function/method, and will use the hook's return value instead of the
|
|
real code's return value.
|
|
|
|
The current behaviour of 'PRE' hooks ignores the return value of any 'PRE' hook
|
|
that does not perform a STOP, however this behaviour B<may> change to use the
|
|
return value as the arguments to the next PRE hook or actual code called. As
|
|
such, it is strongly recommended to return @_ from any 'PRE' hooks.
|
|
|
|
=item 3.
|
|
|
|
Assuming C<-E<gt>action(STOP)> has not been called, the method
|
|
(->dispatch_method) or code reference (->dispatch) will be called, and its
|
|
return value stored.
|
|
|
|
=item 4.
|
|
|
|
Any registered 'POST' hooks registered in the config file will be run. When
|
|
using ->dispatch(), the list-context return value of the main code run (or, if
|
|
a 'PRE' hook called STOP, the return value of that 'PRE' hook) will be passed
|
|
in. When using ->dispatch_method(), the object is additionally passed in as
|
|
the first argument.
|
|
|
|
The list returned by the 'POST' hook will be used as arguments for any
|
|
subsequent 'POST' hooks and as the final result returned by the ->dispatch() or
|
|
->dispatch_method() call. There is one exception to this - for
|
|
->dispatch_method() 'POST' hooks, if the first argument of the return value is
|
|
the object, it will be removed; this is done to prevent a build-up of excess
|
|
objects at the beginning of the 'POST' hook arguments/return values due to
|
|
'POST' hooks simply returning @_ unaltered.
|
|
|
|
=item 5.
|
|
|
|
The return value of the final 'POST' hook, or, when no post hooks are
|
|
configured, of the actual code, is returned as the result of the ->dispatch()
|
|
call.
|
|
|
|
=back
|
|
|
|
=head1 SEE ALSO
|
|
|
|
Also included as part of the plugin system are some modules for web based tools
|
|
to manage plugins:
|
|
|
|
L<GT::Plugins::Manager> - Add, remove and edit plugin files.
|
|
|
|
L<GT::Plugins::Wizard> - Create shell plugins.
|
|
|
|
L<GT::Plugins::Installer> - Used in installing plugins.
|
|
|
|
=head1 COPYRIGHT
|
|
|
|
Copyright (c) 2005 Gossamer Threads Inc. All Rights Reserved.
|
|
http://www.gossamer-threads.com/
|
|
|
|
=head1 VERSION
|
|
|
|
Revision: $Id: Plugins.pm,v 1.55 2005/04/01 00:16:51 brewt Exp $
|
|
|
|
=cut
|