]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 430014: Re-write the code hooks system so that it uses modules instead of individ...
authormkanat%bugzilla.org <>
Tue, 24 Nov 2009 06:09:41 +0000 (06:09 +0000)
committermkanat%bugzilla.org <>
Tue, 24 Nov 2009 06:09:41 +0000 (06:09 +0000)
Patch by Max Kanat-Alexander <mkanat@bugzilla.org> (module owner) a=mkanat

44 files changed:
Bugzilla.pm
Bugzilla/Attachment.pm
Bugzilla/Auth/Login/Stack.pm
Bugzilla/Auth/Verify/Stack.pm
Bugzilla/Bug.pm
Bugzilla/Config.pm
Bugzilla/DB/Schema.pm
Bugzilla/Error.pm
Bugzilla/Extension.pm [new file with mode: 0644]
Bugzilla/Flag.pm
Bugzilla/Hook.pm
Bugzilla/Install/DB.pm
Bugzilla/Install/Requirements.pm
Bugzilla/Install/Util.pm
Bugzilla/Mailer.pm
Bugzilla/Object.pm
Bugzilla/Search.pm
Bugzilla/Template.pm
checksetup.pl
colchange.cgi
editparams.cgi
editproducts.cgi
enter_bug.cgi
extensions/BmpConvert/Config.pm [new file with mode: 0644]
extensions/BmpConvert/Extension.pm [new file with mode: 0644]
extensions/BmpConvert/disabled [new file with mode: 0644]
extensions/Example/Config.pm [new file with mode: 0644]
extensions/Example/Extension.pm [new file with mode: 0644]
extensions/Example/disabled [new file with mode: 0644]
extensions/Example/lib/AuthLogin.pm [new file with mode: 0644]
extensions/Example/lib/AuthVerify.pm [new file with mode: 0644]
extensions/Example/lib/ConfigExample.pm [new file with mode: 0644]
extensions/Example/lib/WSExample.pm [new file with mode: 0644]
extensions/Example/template/en/default/admin/params/example.html.tmpl [new file with mode: 0644]
extensions/Example/template/en/default/pages/example.html.tmpl [new file with mode: 0644]
extensions/Example/template/en/default/setup/strings.txt.pl [new file with mode: 0644]
extensions/Example/template/en/hook/admin/sanitycheck/messages-statuses.html.tmpl [new file with mode: 0644]
extensions/Example/template/en/hook/global/user-error-errors.html.tmpl [new file with mode: 0644]
mod_perl.pl
page.cgi
post_bug.cgi
sanitycheck.cgi
template/en/default/global/code-error.html.tmpl
template/en/default/setup/strings.txt.pl

index b85186e6ef3fd35e7e3ad32c63c778b9674d13dc..9f4e81cb6abb761af15892f1b39a53e01bdb9799 100644 (file)
@@ -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,
index 42372393ced037ddcbf09059e2a38e025291dd0f..f3210425f08122c0ae5c57d4358908d96f2029d5 100644 (file)
@@ -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.
index a5752f22bbb23fa88eda94347475568a78b379cc..bef9171c9dc798574f6262038751743843c03fe0 100644 (file)
@@ -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)) {
index c23b532fdf8a674d4dcde5a5433905d667d9c758..2df3fcd2538e69f402aa9447e1f2f83a3dde6767 100644 (file)
@@ -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)) {
index c27f23823439124d4dd5b3642aa7ef724755df46..d8134e3cbc7ff03c89deda68a924fdee590c6b1f 100644 (file)
@@ -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;
 }
index cab18b5a159042304d15153d505fe441d25e8f9e..ceb1861ed0618d197d66238519de666464c941a6 100644 (file)
@@ -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;
 }
index c5003f798ea5f357617bc454a59363f5f255e243..f34f05e2f4b59246658b064f930db0b2b292989b 100644 (file)
@@ -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);
     }
index 661c72f740738aea56367ee05b9f871c3d452f55..dbd9688a9938c512865664047dc28741897e3718 100644 (file)
@@ -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 (file)
index 0000000..1046e09
--- /dev/null
@@ -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.
index 8315a3ef6425d158696f9085e6437cc1c4c8b9bd..2fa4c8dedb29dd9b12e096a9d3bb21c3fefff469 100644 (file)
@@ -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,
index dc1cd6be1d33d8ab1bf48e531472ca8b4d47f8c1..24a7d73c2b9c6cb4dff7619cb47cfbc335802652 100644 (file)
 # 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>
