aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormkanat%bugzilla.org <>2009-11-24 06:09:41 +0000
committermkanat%bugzilla.org <>2009-11-24 06:09:41 +0000
commit5fc80f94271780b6ff6d1dbba554df35e803ac51 (patch)
treeebc3f2bc12bb32ab280cacb1cd88b35001fb2c0e
parentBug 430012: Make checksetup.pl precompile extension templates (diff)
downloadbugzilla-5fc80f94271780b6ff6d1dbba554df35e803ac51.tar.gz
bugzilla-5fc80f94271780b6ff6d1dbba554df35e803ac51.tar.bz2
bugzilla-5fc80f94271780b6ff6d1dbba554df35e803ac51.zip
Bug 430014: Re-write the code hooks system so that it uses modules instead of individual .pl files
Patch by Max Kanat-Alexander <mkanat@bugzilla.org> (module owner) a=mkanat
-rw-r--r--Bugzilla.pm47
-rw-r--r--Bugzilla/Attachment.pm2
-rw-r--r--Bugzilla/Auth/Login/Stack.pm2
-rw-r--r--Bugzilla/Auth/Verify/Stack.pm2
-rw-r--r--Bugzilla/Bug.pm10
-rw-r--r--Bugzilla/Config.pm4
-rw-r--r--Bugzilla/DB/Schema.pm2
-rw-r--r--Bugzilla/Error.pm2
-rw-r--r--Bugzilla/Extension.pm364
-rw-r--r--Bugzilla/Flag.pm2
-rw-r--r--Bugzilla/Hook.pm204
-rw-r--r--Bugzilla/Install/DB.pm2
-rw-r--r--Bugzilla/Install/Requirements.pm48
-rw-r--r--Bugzilla/Install/Util.pm122
-rw-r--r--Bugzilla/Mailer.pm2
-rw-r--r--Bugzilla/Object.pm10
-rw-r--r--Bugzilla/Search.pm2
-rw-r--r--Bugzilla/Template.pm10
-rwxr-xr-xchecksetup.pl2
-rwxr-xr-xcolchange.cgi2
-rwxr-xr-xeditparams.cgi2
-rwxr-xr-xeditproducts.cgi2
-rwxr-xr-xenter_bug.cgi2
-rw-r--r--extensions/BmpConvert/Config.pm33
-rw-r--r--extensions/BmpConvert/Extension.pm57
-rw-r--r--extensions/BmpConvert/disabled0
-rw-r--r--extensions/Example/Config.pm42
-rw-r--r--extensions/Example/Extension.pm431
-rw-r--r--extensions/Example/disabled0
-rw-r--r--extensions/Example/lib/AuthLogin.pm32
-rw-r--r--extensions/Example/lib/AuthVerify.pm31
-rw-r--r--extensions/Example/lib/ConfigExample.pm41
-rw-r--r--extensions/Example/lib/WSExample.pm32
-rw-r--r--extensions/Example/template/en/default/admin/params/example.html.tmpl28
-rw-r--r--extensions/Example/template/en/default/pages/example.html.tmpl32
-rw-r--r--extensions/Example/template/en/default/setup/strings.txt.pl24
-rw-r--r--extensions/Example/template/en/hook/admin/sanitycheck/messages-statuses.html.tmpl35
-rw-r--r--extensions/Example/template/en/hook/global/user-error-errors.html.tmpl12
-rwxr-xr-xmod_perl.pl7
-rwxr-xr-xpage.cgi2
-rwxr-xr-xpost_bug.cgi2
-rwxr-xr-xsanitycheck.cgi4
-rw-r--r--template/en/default/global/code-error.html.tmpl27
-rw-r--r--template/en/default/setup/strings.txt.pl6
44 files changed, 1470 insertions, 255 deletions
diff --git a/Bugzilla.pm b/Bugzilla.pm
index b85186e6e..9f4e81cb6 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -40,9 +40,11 @@ use Bugzilla::Constants;
use Bugzilla::Auth;
use Bugzilla::Auth::Persist::Cookie;
use Bugzilla::CGI;
+use Bugzilla::Extension;
use Bugzilla::DB;
use Bugzilla::Install::Localconfig qw(read_localconfig);
use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES);
+use Bugzilla::Install::Util;
use Bugzilla::Template;
use Bugzilla::User;
use Bugzilla::Error;
@@ -55,9 +57,6 @@ use File::Spec::Functions;
use DateTime::TimeZone;
use Safe;
-# This creates the request cache for non-mod_perl installations.
-our $_request_cache = {};
-
#####################################################################
# Constants
#####################################################################
@@ -171,8 +170,6 @@ sub init_page {
}
}
-init_page() if !$ENV{MOD_PERL};
-
#####################################################################
# Subroutines and Methods
#####################################################################
@@ -193,6 +190,27 @@ sub template_inner {
return $class->request_cache->{"template_inner_$lang"};
}
+our $extension_packages;
+sub extensions {
+ my ($class) = @_;
+ my $cache = $class->request_cache;
+ if (!$cache->{extensions}) {
+ # Under mod_perl, mod_perl.pl populates $extension_packages for us.
+ if (!$extension_packages) {
+ $extension_packages = Bugzilla::Extension->load_all();
+ }
+ my @extensions;
+ foreach my $package (@$extension_packages) {
+ my $extension = $package->new();
+ if ($extension->enabled) {
+ push(@extensions, $extension);
+ }
+ }
+ $cache->{extensions} = \@extensions;
+ }
+ return $cache->{extensions};
+}
+
sub feature {
my ($class, $feature) = @_;
my $cache = $class->request_cache;
@@ -538,12 +556,6 @@ sub has_flags {
return $class->request_cache->{has_flags};
}
-sub hook_args {
- my ($class, $args) = @_;
- $class->request_cache->{hook_args} = $args if $args;
- return $class->request_cache->{hook_args};
-}
-
sub local_timezone {
my $class = shift;
@@ -554,6 +566,12 @@ sub local_timezone {
return $class->request_cache->{local_timezone};
}
+# This creates the request cache for non-mod_perl installations.
+# This is identical to Install::Util::_cache so that things loaded
+# into Install::Util::_cache during installation can be read out
+# of request_cache later in installation.
+our $_request_cache = $Bugzilla::Install::Util::_cache;
+
sub request_cache {
if ($ENV{MOD_PERL}) {
require Apache2::RequestUtil;
@@ -586,6 +604,8 @@ sub END {
_cleanup() unless $ENV{MOD_PERL};
}
+init_page() if !$ENV{MOD_PERL};
+
1;
__END__
@@ -821,11 +841,6 @@ The current Parameters of Bugzilla, as a hashref. If C<data/params>
does not exist, then we return an empty hashref. If C<data/params>
is unreadable or is not valid perl, we C<die>.
-=item C<hook_args>
-
-If you are running inside a code hook (see L<Bugzilla::Hook>) this
-is how you get the arguments passed to the hook.
-
=item C<local_timezone>
Returns the local timezone of the Bugzilla installation,
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index 42372393c..f3210425f 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -561,7 +561,7 @@ sub _check_data {
$data = <$fh>;
}
}
- Bugzilla::Hook::process('attachment-process_data', { data => \$data,
+ Bugzilla::Hook::process('attachment_process_data', { data => \$data,
attributes => $params });
# Do not validate the size if we have a filehandle. It will be checked later.
diff --git a/Bugzilla/Auth/Login/Stack.pm b/Bugzilla/Auth/Login/Stack.pm
index a5752f22b..bef9171c9 100644
--- a/Bugzilla/Auth/Login/Stack.pm
+++ b/Bugzilla/Auth/Login/Stack.pm
@@ -35,7 +35,7 @@ sub new {
my $list = shift;
my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list);
lock_keys(%methods);
- Bugzilla::Hook::process('auth-login_methods', { modules => \%methods });
+ Bugzilla::Hook::process('auth_login_methods', { modules => \%methods });
$self->{_stack} = [];
foreach my $login_method (split(',', $list)) {
diff --git a/Bugzilla/Auth/Verify/Stack.pm b/Bugzilla/Auth/Verify/Stack.pm
index c23b532fd..2df3fcd25 100644
--- a/Bugzilla/Auth/Verify/Stack.pm
+++ b/Bugzilla/Auth/Verify/Stack.pm
@@ -30,7 +30,7 @@ sub new {
my $self = $class->SUPER::new(@_);
my %methods = map { $_ => "Bugzilla/Auth/Verify/$_.pm" } split(',', $list);
lock_keys(%methods);
- Bugzilla::Hook::process('auth-verify_methods', { modules => \%methods });
+ Bugzilla::Hook::process('auth_verify_methods', { modules => \%methods });
$self->{_stack} = [];
foreach my $verify_method (split(',', $list)) {
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index c27f23823..d8134e3cb 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -107,7 +107,7 @@ sub DB_COLUMNS {
$dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline',
@custom_names);
- Bugzilla::Hook::process("bug-columns", { columns => \@columns });
+ Bugzilla::Hook::process("bug_columns", { columns => \@columns });
return @columns;
}
@@ -543,7 +543,7 @@ sub create {
$dbh->do('INSERT INTO longdescs (' . join(',', @columns) . ")
VALUES ($qmarks)", undef, @values);
- Bugzilla::Hook::process('bug-end_of_create', { bug => $bug,
+ Bugzilla::Hook::process('bug_end_of_create', { bug => $bug,
timestamp => $timestamp,
});
@@ -613,7 +613,7 @@ sub run_create_validators {
delete $params->{lastdiffed};
delete $params->{bug_id};
- Bugzilla::Hook::process('bug-end_of_create_validators',
+ Bugzilla::Hook::process('bug_end_of_create_validators',
{ params => $params });
return $params;
@@ -870,7 +870,7 @@ sub update {
$changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef];
}
- Bugzilla::Hook::process('bug-end_of_update', { bug => $self,
+ Bugzilla::Hook::process('bug_end_of_update', { bug => $self,
timestamp => $delta_ts,
changes => $changes,
});
@@ -1779,7 +1779,7 @@ sub fields {
# Custom Fields
map { $_->name } Bugzilla->active_custom_fields
);
- Bugzilla::Hook::process("bug-fields", {'fields' => \@fields} );
+ Bugzilla::Hook::process('bug_fields', {'fields' => \@fields} );
return @fields;
}
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index cab18b5a1..ceb1861ed 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -68,7 +68,7 @@ sub _load_params {
}
# This hook is also called in editparams.cgi. This call here is required
# to make SetParam work.
- Bugzilla::Hook::process('config-modify_panels',
+ Bugzilla::Hook::process('config_modify_panels',
{ panels => \%hook_panels });
}
# END INIT CODE
@@ -84,7 +84,7 @@ sub param_panels {
$param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common';
}
# Now check for any hooked params
- Bugzilla::Hook::process('config-add_panels',
+ Bugzilla::Hook::process('config_add_panels',
{ panel_modules => $param_panels });
return $param_panels;
}
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index c5003f798..f34f05e2f 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -1635,7 +1635,7 @@ sub _initialize {
if exists $abstract_schema->{$table};
}
unlock_keys(%$abstract_schema);
- Bugzilla::Hook::process('db_schema-abstract_schema',
+ Bugzilla::Hook::process('db_schema_abstract_schema',
{ schema => $abstract_schema });
unlock_hash(%$abstract_schema);
}
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index 661c72f74..dbd9688a9 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -107,7 +107,7 @@ sub _throw_error {
# Clone the hash so we aren't modifying the constant.
my %error_map = %{ WS_ERROR_CODE() };
require Bugzilla::Hook;
- Bugzilla::Hook::process('webservice-error_codes',
+ Bugzilla::Hook::process('webservice_error_codes',
{ error_map => \%error_map });
my $code = $error_map{$error};
if (!$code) {
diff --git a/Bugzilla/Extension.pm b/Bugzilla/Extension.pm
new file mode 100644
index 000000000..1046e09ae
--- /dev/null
+++ b/Bugzilla/Extension.pm
@@ -0,0 +1,364 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developers are Copyright (C) 2009 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Extension;
+use strict;
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Install::Util qw(extension_code_files);
+
+use File::Basename qw(basename);
+
+####################
+# Subclass Methods #
+####################
+
+sub new {
+ my ($class, $params) = @_;
+ $params ||= {};
+ bless $params, $class;
+ return $params;
+}
+
+#######################################
+# Class (Bugzilla::Extension) Methods #
+#######################################
+
+sub load {
+ my ($class, $extension_file, $config_file) = @_;
+ require $config_file if $config_file;
+
+ my $package;
+ # This is needed during checksetup.pl, because Extension packages can
+ # only be loaded once (they return "1" the second time they're loaded,
+ # instead of their name). If an extension has only an Extension.pm,
+ # and no Config.pm, the Extension.pm gets loaded by
+ # Bugzilla::Install::Requirements before this load() method is ever
+ # called.
+ my $map = Bugzilla->request_cache->{extension_requirement_package_map};
+ if ($map and defined $map->{$extension_file}) {
+ $package = $map->{$extension_file};
+ }
+ else {
+ my $name = require $extension_file;
+ if ($name =~ /^\d+$/) {
+ ThrowCodeError('extension_must_return_name',
+ { extension => $extension_file, returned => $name });
+ }
+ $package = "${class}::$name";
+ }
+
+ if (!eval { $package->NAME }) {
+ ThrowCodeError('extension_no_name',
+ { filename => $extension_file, package => $package });
+ }
+
+ if (!$package->isa($class)) {
+ ThrowCodeError('extension_must_be_subclass',
+ { filename => $extension_file,
+ package => $package,
+ class => $class });
+ }
+
+ return $package;
+}
+
+sub load_all {
+ my $class = shift;
+ my $file_sets = extension_code_files();
+ my @packages;
+ foreach my $file_set (@$file_sets) {
+ my $package = $class->load(@$file_set);
+ push(@packages, $package);
+ }
+
+ return \@packages;
+}
+
+####################
+# Instance Methods #
+####################
+
+use constant enabled => 1;
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Extension - Base class for Bugzilla Extensions.
+
+=head1 SYNOPSIS
+
+The following would be in F<extensions/Foo/Extension.pm> or
+F<extensions/Foo.pm>:
+
+ package Bugzilla::Extension::Foo
+ use strict;
+ use base qw(Bugzilla::Extension);
+
+ our $VERSION = '0.02';
+ use constant NAME => 'Foo';
+
+ sub some_hook_name { ... }
+
+ __PACKAGE__->NAME;
+
+=head1 DESCRIPTION
+
+This is the base class for all Bugzilla extensions.
+
+=head1 WRITING EXTENSIONS
+
+The L</SYNOPSIS> above gives a pretty good overview of what's basically
+required to write an extension. This section gives more information
+on exactly how extensions work and how you write them.
+
+=head2 Example Extension
+
+There is a sample extension in F<extensions/Example/> that demonstrates
+most of the things described in this document, so if you find the
+documentation confusing, try just reading the code instead.
+
+=head2 Where Extension Code Goes
+
+Extension code lives under the F<extensions/> directory in Bugzilla.
+
+There are two ways to write extensions:
+
+=over
+
+=item 1
+
+If your extension will have only code and no templates or other files,
+you can create a simple C<.pm> file in the F<extensions/> directory.
+
+For example, if you wanted to create an extension called "Foo" using this
+method, you would put your code into a file called F<extensions/Foo.pm>.
+
+=item 2
+
+If you plan for your extension to have templates and other files, you
+can create a whole directory for your extension, and the main extension
+code would go into a file called F<Extension.pm> in that directory.
+
+For example, if you wanted to create an extension called "Foo" using this
+method, you would put your code into a file called
+F<extensions/Foo/Extension.pm>.
+
+=back
+
+=head2 The Extension C<NAME>.
+
+The "name" of an extension shows up in several places:
+
+=over
+
+=item 1
+
+The name of the package:
+
+C<package Bugzilla::Extension::Foo;>
+
+=item 2
+
+In a C<NAME> constant that B<must> be defined for every extension:
+
+C<< use constant NAME => 'Foo'; >>
+
+=item 3
+
+At the very end of the file:
+
+C<< __PACKAGE__->NAME; >>
+
+You'll notice that though most Perl packages end with C<1;>, Bugzilla
+Extensions must B<always> end with C<< __PACKAGE__->NAME; >>.
+
+=back
+
+The name must be identical in all of those locations.
+
+=head2 Hooks
+
+In L<Bugzilla::Hook>, there is a L<list of hooks|Bugzilla::Hook/HOOKS>.
+These are the various areas of Bugzilla that an extension can "hook" into,
+which allow your extension to perform code during that point in Bugzilla's
+execution.
+
+If your extension wants to implement a hook, all you have to do is
+write a subroutine in your hook package that has the same name as
+the hook. The subroutine will be called as a method on your extension,
+and it will get the arguments specified in the hook's documentation as
+named parameters in a hashref.
+
+For example, here's an implementation of a hook named C<foo_start>
+that gets an argument named C<bar>:
+
+ sub foo_start {
+ my ($self, $params) = @_;
+ my $bar = $params->{bar};
+ print "I got $bar!\n";
+ }
+
+And that would go into your extension's code file--the file that was
+described in the L</Where Extension Code Goes> section above.
+
+During your subroutine, you may want to know what values were passed
+as CGI arguments to the current script, or what arguments were passed to
+the current WebService method. You can get that data via
+<Bugzilla/input_params>.
+
+=head2 If Your Extension Requires Certain Perl Modules
+
+If there are certain Perl modules that your extension requires in order
+to run, there is a way you can tell Bugzilla this, and then L<checksetup>
+will make sure that those modules are installed, when you run L<checksetup>.
+
+To do this, you need to specify a constant called C<REQUIRED_MODULES>
+in your extension. This constant has the same format as
+L<Bugzilla::Install::Requirements/REQUIRED_MODULES>.
+
+If there are optional modules that add additional functionality to your
+application, you can specify them in a constant called OPTIONAL_MODULES,
+which has the same format as
+L<Bugzilla::Install::Requirements/OPTIONAL_MODULES>.
+
+=head3 If Your Extension Needs Certain Modules In Order To Compile
+
+If your extension needs a particular Perl module in order to
+I<compile>, then you have a "chicken and egg" problem--in order to
+read C<REQUIRED_MODULES>, we have to compile your extension. In order
+to compile your extension, we need to already have the modules in
+C<REQUIRED_MODULES>!
+
+To get around this problem, Bugzilla allows you to have an additional
+file, besides F<Extension.pm>, called F<Config.pm>, that contains
+just C<REQUIRED_MODULES>. If you have a F<Config.pm>, it must also
+contain the C<NAME> constant, instead of your main F<Extension.pm>
+containing the C<NAME> constant.
+
+The contents of the file would look something like this for an extension
+named C<Foo>:
+
+ package Bugzilla::Extension::Foo;
+ use strict;
+ use constant NAME => 'Foo';
+ use constant REQUIRED_MODULES => [
+ {
+ package => 'Some-Package',
+ module => 'Some::Module',
+ version => 0,
+ }
+ ];
+ __PACKAGE__->NAME;
+
+Note that it is I<not> a subclass of C<Bugzilla::Extension>, because
+at the time that module requirements are being checked in L<checksetup>,
+C<Bugzilla::Extension> cannot be loaded. Also, just like F<Extension.pm>,
+it ends with C<< __PACKAGE__->NAME; >>. Note also that it has the exact
+same C<package> name as F<Extension.pm>.
+
+This file may not use any Perl modules other than L<Bugzilla::Constants>,
+L<Bugzilla::Install::Util>, L<Bugzilla::Install::Requirements>, and
+modules that ship with Perl itself.
+
+If you want to define both C<REQUIRED_MODULES> and C<OPTIONAL_MODULES>,
+they must both be in F<Config.pm> or both in F<Extension.pm>.
+
+Every time your extension is loaded by Bugzilla, F<Config.pm> will be
+read and then F<Extension.pm> will be read, so your methods in F<Extension.pm>
+will have access to everything in F<Config.pm>. Don't define anything
+with an identical name in both files, or Perl may throw a warning that
+you are redefining things.
+
+This method of setting C<REQUIRED_MODULES> is of course not available if
+your extension is a single file named C<Foo.pm>.
+
+If any of this is confusing, just look at the code of the Example extension.
+It uses this method to specify requirements.
+
+=head2 Disabling Your Extension
+
+If you want your extension to be totally ignored by Bugzilla (it will
+not be compiled or seen to exist at all), then create a file called
+C<disabled> in your extension's directory. (If your extension is just
+a file, like F<extensions/Foo.pm>, you cannot use this method to disable
+your extension, and will just have to remove it from the directory if you
+want to totally disable it.) Note that if you are running under mod_perl,
+you may have to restart your web server for this to take effect.
+
+If you want your extension to be compiled and have L<checksetup> check
+for its module pre-requisites, but you don't want the module to be used
+by Bugzilla, then you should make your extension's L</enabled> method
+return C<0> or some false value.
+
+=head1 ADDITIONAL CONSTANTS
+
+In addition to C<NAME>, there are some other constants you might
+want to define:
+
+=head2 C<$VERSION>
+
+This should be a string that describes what version of your extension
+this is. Something like C<1.0>, C<1.3.4> or a similar string.
+
+There are no particular restrictions on the format of version numbers,
+but you should probably keep them to just numbers and periods, in the
+interest of other software that parses version numbers.
+
+By default, this will be C<undef> if you don't define it.
+
+=head1 SUBCLASS METHODS
+
+In addition to hooks, there are a few methods that your extension can
+define to modify its behavior, if you want:
+
+=head2 C<enabled>
+
+This should return C<1> if this extension's hook code should be run
+by Bugzilla, and C<0> otherwise.
+
+=head2 C<new>
+
+Once every request, this method is called on your extension in order
+to create an "instance" of it. (Extensions are treated like objects--they
+are instantiated once per request in Bugzilla, and then methods are
+called on the object.)
+
+=head1 BUGZILLA::EXTENSION CLASS METHODS
+
+These are used internally by Bugzilla to load and set up extensions.
+If you are an extension author, you don't need to care about these.
+
+=head2 C<load>
+
+Takes two arguments, the path to F<Extension.pm> and the path to F<Config.pm>,
+for an extension. Loads the extension's code packages into memory using
+C<require>, does some sanity-checking on the extension, and returns the
+package name of the loaded extension.
+
+=head2 C<load_all>
+
+Calls L</load> for every enabled extension installed into Bugzilla,
+and returns an arrayref of all the package names that were loaded.
diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm
index 8315a3ef6..2fa4c8ded 100644
--- a/Bugzilla/Flag.pm
+++ b/Bugzilla/Flag.pm
@@ -533,7 +533,7 @@ sub update_flags {
my @new_summaries = $class->snapshot($self->flags);
my @changes = $class->update_activity(\@old_summaries, \@new_summaries);
- Bugzilla::Hook::process('flag-end_of_update', { object => $self,
+ Bugzilla::Hook::process('flag_end_of_update', { object => $self,
timestamp => $timestamp,
old_flags => \@old_summaries,
new_flags => \@new_summaries,
diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm
index dc1cd6be1..24a7d73c2 100644
--- a/Bugzilla/Hook.pm
+++ b/Bugzilla/Hook.pm
@@ -18,88 +18,25 @@
# Rights Reserved.
#
# Contributor(s): Zach Lipton <zach@zachlipton.com>
-#
+# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Hook;
use strict;
-
use Bugzilla::Constants;
-use Bugzilla::Util;
-use Bugzilla::Error;
-
-use Scalar::Util qw(blessed);
-
-BEGIN {
- if ($ENV{MOD_PERL}) {
- require ModPerl::Const;
- import ModPerl::Const -compile => 'EXIT';
- }
- else {
- # Create a fake constant. We have to do this in a string eval,
- # otherwise this will always be defined.
- eval('sub ModPerl::EXIT;');
- }
-}
sub process {
my ($name, $args) = @_;
-
- # get a list of all extensions
- my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
-
- # check each extension to see if it uses the hook
- # if so, invoke the extension source file:
- foreach my $extension (@extensions) {
- # all of these variables come directly from code or directory names.
- # If there's malicious data here, we have much bigger issues to
- # worry about, so we can safely detaint them:
- trick_taint($extension);
- # Skip CVS directories and any hidden files/dirs.
- next if $extension =~ m{/CVS$} || $extension =~ m{/\.[^/]+$};
- next if -e "$extension/disabled";
- if (-e $extension.'/code/'.$name.'.pl') {
- Bugzilla->hook_args($args);
- # Allow extensions to load their own libraries.
- local @INC = ("$extension/lib", @INC);
- do($extension.'/code/'.$name.'.pl');
- if ($@) {
- if ($ENV{MOD_PERL} and blessed $@ and $@ == ModPerl::EXIT) {
- exit;
- }
- else {
- ThrowCodeError('extension_invalid',
- { errstr => $@, name => $name,
- extension => $extension });
- }
- }
- # Flush stored data.
- Bugzilla->hook_args({});
+ foreach my $extension (@{ Bugzilla->extensions }) {
+ local @INC = @INC;
+ my $ext_dir = bz_locations()->{'extensionsdir'};
+ my $ext_name = $extension->NAME;
+ unshift(@INC, "$ext_dir/$ext_name/lib");
+ if ($extension->can($name)) {
+ $extension->$name($args);
}
}
}
-sub enabled_plugins {
- my $extdir = bz_locations()->{'extensionsdir'};
- my @extensions = glob("$extdir/*");
- my %enabled;
- foreach my $extension (@extensions) {
- trick_taint($extension);
- my $extname = $extension;
- $extname =~ s{^\Q$extdir\E/}{};
- next if $extname eq 'CVS' || $extname =~ /^\./;
- next if -e "$extension/disabled";
- # Allow extensions to load their own libraries.
- local @INC = ("$extension/lib", @INC);
- $enabled{$extname} = do("$extension/info.pl");
- ThrowCodeError('extension_invalid',
- { errstr => $@, name => 'version',
- extension => $extension }) if $@;
-
- }
-
- return \%enabled;
-}
-
1;
__END__
@@ -122,39 +59,16 @@ hooks. When a piece of standard Bugzilla code wants to allow an extension
to perform additional functions, it uses Bugzilla::Hook's L</process>
subroutine to invoke any extension code if installed.
-There is a sample extension in F<extensions/example/> that demonstrates
-most of the things described in this document, as well as many of the
-hooks available.
+The implementation of extensions is described in L<Bugzilla::Extension>.
=head2 How Hooks Work
-When a hook named C<HOOK_NAME> is run, Bugzilla will attempt to invoke any
-source files named F<extensions/*/code/HOOK_NAME.pl>.
-
-So, for example, if your extension is called "testopia", and you
-want to have code run during the L</install-update_db> hook, you
-would have a file called F<extensions/testopia/code/install-update_db.pl>
-that contained perl code to run during that hook.
-
-=head2 Arguments Passed to Hooks
-
-Some L<hooks|/HOOKS> have params that are passed to them.
+When a hook named C<HOOK_NAME> is run, Bugzilla looks through all
+enabled L<extensions|Bugzilla::Extension> for extensions that implement
+a subroutined named C<HOOK_NAME>.
-These params are accessible through L<Bugzilla/hook_args>.
-That returns a hashref. Very frequently, if you want your
-hook to do anything, you have to modify these variables.
-
-You may also want to use L<Bugzilla/input_params> to get parameters
-that were passed to the current CGI script or WebService method.
-
-=head2 Versioning Extensions
-
-Every extension must have a file in its root called F<info.pl>.
-This file must return a hash when called with C<do>.
-The hash must contain a 'version' key with the current version of the
-extension. Extension authors can also add any extra infomration to this hash if
-required, by adding a new key beginning with x_ which will not be used the
-core Bugzilla code.
+See L<Bugzilla::Extension> for more details about how an extension
+can run code during a hook.
=head1 SUBROUTINES
@@ -194,7 +108,7 @@ This describes what hooks exist in Bugzilla currently. They are mostly
in alphabetical order, but some related hooks are near each other instead
of being alphabetical.
-=head2 attachment-process_data
+=head2 attachment_process_data
This happens at the very beginning process of the attachment creation.
You can edit the attachment content itself as well as all attributes
@@ -212,7 +126,7 @@ L<Bugzilla::Attachment/create>. The data it contains hasn't been checked yet.
=back
-=head2 auth-login_methods
+=head2 auth_login_methods
This allows you to add new login types to Bugzilla.
(See L<Bugzilla::Auth::Login>.)
@@ -243,13 +157,13 @@ login methods that weren't passed to L<Bugzilla::Auth/login>.)
=back
-=head2 auth-verify_methods
+=head2 auth_verify_methods
This works just like L</auth-login_methods> except it's for
login verification methods (See L<Bugzilla::Auth::Verify>.) It also
takes a C<modules> parameter, just like L</auth-login_methods>.
-=head2 bug-columns
+=head2 bug_columns
This allows you to add new fields that will show up in every L<Bugzilla::Bug>
object. Note that you will also need to use the L</bug-fields> hook in
@@ -264,7 +178,7 @@ your column name(s) onto the array.
=back
-=head2 bug-end_of_create
+=head2 bug_end_of_create
This happens at the end of L<Bugzilla::Bug/create>, after all other changes are
made to the database. This occurs inside a database transaction.
@@ -280,7 +194,7 @@ values.
=back
-=head2 bug-end_of_create_validators
+=head2 bug_end_of_create_validators
This happens during L<Bugzilla::Bug/create>, after all parameters have
been validated, but before anything has been inserted into the database.
@@ -295,7 +209,7 @@ A hashref. The validated parameters passed to C<create>.
=back
-=head2 bug-end_of_update
+=head2 bug_end_of_update
This happens at the end of L<Bugzilla::Bug/update>, after all other changes are
made to the database. This generally occurs inside a database transaction.
@@ -314,7 +228,7 @@ C<$changes-E<gt>{field} = [old, new]>
=back
-=head2 bug-fields
+=head2 bug_fields
Allows the addition of database fields from the bugs table to the standard
list of allowable fields in a L<Bugzilla::Bug> object, so that
@@ -331,7 +245,7 @@ your column name(s) onto the array.
=back
-=head2 bug-format_comment
+=head2 bug_format_comment
Allows you to do custom parsing on comments before they are displayed. You do
this by returning two regular expressions: one that matches the section you
@@ -396,7 +310,7 @@ the summary line).
=back
-=head2 buglist-columns
+=head2 buglist_columns
This happens in buglist.cgi after the standard columns have been defined and
right before the display column determination. It gives you the opportunity
@@ -424,7 +338,7 @@ The definition is structured as:
=back
-=head2 colchange-columns
+=head2 colchange_columns
This happens in F<colchange.cgi> right after the list of possible display
columns have been defined and gives you the opportunity to add additional
@@ -440,7 +354,7 @@ See L</buglist-columns>.
=back
-=head2 config-add_panels
+=head2 config_add_panels
If you want to add new panels to the Parameters administrative interface,
this is where you do it.
@@ -461,7 +375,7 @@ extension.)
=back
-=head2 config-modify_panels
+=head2 config_modify_panels
This is how you modify already-existing panels in the Parameters
administrative interface. For example, if you wanted to add a new
@@ -485,7 +399,7 @@ L</config-add_panels> if you want to add new panels.
=back
-=head2 enter_bug-entrydefaultvars
+=head2 enter_bug_entrydefaultvars
This happens right before the template is loaded on enter_bug.cgi.
@@ -497,7 +411,7 @@ Params:
=back
-=head2 flag-end_of_update
+=head2 flag_end_of_update
This happens at the end of L<Bugzilla::Flag/update_flags>, after all other changes
are made to the database and after emails are sent. It gives you a before/after
@@ -523,7 +437,7 @@ changed flags, and search for a specific condition like C<added eq 'review-'>.
=back
-=head2 install-before_final_checks
+=head2 install_before_final_checks
Allows execution of custom code before the final checks are done in
checksetup.pl.
@@ -538,37 +452,13 @@ A flag that indicates whether or not checksetup is running in silent mode.
=back
-=head2 install-requirements
-
-Because of the way Bugzilla installation works, there can't be a normal
-hook during the time that F<checksetup.pl> checks what modules are
-installed. (C<Bugzilla::Hook> needs to have those modules installed--it's
-a chicken-and-egg problem.)
-
-So instead of the way hooks normally work, this hook just looks for two
-subroutines (or constants, since all constants are just subroutines) in
-your file, called C<OPTIONAL_MODULES> and C<REQUIRED_MODULES>,
-which should return arrayrefs in the same format as C<OPTIONAL_MODULES> and
-C<REQUIRED_MODULES> in L<Bugzilla::Install::Requirements>.
-
-These subroutines will be passed an arrayref that contains the current
-Bugzilla requirements of the same type, in case you want to modify
-Bugzilla's requirements somehow. (Probably the most common would be to
-alter a version number or the "feature" element of C<OPTIONAL_MODULES>.)
-
-F<checksetup.pl> will add these requirements to its own.
-
-Please remember--if you put something in C<REQUIRED_MODULES>, then
-F<checksetup.pl> B<cannot complete> unless the user has that module
-installed! So use C<OPTIONAL_MODULES> whenever you can.
-
-=head2 install-update_db
+=head2 install_update_db
This happens at the very end of all the tables being updated
during an installation or upgrade. If you need to modify your custom
schema, do it here. No params are passed.
-=head2 db_schema-abstract_schema
+=head2 db_schema_abstract_schema
This allows you to add tables to Bugzilla. Note that we recommend that you
prefix the names of your tables with some word, so that they don't conflict
@@ -588,7 +478,7 @@ database when run.
=back
-=head2 mailer-before_send
+=head2 mailer_before_send
Called right before L<Bugzilla::Mailer> sends a message to the MTA.
@@ -600,7 +490,7 @@ Params:
=back
-=head2 object-before_create
+=head2 object_before_create
This happens at the beginning of L<Bugzilla::Object/create>.
@@ -620,7 +510,7 @@ A hashref. The set of named parameters passed to C<create>.
=back
-=head2 object-before_set
+=head2 object_before_set
Called during L<Bugzilla::Object/set>, before any actual work is done.
You can use this to perform actions before a value is changed for
@@ -646,7 +536,7 @@ The value being set on the object.
=back
-=head2 object-end_of_create_validators
+=head2 object_end_of_create_validators
Called at the end of L<Bugzilla::Object/run_create_validators>. You can
use this to run additional validation when creating an object.
@@ -671,7 +561,7 @@ validated by the C<VALIDATORS> specified for the object.
=back
-=head2 object-end_of_set_all
+=head2 object_end_of_set_all
This happens at the end of L<Bugzilla::Object/set_all>. This is a
good place to call custom set_ functions on objects, or to make changes
@@ -693,7 +583,7 @@ A hashref. The set of named parameters passed to C<set_all>.
=back
-=head2 object-end_of_update
+=head2 object_end_of_update
Called during L<Bugzilla::Object/update>, after changes are made
to the database, but while still inside a transaction.
@@ -719,7 +609,7 @@ L<Bugzilla::Object/update> returns.
=back
-=head2 page-before_template
+=head2 page_before_template
This is a simple way to add your own pages to Bugzilla. This hooks C<page.cgi>,
which loads templates from F<template/en/default/pages>. For example,
@@ -746,7 +636,7 @@ your template.
=back
-=head2 product-confirm_delete
+=head2 product_confirm_delete
Called before displaying the confirmation message when deleting a product.
@@ -758,7 +648,7 @@ Params:
=back
-=head2 sanitycheck-check
+=head2 sanitycheck_check
This hook allows for extra sanity checks to be added, for use by
F<sanitycheck.cgi>.
@@ -772,7 +662,7 @@ to the user. (F<sanitycheck.cgi>'s C<Status>)
=back
-=head2 sanitycheck-repair
+=head2 sanitycheck_repair
This hook allows for extra sanity check repairs to be made, for use by
F<sanitycheck.cgi>.
@@ -786,7 +676,7 @@ to the user. (F<sanitycheck.cgi>'s C<Status>)
=back
-=head2 template-before_create
+=head2 template_before_create
This hook allows you to modify the configuration of L<Bugzilla::Template>
objects before they are created. For example, you could add a new
@@ -805,7 +695,7 @@ look at the code for C<create> in L<Bugzilla::Template>.)
=back
-=head2 template-before_process
+=head2 template_before_process
This hook allows you to define additional variables that will be available to
the template being processed. You probably want to restrict your hook
@@ -869,7 +759,7 @@ plugins).
=back
-=head2 webservice-error_codes
+=head2 webservice_error_codes
If your webservice extension throws custom errors, you can set numeric
codes for those errors here.
@@ -887,3 +777,7 @@ A hash that maps the names of errors (like C<invalid_param>) to numbers.
See L<Bugzilla::WebService::Constants/WS_ERROR_CODE> for an example.
=back
+
+=head1 SEE ALSO
+
+L<Bugzilla::Extension>
diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm
index 51d258227..d7eb14c24 100644
--- a/Bugzilla/Install/DB.pm
+++ b/Bugzilla/Install/DB.pm
@@ -590,7 +590,7 @@ sub update_table_definitions {
# New --TABLE-- changes should go *** A B O V E *** this point #
################################################################
- Bugzilla::Hook::process('install-update_db');
+ Bugzilla::Hook::process('install_update_db');
# We do this here because otherwise the foreign key from
# products.classification_id to classifications.id will fail
diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm
index 1fa53de9b..190dbe968 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -26,7 +26,8 @@ package Bugzilla::Install::Requirements;
use strict;
use Bugzilla::Constants;
-use Bugzilla::Install::Util qw(vers_cmp install_string);
+use Bugzilla::Install::Util qw(vers_cmp install_string
+ extension_requirement_packages);
use List::Util qw(max);
use Safe;
use Term::ANSIColor;
@@ -138,9 +139,9 @@ sub REQUIRED_MODULES {
},
);
- my $all_modules = _get_extension_requirements(
- 'REQUIRED_MODULES', \@modules);
- return $all_modules;
+ my $extra_modules = _get_extension_requirements('REQUIRED_MODULES');
+ push(@modules, @$extra_modules);
+ return \@modules;
};
sub OPTIONAL_MODULES {
@@ -291,9 +292,9 @@ sub OPTIONAL_MODULES {
},
);
- my $all_modules = _get_extension_requirements(
- 'OPTIONAL_MODULES', \@modules);
- return $all_modules;
+ my $extra_modules = _get_extension_requirements('OPTIONAL_MODULES');
+ push(@modules, @$extra_modules);
+ return \@modules;
};
# This maps features to the files that require that feature in order
@@ -312,31 +313,20 @@ use constant FEATURE_FILES => (
updates => ['Bugzilla/Update.pm'],
);
-# This implements the install-requirements hook described in Bugzilla::Hook.
+# This implements the REQUIRED_MODULES and OPTIONAL_MODULES stuff
+# described in in Bugzilla::Extension.
sub _get_extension_requirements {
- my ($function, $base_modules) = @_;
- my @all_modules;
- # get a list of all extensions
- my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
- foreach my $extension (@extensions) {
- my $file = "$extension/code/install-requirements.pl";
- if (-e $file) {
- my $safe = new Safe;
- # This is a very liberal Safe.
- $safe->permit(qw(:browse require entereval caller));
- $safe->rdo($file);
- if ($@) {
- warn $@;
- next;
- }
- my $modules = eval { &{$safe->varglob($function)}($base_modules) };
- next unless $modules;
- push(@all_modules, @$modules);
+ my ($function) = @_;
+
+ my $packages = extension_requirement_packages();
+ my @modules;
+ foreach my $package (@$packages) {
+ if ($package->can($function)) {
+ my $extra_modules = $package->$function;
+ push(@modules, @$extra_modules);
}
}
-
- unshift(@all_modules, @$base_modules);
- return \@all_modules;
+ return \@modules;
};
sub check_requirements {
diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm
index d3fb4e5f8..254cc237b 100644
--- a/Bugzilla/Install/Util.pm
+++ b/Bugzilla/Install/Util.pm
@@ -37,6 +37,8 @@ use base qw(Exporter);
our @EXPORT_OK = qw(
bin_loc
get_version_and_os
+ extension_code_files
+ extension_requirement_packages
indicate_progress
install_string
include_languages
@@ -79,6 +81,96 @@ sub get_version_and_os {
os_ver => $os_details[3] };
}
+sub _extension_paths {
+ my $dir = bz_locations()->{'extensionsdir'};
+ my @extension_items = glob("$dir/*");
+ my @paths;
+ 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 "$item/disabled") {
+ push(@paths, $item);
+ }
+ }
+ elsif ($item =~ /\.pm$/i) {
+ push(@paths, $item);
+ }
+ }
+ return @paths;
+}
+
+sub extension_code_files {
+ my ($requirements_only) = @_;
+ my @files;
+ foreach my $path (_extension_paths()) {
+ my @load_files;
+ if (-d $path) {
+ my $extension_file = "$path/Extension.pm";
+ my $config_file = "$path/Config.pm";
+ if (-e $extension_file) {
+ push(@load_files, $extension_file);
+ }
+ if (-e $config_file) {
+ push(@load_files, $config_file);
+ }
+
+ # Don't load Extension.pm if we just want Config.pm and
+ # we found both.
+ if ($requirements_only and scalar(@load_files) == 2) {
+ shift(@load_files);
+ }
+ }
+ else {
+ push(@load_files, $path);
+ }
+ next if !scalar(@load_files);
+ # We know that these paths are safe, because they came from
+ # extensionsdir and we checked them specifically for their format.
+ # Also, the only thing we ever do with them is pass them to "require".
+ trick_taint($_) foreach @load_files;
+ push(@files, \@load_files);
+ }
+ return \@files;
+}
+
+# Used by _get_extension_requirements in Bugzilla::Install::Requirements.
+sub extension_requirement_packages {
+ # If we're in a .cgi script or some time that's not the requirements phase,
+ # just use Bugzilla->extensions. This avoids running the below code during
+ # a normal Bugzilla page, which is important because the below code
+ # doesn't actually function right if it runs after
+ # Bugzilla::Extension->load_all (because stuff has already been loaded).
+ # (This matters because almost every page calls Bugzilla->feature, which
+ # calls OPTIONAL_MODULES, which calls this method.)
+ if (eval { Bugzilla->extensions }) {
+ return Bugzilla->extensions;
+ }
+ my $packages = _cache()->{extension_requirement_packages};
+ return $packages if $packages;
+ $packages = [];
+ my %package_map;
+
+ my $extension_files = extension_code_files('requirements only');
+ foreach my $file_set (@$extension_files) {
+ my $file = shift @$file_set;
+ my $name = require $file;
+ if ($name =~ /^\d+$/) {
+ die install_string('extension_must_return_name',
+ { file => $file, returned => $name });
+ }
+ my $package = "Bugzilla::Extension::$name";
+ $package_map{$file} = $package;
+ push(@$packages, $package);
+ }
+ _cache()->{extension_requirement_packages} = $packages;
+ # Used by Bugzilla::Extension->load if it's called after this method
+ # (which only happens during checksetup.pl, currently).
+ _cache()->{extension_requirement_package_map} = \%package_map;
+ return $packages;
+}
+
sub indicate_progress {
my ($params) = @_;
my $current = $params->{current};
@@ -93,8 +185,8 @@ sub indicate_progress {
sub install_string {
my ($string_id, $vars) = @_;
- _cache()->{template_include_path} ||= template_include_path();
- my $path = _cache()->{template_include_path};
+ _cache()->{install_string_path} ||= template_include_path();
+ my $path = _cache()->{install_string_path};
my $string_template;
# Find the first template that defines this string.
@@ -134,10 +226,10 @@ sub include_languages {
# function in Bugzilla->request_cache. This is done to improve the
# performance of the template processing.
my $to_be_cached = 0;
- if (exists $ENV{'SERVER_SOFTWARE'} and not @_) {
- my $cache = Bugzilla->request_cache;
+ if (not @_) {
+ my $cache = _cache();
if (exists $cache->{include_languages}) {
- return @{$cache->{include_languages}}
+ return @{ $cache->{include_languages} };
}
$to_be_cached = 1;
}
@@ -202,8 +294,7 @@ sub include_languages {
# Cache the result if we are in CGI mode and called without parameter
# (see the comment at the top of this function).
if ($to_be_cached) {
- my $cache = Bugzilla->request_cache;
- $cache->{include_languages} = \@usedlanguages;
+ _cache()->{include_languages} = \@usedlanguages;
}
return @usedlanguages;
@@ -241,12 +332,8 @@ sub template_base_directories {
# First, we add extension template directories, because extension templates
# override standard templates. Extensions may be localized in the same way
# that Bugzilla templates are localized.
- my @template_dirs;
- my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
- foreach my $extension (@extensions) {
- next if (-e "$extension/disabled" or !-d "$extension/template");
- push(@template_dirs, "$extension/template");
- }
+ my @extensions = grep { -d "$_/template" } _extension_paths();
+ my @template_dirs = map { "$_/template" } @extensions;
push(@template_dirs, bz_locations()->{'templatedir'});
return \@template_dirs;
}
@@ -384,12 +471,13 @@ sub init_console {
}
# This is like request_cache, but it's used only by installation code
-# for setup.cgi and things like that.
+# for checksetup.pl and things like that.
our $_cache = {};
sub _cache {
- if ($ENV{MOD_PERL}) {
- require Apache2::RequestUtil;
- return Apache2::RequestUtil->request->pnotes();
+ # If the normal request_cache is available (which happens any time
+ # after the requirements phase) then we should use that.
+ if (eval { Bugzilla->request_cache; }) {
+ return Bugzilla->request_cache;
}
return $_cache;
}
diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm
index 83ae5a600..71dc8f1f5 100644
--- a/Bugzilla/Mailer.pm
+++ b/Bugzilla/Mailer.pm
@@ -171,7 +171,7 @@ sub MessageToMTA {
Debug => Bugzilla->params->{'smtp_debug'};
}
- Bugzilla::Hook::process('mailer-before_send', { email => $email });
+ Bugzilla::Hook::process('mailer_before_send', { email => $email });
if ($method eq "Test") {
my $filename = bz_locations()->{'datadir'} . '/mailer.testfile';
diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm
index a1857db1c..92353b6a0 100644
--- a/Bugzilla/Object.pm
+++ b/Bugzilla/Object.pm
@@ -279,7 +279,7 @@ sub set {
superclass => __PACKAGE__,
function => 'Bugzilla::Object->set' });
- Bugzilla::Hook::process('object-before_set',
+ Bugzilla::Hook::process('object_before_set',
{ object => $self, field => $field,
value => $value });
@@ -303,7 +303,7 @@ sub set_all {
my $method = "set_$key";
$self->$method($params->{$key});
}
- Bugzilla::Hook::process('object-end_of_set_all', { object => $self,
+ Bugzilla::Hook::process('object_end_of_set_all', { object => $self,
params => $params });
}
@@ -348,7 +348,7 @@ sub update {
$dbh->do("UPDATE $table SET $columns WHERE $id_field = ?", undef,
@values, $self->id) if @values;
- Bugzilla::Hook::process('object-end_of_update',
+ Bugzilla::Hook::process('object_end_of_update',
{ object => $self, old_object => $old_self,
changes => \%changes });
@@ -412,7 +412,7 @@ sub check_required_create_fields {
# This hook happens here so that even subclasses that don't call
# SUPER::create are still affected by the hook.
- Bugzilla::Hook::process('object-before_create', { class => $class,
+ Bugzilla::Hook::process('object_before_create', { class => $class,
params => $params });
foreach my $field ($class->REQUIRED_CREATE_FIELDS) {
@@ -445,7 +445,7 @@ sub run_create_validators {
$field_values{$field} = $value;
}
- Bugzilla::Hook::process('object-end_of_create_validators',
+ Bugzilla::Hook::process('object_end_of_create_validators',
{ class => $class, params => \%field_values });
return \%field_values;
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index 4aaf7e14c..7b8ac10e2 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -170,7 +170,7 @@ sub COLUMNS {
# The short_short_desc column is identical to short_desc
$columns{'short_short_desc'} = $columns{'short_desc'};
- Bugzilla::Hook::process("buglist-columns", { columns => \%columns });
+ Bugzilla::Hook::process('buglist_columns', { columns => \%columns });
$cache->{search_columns} = \%columns;
return $cache->{search_columns};
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index c7b8cee52..ce7c2ab1c 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -86,9 +86,9 @@ sub process {
my $self = shift;
my ($file, $vars) = @_;
- Bugzilla::Hook::process("template-before_process",
- { vars => $vars, file => $file,
- template => $self });
+ #Bugzilla::Hook::process('template_before_process',
+ # { vars => $vars, file => $file,
+ # template => $self });
return $self->SUPER::process(@_);
}
@@ -188,7 +188,7 @@ sub quoteUrls {
my $tmp;
my @hook_regexes;
- Bugzilla::Hook::process('bug-format_comment',
+ Bugzilla::Hook::process('bug_format_comment',
{ text => \$text, bug => $bug, regexes => \@hook_regexes,
comment => $comment });
@@ -793,7 +793,7 @@ sub create {
},
};
- Bugzilla::Hook::process('template-before_create', { config => $config });
+ Bugzilla::Hook::process('template_before_create', { config => $config });
my $template = $class->new($config)
|| die("Template creation failed: " . $class->error());
return $template;
diff --git a/checksetup.pl b/checksetup.pl
index d3e3f7952..8993ff08d 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -222,7 +222,7 @@ Bugzilla::Install::reset_password($switch{'reset-password'})
Bugzilla::Install::create_default_product();
-Bugzilla::Hook::process('install-before_final_checks', {'silent' => $silent });
+Bugzilla::Hook::process('install_before_final_checks', { silent => $silent });
###########################################################################
# Final checks
diff --git a/colchange.cgi b/colchange.cgi
index 6c2fa8090..409f15e5a 100755
--- a/colchange.cgi
+++ b/colchange.cgi
@@ -88,7 +88,7 @@ my @custom_fields = grep { $_->type != FIELD_TYPE_MULTI_SELECT }
Bugzilla->active_custom_fields;
push(@masterlist, map { $_->name } @custom_fields);
-Bugzilla::Hook::process("colchange-columns", {'columns' => \@masterlist} );
+Bugzilla::Hook::process('colchange_columns', {'columns' => \@masterlist} );
$vars->{'masterlist'} = \@masterlist;
diff --git a/editparams.cgi b/editparams.cgi
index 2c3e4089e..9b4f04e3c 100755
--- a/editparams.cgi
+++ b/editparams.cgi
@@ -74,7 +74,7 @@ foreach my $panel (keys %$param_panels) {
my %hook_panels = map { $_->{name} => { params => $_->{param_list} } }
@panels;
# Note that this hook is also called in Bugzilla::Config.
-Bugzilla::Hook::process('config-modify_panels', { panels => \%hook_panels });
+Bugzilla::Hook::process('config_modify_panels', { panels => \%hook_panels });
$vars->{panels} = \@panels;
diff --git a/editproducts.cgi b/editproducts.cgi
index 1d4335466..86f2c0405 100755
--- a/editproducts.cgi
+++ b/editproducts.cgi
@@ -218,7 +218,7 @@ if ($action eq 'del') {
$vars->{'product'} = $product;
$vars->{'token'} = issue_session_token('delete_product');
- Bugzilla::Hook::process("product-confirm_delete", { vars => $vars });
+ Bugzilla::Hook::process('product_confirm_delete', { vars => $vars });
$template->process("admin/products/confirm-delete.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
diff --git a/enter_bug.cgi b/enter_bug.cgi
index ab0108748..e6e78eaa1 100755
--- a/enter_bug.cgi
+++ b/enter_bug.cgi
@@ -604,7 +604,7 @@ foreach my $row (@$grouplist) {
$vars->{'group'} = \@groups;
-Bugzilla::Hook::process("enter_bug-entrydefaultvars", { vars => $vars });
+Bugzilla::Hook::process('enter_bug_entrydefaultvars', { vars => $vars });
$vars->{'default'} = \%default;
diff --git a/extensions/BmpConvert/Config.pm b/extensions/BmpConvert/Config.pm
new file mode 100644
index 000000000..1b314917a
--- /dev/null
+++ b/extensions/BmpConvert/Config.pm
@@ -0,0 +1,33 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developer are Copyright (C) 2009
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Extension::BmpConvert;
+use strict;
+use constant NAME => 'BmpConvert';
+use constant REQUIRED_MODULES => [
+ {
+ package => 'PerlMagick',
+ module => 'Image::Magick',
+ version => 0,
+ },
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/BmpConvert/Extension.pm b/extensions/BmpConvert/Extension.pm
new file mode 100644
index 000000000..29113bd08
--- /dev/null
+++ b/extensions/BmpConvert/Extension.pm
@@ -0,0 +1,57 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Frédéric Buclin.
+# Portions created by Frédéric Buclin are Copyright (C) 2009
+# Frédéric Buclin. All Rights Reserved.
+#
+# Contributor(s):
+# Frédéric Buclin <LpSolit@gmail.com>
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Extension::BmpConvert;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Image::Magick;
+
+our $VERSION = '1.0';
+
+sub attachment_process_data {
+ my ($self, $params) = @_;
+ return unless $params->{attributes}->{mimetype} eq 'image/bmp';
+
+ my $data = ${$params->{data}};
+ my $img = Image::Magick->new(magick => 'bmp');
+
+ # $data is a filehandle.
+ if (ref $data) {
+ $img->Read(file => \*$data);
+ $img->set(magick => 'png');
+ $img->Write(file => \*$data);
+ }
+ # $data is a blob.
+ else {
+ $img->BlobToImage($data);
+ $img->set(magick => 'png');
+ $data = $img->ImageToBlob();
+ }
+ undef $img;
+
+ ${$params->{data}} = $data;
+ $params->{attributes}->{mimetype} = 'image/png';
+ $params->{attributes}->{filename} =~ s/^(.+)\.bmp$/$1.png/i;
+}
+
+ __PACKAGE__->NAME;
diff --git a/extensions/BmpConvert/disabled b/extensions/BmpConvert/disabled
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/extensions/BmpConvert/disabled
diff --git a/extensions/Example/Config.pm b/extensions/Example/Config.pm
new file mode 100644
index 000000000..378db359d
--- /dev/null
+++ b/extensions/Example/Config.pm
@@ -0,0 +1,42 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developers are Copyright (C) 2009 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Extension::Example;
+use strict;
+use constant NAME => 'Example';
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Data-Dumper',
+ module => 'Data::Dumper',
+ version => 0,
+ },
+];
+
+use constant OPTIONAL_MODULES => [
+ {
+ package => 'Acme',
+ module => 'Acme',
+ version => 1.11,
+ feature => ['example_acme'],
+ },
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Example/Extension.pm b/extensions/Example/Extension.pm
new file mode 100644
index 000000000..8e3a385d6
--- /dev/null
+++ b/extensions/Example/Extension.pm
@@ -0,0 +1,431 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developers are Copyright (C) 2009 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+# Frédéric Buclin <LpSolit@gmail.com>
+
+package Bugzilla::Extension::Example;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Util qw(
+ diff_arrays
+ html_quote
+);
+
+use Data::Dumper;
+
+our $VERSION = '1.0';
+
+sub attachment_process_data {
+ my ($self, $params) = @_;
+ my $type = $params->{attributes}->{mimetype};
+ my $filename = $params->{attributes}->{filename};
+
+ # Make sure images have the correct extension.
+ # Uncomment the two lines below to make this check effective.
+ if ($type =~ /^image\/(\w+)$/) {
+ my $format = $1;
+ if ($filename =~ /^(.+)(:?\.[^\.]+)$/) {
+ my $name = $1;
+ #$params->{attributes}->{filename} = "${name}.$format";
+ }
+ else {
+ # The file has no extension. We append it.
+ #$params->{attributes}->{filename} .= ".$format";
+ }
+ }
+}
+
+sub auth_login_methods {
+ my ($self, $params) = @_;
+ my $modules = $params->{modules};
+ if (exists $modules->{Example}) {
+ $modules->{Example} = 'extensions/Example/lib/AuthLogin.pm';
+ }
+}
+
+sub auth_verify_methods {
+ my ($self, $params) = @_;
+ my $modules = $params->{modules};
+ if (exists $modules->{Example}) {
+ $modules->{Example} = 'extensions/Example/lib/AuthVerify.pm';
+ }
+}
+
+sub bug_columns {
+ my ($self, $params) = @_;
+ my $columns = $params->{'columns'};
+ push (@$columns, "delta_ts AS example")
+}
+
+sub bug_end_of_create {
+ my ($self, $params) = @_;
+
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my $bug = $params->{'bug'};
+ my $timestamp = $params->{'timestamp'};
+
+ my $bug_id = $bug->id;
+ # Uncomment this line to see a line in your webserver's error log whenever
+ # you file a bug.
+ # warn "Bug $bug_id has been filed!";
+}
+
+sub bug_end_of_create_validators {
+ my ($self, $params) = @_;
+
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my $bug_params = $params->{'params'};
+
+ # Uncomment this line below to see a line in your webserver's error log
+ # containing all validated bug field values every time you file a bug.
+ # warn Dumper($bug_params);
+
+ # This would remove all ccs from the bug, preventing ANY ccs from being
+ # added on bug creation.
+ # $bug_params->{cc} = [];
+}
+
+sub bug_end_of_update {
+ my ($self, $params) = @_;
+
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my ($bug, $timestamp, $changes) = @$params{qw(bug timestamp changes)};
+
+ foreach my $field (keys %$changes) {
+ my $used_to_be = $changes->{$field}->[0];
+ my $now_it_is = $changes->{$field}->[1];
+ }
+
+ my $status_message;
+ if (my $status_change = $changes->{'bug_status'}) {
+ my $old_status = new Bugzilla::Status({ name => $status_change->[0] });
+ my $new_status = new Bugzilla::Status({ name => $status_change->[1] });
+ if ($new_status->is_open && !$old_status->is_open) {
+ $status_message = "Bug re-opened!";
+ }
+ if (!$new_status->is_open && $old_status->is_open) {
+ $status_message = "Bug closed!";
+ }
+ }
+
+ my $bug_id = $bug->id;
+ my $num_changes = scalar keys %$changes;
+ my $result = "There were $num_changes changes to fields on bug $bug_id"
+ . " at $timestamp.";
+ # Uncomment this line to see $result in your webserver's error log whenever
+ # you update a bug.
+ # warn $result;
+}
+
+sub bug_fields {
+ my ($self, $params) = @_;
+
+ my $fields = $params->{'fields'};
+ push (@$fields, "example")
+}
+
+sub bug_format_comment {
+ my ($self, $params) = @_;
+
+ # This replaces every occurrence of the word "foo" with the word
+ # "bar"
+
+ my $regexes = $params->{'regexes'};
+ push(@$regexes, { match => qr/\bfoo\b/, replace => 'bar' });
+
+ # And this links every occurrence of the word "bar" to example.com,
+ # but it won't affect "foo"s that have already been turned into "bar"
+ # above (because each regex is run in order, and later regexes don't modify
+ # earlier matches, due to some cleverness in Bugzilla's internals).
+ #
+ # For example, the phrase "foo bar" would become:
+ # bar <a href="http://example.com/bar">bar</a>
+ my $bar_match = qr/\b(bar)\b/;
+ push(@$regexes, { match => $bar_match, replace => \&_replace_bar });
+}
+
+# Used by bug_format_comment--see its code for an explanation.
+sub _replace_bar {
+ my $params = shift;
+ # $match is the first parentheses match in the $bar_match regex
+ # in bug-format_comment.pl. We get up to 10 regex matches as
+ # arguments to this function.
+ my $match = $params->{matches}->[0];
+ # Remember, you have to HTML-escape any data that you are returning!
+ $match = html_quote($match);
+ return qq{<a href="http://example.com/">$match</a>};
+};
+
+sub buglist_columns {
+ my ($self, $params) = @_;
+
+ my $columns = $params->{'columns'};
+ $columns->{'example'} = { 'name' => 'bugs.delta_ts' , 'title' => 'Example' };
+}
+
+sub colchange_columns {
+ my ($self, $params) = @_;
+
+ my $columns = $params->{'columns'};
+ push (@$columns, "example")
+}
+
+sub config {
+ my ($self, $params) = @_;
+
+ my $config = $params->{config};
+ $config->{Example} = "extensions::Example::lib::ConfigExample";
+}
+
+sub config_add_panels {
+ my ($self, $params) = @_;
+
+ my $modules = $params->{panel_modules};
+ $modules->{Example} = "extensions::Example::lib::ConfigExample";
+}
+
+sub config_modify_panels {
+ my ($self, $params) = @_;
+
+ my $panels = $params->{panels};
+
+ # Add the "Example" auth methods.
+ my $auth_params = $panels->{'auth'}->{params};
+ my ($info_class) = grep($_->{name} eq 'user_info_class', @$auth_params);
+ my ($verify_class) = grep($_->{name} eq 'user_verify_class', @$auth_params);
+
+ push(@{ $info_class->{choices} }, 'CGI,Example');
+ push(@{ $verify_class->{choices} }, 'Example');
+}
+
+sub flag_end_of_update {
+ my ($self, $params) = @_;
+
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my $flag_params = $params;
+ my ($object, $timestamp, $old_flags, $new_flags) =
+ @$flag_params{qw(object timestamp old_flags new_flags)};
+ my ($removed, $added) = diff_arrays($old_flags, $new_flags);
+ my ($granted, $denied) = (0, 0);
+ foreach my $new_flag (@$added) {
+ $granted++ if $new_flag =~ /\+$/;
+ $denied++ if $new_flag =~ /-$/;
+ }
+ my $bug_id = $object->isa('Bugzilla::Bug') ? $object->id
+ : $object->bug_id;
+ my $result = "$granted flags were granted and $denied flags were denied"
+ . " on bug $bug_id at $timestamp.";
+ # Uncomment this line to see $result in your webserver's error log whenever
+ # you update flags.
+ # warn $result;
+}
+
+sub install_before_final_checks {
+ my ($self, $params) = @_;
+ print "Install-before_final_checks hook\n" unless $params->{silent};
+}
+
+sub mailer_before_send {
+ my ($self, $params) = @_;
+
+ my $email = $params->{email};
+ # If you add a header to an email, it's best to start it with
+ # 'X-Bugzilla-<Extension>' so that you don't conflict with
+ # other extensions.
+ $email->header_set('X-Bugzilla-Example-Header', 'Example');
+}
+
+sub object_before_create {
+ my ($self, $params) = @_;
+
+ my $class = $params->{'class'};
+ my $object_params = $params->{'params'};
+
+ # Note that this is a made-up class, for this example.
+ if ($class->isa('Bugzilla::ExampleObject')) {
+ warn "About to create an ExampleObject!";
+ warn "Got the following parameters: "
+ . join(', ', keys(%$object_params));
+ }
+}
+
+sub object_before_set {
+ my ($self, $params) = @_;
+
+ my ($object, $field, $value) = @$params{qw(object field value)};
+
+ # Note that this is a made-up class, for this example.
+ if ($object->isa('Bugzilla::ExampleObject')) {
+ warn "The field $field is changing from " . $object->{$field}
+ . " to $value!";
+ }
+}
+
+sub object_end_of_create_validators {
+ my ($self, $params) = @_;
+
+ my $class = $params->{'class'};
+ my $object_params = $params->{'params'};
+
+ # Note that this is a made-up class, for this example.
+ if ($class->isa('Bugzilla::ExampleObject')) {
+ # Always set example_field to 1, even if the validators said otherwise.
+ $object_params->{example_field} = 1;
+ }
+
+}
+
+sub object_end_of_set_all {
+ my ($self, $params) = @_;
+
+ my $object = $params->{'class'};
+ my $object_params = $params->{'params'};
+
+ # Note that this is a made-up class, for this example.
+ if ($object->isa('Bugzilla::ExampleObject')) {
+ if ($object_params->{example_field} == 1) {
+ $object->{example_field} = 1;
+ }
+ }
+
+}
+
+sub object_end_of_update {
+ my ($self, $params) = @_;
+
+ my ($object, $old_object, $changes) =
+ @$params{qw(object old_object changes)};
+
+ # Note that this is a made-up class, for this example.
+ if ($object->isa('Bugzilla::ExampleObject')) {
+ if (defined $changes->{'name'}) {
+ my ($old, $new) = @{ $changes->{'name'} };
+ print "The name field changed from $old to $new!";
+ }
+ }
+}
+
+sub page_before_template {
+ my ($self, $params) = @_;
+
+ my ($vars, $page) = @$params{qw(vars page_id)};
+
+ # You can see this hook in action by loading page.cgi?id=example.html
+ if ($page eq 'example.html') {
+ $vars->{cgi_variables} = { Bugzilla->cgi->Vars };
+ }
+}
+
+sub product_confirm_delete {
+ my ($self, $params) = @_;
+
+ my $vars = $params->{vars};
+ $vars->{'example'} = 1;
+}
+
+sub sanitycheck_check {
+ my ($self, $params) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $sth;
+
+ my $status = $params->{'status'};
+
+ # Check that all users are Australian
+ $status->('example_check_au_user');
+
+ $sth = $dbh->prepare("SELECT userid, login_name
+ FROM profiles
+ WHERE login_name NOT LIKE '%.au'");
+ $sth->execute;
+
+ my $seen_nonau = 0;
+ while (my ($userid, $login, $numgroups) = $sth->fetchrow_array) {
+ $status->('example_check_au_user_alert',
+ { userid => $userid, login => $login },
+ 'alert');
+ $seen_nonau = 1;
+ }
+
+ $status->('example_check_au_user_prompt') if $seen_nonau;
+}
+
+sub sanitycheck_repair {
+ my ($self, $params) = @_;
+
+ my $cgi = Bugzilla->cgi;
+ my $dbh = Bugzilla->dbh;
+
+ my $status = $params->{'status'};
+
+ if ($cgi->param('example_repair_au_user')) {
+ $status->('example_repair_au_user_start');
+
+ #$dbh->do("UPDATE profiles
+ # SET login_name = CONCAT(login_name, '.au')
+ # WHERE login_name NOT LIKE '%.au'");
+
+ $status->('example_repair_au_user_end');
+ }
+}
+
+sub template_before_create {
+ my ($self, $params) = @_;
+
+ my $config = $params->{'config'};
+ # This will be accessible as "example_global_variable" in every
+ # template in Bugzilla. See Bugzilla/Template.pm's create() function
+ # for more things that you can set.
+ $config->{VARIABLES}->{example_global_variable} = sub { return 'value' };
+}
+
+sub template_before_process {
+ my ($self, $params) = @_;
+
+ my ($vars, $file, $template) = @$params{qw(vars file template)};
+
+ $vars->{'example'} = 1;
+
+ if ($file =~ m{^bug/show}) {
+ $vars->{'showing_a_bug'} = 1;
+ }
+}
+
+sub webservice {
+ my ($self, $params) = @_;
+
+ my $dispatch = $params->{dispatch};
+ $dispatch->{Example} = "extensions::Example::lib::WSExample";
+}
+
+sub webservice_error_codes {
+ my ($self, $params) = @_;
+
+ my $error_map = $params->{error_map};
+ $error_map->{'example_my_error'} = 10001;
+}
+
+# This must be the last line of your extension.
+__PACKAGE__->NAME;
diff --git a/extensions/Example/disabled b/extensions/Example/disabled
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/extensions/Example/disabled
diff --git a/extensions/Example/lib/AuthLogin.pm b/extensions/Example/lib/AuthLogin.pm
new file mode 100644
index 000000000..46147de9c
--- /dev/null
+++ b/extensions/Example/lib/AuthLogin.pm
@@ -0,0 +1,32 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Example Plugin.
+#
+# The Initial Developer of the Original Code is Canonical Ltd.
+# Portions created by Canonical are Copyright (C) 2008 Canonical Ltd.
+# All Rights Reserved.
+#
+# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package extensions::Example::lib::AuthLogin;
+use strict;
+use base qw(Bugzilla::Auth::Login);
+use constant user_can_create_account => 0;
+use Bugzilla::Constants;
+
+# Always returns no data.
+sub get_login_info {
+ return { failure => AUTH_NODATA };
+}
+
+1;
diff --git a/extensions/Example/lib/AuthVerify.pm b/extensions/Example/lib/AuthVerify.pm
new file mode 100644
index 000000000..2ecf83a31
--- /dev/null
+++ b/extensions/Example/lib/AuthVerify.pm
@@ -0,0 +1,31 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Example Plugin.
+#
+# The Initial Developer of the Original Code is Canonical Ltd.
+# Portions created by Canonical are Copyright (C) 2008 Canonical Ltd.
+# All Rights Reserved.
+#
+# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package extensions::Example::lib::AuthVerify;
+use strict;
+use base qw(Bugzilla::Auth::Verify);
+use Bugzilla::Constants;
+
+# A verifier that always fails.
+sub check_credentials {
+ return { failure => AUTH_NO_SUCH_USER };
+}
+
+1;
diff --git a/extensions/Example/lib/ConfigExample.pm b/extensions/Example/lib/ConfigExample.pm
new file mode 100644
index 000000000..49ffefed9
--- /dev/null
+++ b/extensions/Example/lib/ConfigExample.pm
@@ -0,0 +1,41 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Example Plugin.
+#
+# The Initial Developer of the Original Code is Canonical Ltd.
+# Portions created by Canonical Ltd. are Copyright (C) 2008
+# Canonical Ltd. All Rights Reserved.
+#
+# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+# Bradley Baetz <bbaetz@acm.org>
+
+package extensions::Example::lib::ConfigExample;
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+
+sub get_param_list {
+ my ($class) = @_;
+
+ my @param_list = (
+ {
+ name => 'example_string',
+ type => 't',
+ default => 'EXAMPLE',
+ },
+ );
+ return @param_list;
+}
+
+1;
diff --git a/extensions/Example/lib/WSExample.pm b/extensions/Example/lib/WSExample.pm
new file mode 100644
index 000000000..4330633ce
--- /dev/null
+++ b/extensions/Example/lib/WSExample.pm
@@ -0,0 +1,32 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by Everything Solved, Inc. are Copyright (C) 2007
+# Everything Solved, Inc. All Rights Reserved.
+#
+# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package extensions::Example::lib::WSExample;
+use strict;
+use warnings;
+use base qw(Bugzilla::WebService);
+use Bugzilla::Error;
+
+# This can be called as Example.hello() from the WebService.
+sub hello { return 'Hello!'; }
+
+sub throw_an_error { ThrowUserError('example_my_error') }
+
+1;
diff --git a/extensions/Example/template/en/default/admin/params/example.html.tmpl b/extensions/Example/template/en/default/admin/params/example.html.tmpl
new file mode 100644
index 000000000..e2bb5f35c
--- /dev/null
+++ b/extensions/Example/template/en/default/admin/params/example.html.tmpl
@@ -0,0 +1,28 @@
+[%#
+ # The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Example Plugin.
+ #
+ # The Initial Developer of the Original Code is Canonical Ltd.
+ # Portions created by Canonical Ltd. are Copyright (C) 2008
+ # Canonical Ltd. All Rights Reserved.
+ #
+ # Contributor(s): Bradley Baetz <bbaetz@acm.org>
+ #%]
+[%
+ title = "Example Extension"
+ desc = "Configure example extension"
+%]
+
+[% param_descs = {
+ example_string => "Example string",
+}
+%]
diff --git a/extensions/Example/template/en/default/pages/example.html.tmpl b/extensions/Example/template/en/default/pages/example.html.tmpl
new file mode 100644
index 000000000..d53f78fde
--- /dev/null
+++ b/extensions/Example/template/en/default/pages/example.html.tmpl
@@ -0,0 +1,32 @@
+[%#
+ # The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Example Plugin.
+ #
+ # The Initial Developer of the Original Code is Canonical Ltd.
+ # Portions created by Canonical Ltd. are Copyright (C) 2009
+ # Canonical Ltd. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Max Kanat-Alexander <mkanat@bugzilla.org>
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Example Page"
+%]
+
+<p>Here's what you passed me:</p>
+[% USE Dumper %]
+<pre>
+ [% Dumper.dump_html(cgi_variables) %]
+</pre>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/Example/template/en/default/setup/strings.txt.pl b/extensions/Example/template/en/default/setup/strings.txt.pl
new file mode 100644
index 000000000..8da19c0aa
--- /dev/null
+++ b/extensions/Example/template/en/default/setup/strings.txt.pl
@@ -0,0 +1,24 @@
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developer are Copyright (C) 2009 the
+# Initial Developer. All Rights Reserved.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# Contributor(s):
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+
+%strings = (
+ feature_example_acme => 'Example Extension: Acme Feature',
+);
+
+1;
diff --git a/extensions/Example/template/en/hook/admin/sanitycheck/messages-statuses.html.tmpl b/extensions/Example/template/en/hook/admin/sanitycheck/messages-statuses.html.tmpl
new file mode 100644
index 000000000..8a825e57c
--- /dev/null
+++ b/extensions/Example/template/en/hook/admin/sanitycheck/messages-statuses.html.tmpl
@@ -0,0 +1,35 @@
+[%# -*- Mode: perl; indent-tabs-mode: nil -*-
+ #
+ # The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Example Plugin.
+ #
+ # The Initial Developer of the Original Code is ITA Software
+ # Portions created by the Initial Developer are Copyright (C) 2009
+ # the Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Bradley Baetz <bbaetz@everythingsolved.com>
+ #%]
+
+[% IF san_tag == "example_check_au_user" %]
+ <em>EXAMPLE PLUGIN</em> - Checking for non-Australian users.
+[% ELSIF san_tag == "example_check_au_user_alert" %]
+ User &lt;[% login FILTER html %]&gt; isn't Australian.
+ [% IF user.in_group('editusers') %]
+ <a href="editusers.cgi?id=[% userid FILTER none %]">Edit this user</a>.
+ [% END %]
+[% ELSIF san_tag == "example_check_au_user_prompt" %]
+ <a href="sanitycheck.cgi?example_repair_au_user=1">Fix these users</a>.
+[% ELSIF san_tag == "example_repair_au_user_start" %]
+ <em>EXAMPLE PLUGIN</em> - OK, would now make users Australian.
+[% ELSIF san_tag == "example_repair_au_user_end" %]
+ <em>EXAMPLE PLUGIN</em> - Users would now be Australian.
+[% END %]
diff --git a/extensions/Example/template/en/hook/global/user-error-errors.html.tmpl b/extensions/Example/template/en/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..df5a203dd
--- /dev/null
+++ b/extensions/Example/template/en/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,12 @@
+[%# Note that error messages should generally be indented four spaces, like
+ # below, because when Bugzilla translates an error message into plain
+ # text, it takes four spaces off the beginning of the lines.
+ #
+ # Note also that I prefixed my error name with "example", the name of my
+ # extension, so that I wouldn't conflict with other error names in
+ # Bugzilla or other extensions.
+ #%]
+[% IF error == "example_my_error" %]
+ [% title = "Example Error Title" %]
+ This is the error message! It contains <em>some html</em>.
+[% END %]
diff --git a/mod_perl.pl b/mod_perl.pl
index 8ca691f9c..dc737af13 100755
--- a/mod_perl.pl
+++ b/mod_perl.pl
@@ -20,8 +20,8 @@ package Bugzilla::ModPerl;
use strict;
# If you have an Apache2::Status handler in your Apache configuration,
-# you need to load Apache2::Status *here*, so that Apache::DBI can
-# report information to Apache2::Status.
+# you need to load Apache2::Status *here*, so that any later-loaded modules
+# can report information to Apache2::Status.
#use Apache2::Status ();
# We don't want to import anything into the global scope during
@@ -41,6 +41,7 @@ Template::Config->preload();
use Bugzilla ();
use Bugzilla::Constants ();
use Bugzilla::CGI ();
+use Bugzilla::Extension ();
use Bugzilla::Install::Requirements ();
use Bugzilla::Mailer ();
use Bugzilla::Template ();
@@ -87,6 +88,8 @@ foreach my $file (glob "$cgi_path/*.cgi") {
$rl->handler($file, $file);
}
+# And now pre-load all extensions
+$Bugzilla::extension_packages = Bugzilla::Extension->load_all();
package Bugzilla::ModPerl::ResponseHandler;
use strict;
diff --git a/page.cgi b/page.cgi
index 914ba3f22..d889841b3 100755
--- a/page.cgi
+++ b/page.cgi
@@ -52,7 +52,7 @@ if ($id) {
}
my %vars;
- Bugzilla::Hook::process('page-before_template',
+ Bugzilla::Hook::process('page_before_template',
{ page_id => $id, vars => \%vars });
my $format = $template->get_format("pages/$1", undef, $2);
diff --git a/post_bug.cgi b/post_bug.cgi
index ed483aec9..c9de83bb7 100755
--- a/post_bug.cgi
+++ b/post_bug.cgi
@@ -254,7 +254,7 @@ $vars->{'mailrecipients'} = {'changer' => $user->login};
$vars->{'id'} = $id;
$vars->{'bug'} = $bug;
-Bugzilla::Hook::process("post_bug-after_creation", { vars => $vars });
+Bugzilla::Hook::process('post_bug_after_creation', { vars => $vars });
ThrowCodeError("bug_error", { bug => $bug }) if $bug->error;
diff --git a/sanitycheck.cgi b/sanitycheck.cgi
index f5ba1024f..614273769 100755
--- a/sanitycheck.cgi
+++ b/sanitycheck.cgi
@@ -388,7 +388,7 @@ if ($cgi->param('remove_old_whine_targets')) {
# Repair hook
###########################################################################
-Bugzilla::Hook::process("sanitycheck-repair", { status => \&Status });
+Bugzilla::Hook::process('sanitycheck_repair', { status => \&Status });
###########################################################################
# Checks
@@ -1075,7 +1075,7 @@ Status('whines_obsolete_target_fix') if $display_repair_whines_link;
# Check hook
###########################################################################
-Bugzilla::Hook::process("sanitycheck-check", { status => \&Status });
+Bugzilla::Hook::process('sanitycheck_check', { status => \&Status });
###########################################################################
# End
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index c1ff61159..d64b3656a 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -143,6 +143,33 @@
An error occurred processing hook [% name FILTER html %] in
extension [% extension FILTER html %]: [% errstr FILTER html %]
+ [% ELSIF error == "extension_must_be_subclass" %]
+ <code>[% package FILTER html %]</code> from
+ <code>[% filename FILTER html %]</code> is not a subclass of
+ <code>[% class FILTER html %]</code>.
+
+ [% ELSIF error == "extension_must_return_name" %]
+ <code>[% extension FILTER html %]</code> returned
+ <code>[% returned FILTER html %]</code>, which is not a valid name
+ for an extension. Extensions must return their name, not <code>1</code>
+ or a number. See the documentation of
+ <a href="[% docs_urlbase %]api/Bugzilla/Extension.html">Bugzilla::Extension</a>
+ for details.
+
+ [% ELSIF error == "extension_no_name" %]
+ We did not find a <code>NAME</code> method in
+ <code>[% package FILTER html %]</code> (loaded from
+ <code>[% filename FILTER html %]</code>). This means that
+ the extension has one or more of the following problems:
+
+ <ul>
+ <li><code>[% filename FILTER html %]</code> did not define a
+ <code>[% package FILTER html %]</code> package.</li>
+ <li><code>[% package FILTER html %]</code> did not define a
+ <code>NAME</code> method (or the <code>NAME</code> method
+ returned an empty string).</li>
+ </ul>
+
[% ELSIF error == "extern_id_conflict" %]
The external ID '[% extern_id FILTER html %]' already exists
in the database for '[% username FILTER html %]', but your
diff --git a/template/en/default/setup/strings.txt.pl b/template/en/default/setup/strings.txt.pl
index 2a8e993e7..eec0bd90e 100644
--- a/template/en/default/setup/strings.txt.pl
+++ b/template/en/default/setup/strings.txt.pl
@@ -45,7 +45,11 @@ COMMANDS TO INSTALL REQUIRED MODULES (You *must* run all these commands
and then re-run this script):
EOT
done => 'done.',
-
+ extension_must_return_name => <<END,
+##file## returned ##returned##, which is not a valid name for an extension.
+Extensions must return their name, not <code>1</code> or a number. See
+the documentation of Bugzilla::Extension for details.
+END
feature_auth_ldap => 'LDAP Authentication',
feature_auth_radius => 'RADIUS Authentication',
feature_graphical_reports => 'Graphical Reports',