index 51d2582274f77c69a48f57a3c113faa5ac9751fe..d7eb14c249448e8dc2efdf9c509041782c964c98 100644 (file)
@@ -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
index 1fa53de9bcc50de25320895693cf8ffb81337a8d..190dbe9682316fb67e76bebc262fabdbc79100d8 100644 (file)
@@ -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 {
index d3fb4e5f88a44d31ec7a1fcdd64247f20873b5fa..254cc237bbeb00b2a8db66bc4d5f9f56e5210c83 100644 (file)
@@ -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;
 }
index 83ae5a60046a0c9b8b5c9d740ab08bd62d4716c6..71dc8f1f51f18c66ce051f7e3be662413b9b73c4 100644 (file)
@@ -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';
index a1857db1c2036ff4599bc2d159ff112ce2ba29ff..92353b6a08b18034249c806af3eee4deba5d022c 100644 (file)
@@ -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;
index 4aaf7e14c2385cf4f2997b7a4944fa6b95f52d3d..7b8ac10e2beb9836d1b2e507a1b870338537a02f 100644 (file)
@@ -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};
index c7b8cee52bc58a925954f1625de459aef788eb95..ce7c2ab1c67a159f5f4bf44de4f654f104687c03 100644 (file)
@@ -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;
index d3e3f7952cf21443c880f003fa3aa1df7c4290b2..8993ff08d56c59f433cf595b30976068b502571e 100755 (executable)
@@ -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
index 6c2fa8090b772d6df1d61aba3674063d75fb3e62..409f15e5a3ab469353ff809b09084a4228125c02 100755 (executable)
@@ -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;
 
index 2c3e4089ef2c0e3d8c34e058188cd18e8945c529..9b4f04e3c16755ba52f7e5b192493c1bf5b10bfb 100755 (executable)
@@ -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;
 
index 1d43354665bd033f957a8ea364923f6c80b177eb..86f2c04050949e45338030974ef23ed740fc0616 100755 (executable)
@@ -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());
index ab0108748b92ae75468b3fd6beaf64088c4e9019..e6e78eaa1fc889244721e1ec96079ff24333379e 100755 (executable)
@@ -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 (file)
index 0000000..1b31491
--- /dev/null
@@ -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 (file)
index 0000000..29113bd
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/extensions/Example/Config.pm b/extensions/Example/Config.pm
new file mode 100644 (file)
index 0000000..378db35
--- /dev/null
@@ -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 (file)
index 0000000..8e3a385
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/extensions/Example/lib/AuthLogin.pm b/extensions/Example/lib/AuthLogin.pm
new file mode 100644 (file)
index 0000000..46147de
--- /dev/null
@@ -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 (file)
index 0000000..2ecf83a
--- /dev/null
@@ -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 (file)
index 0000000..49ffefe
--- /dev/null
@@ -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 (file)
index 0000000..4330633
--- /dev/null
@@ -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 (file)
index 0000000..e2bb5f3
--- /dev/null
@@ -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 (file)
index 0000000..d53f78f
--- /dev/null
@@ -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 (file)
index 0000000..8da19c0
--- /dev/null
@@ -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 (file)
index 0000000..8a825e5
--- /dev/null
@@ -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 (file)
index 0000000..df5a203
--- /dev/null
@@ -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 %]
index 8ca691f9ce1841b94b2598a17421e95f08e4827e..dc737af1317b6ea5908bdc6ca41156b800b6178f 100755 (executable)
@@ -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;
index 914ba3f221237e1467bdef5661289a390897246c..d889841b33e90a20c5c9a84aa941374b80baa8a1 100755 (executable)
--- 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);
index ed483aec9bd1ff04f54005cacf4b82d2376fd870..c9de83bb781a1bf7f3994527d4a0c52b4586eb18 100755 (executable)
@@ -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;
 
index f5ba1024ff7de39a9182eabb1d90771ce8273ed5..6142737696707b1cb3b2528b4cd49ea8e02a62bf 100755 (executable)
@@ -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
index c1ff611594ebe65bbbfca45a1e1b82016d7bd85e..d64b3656a367ffbd9a7e92359b8fc444d989eabf 100644 (file)
     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
index 2a8e993e75efdf4ecacfaa62ee3b69cf3d038c74..eec0bd90eb4c55065970403d9d3aea4569a64129 100644 (file)
@@ -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',