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;
use DateTime::TimeZone;
use Safe;
-# This creates the request cache for non-mod_perl installations.
-our $_request_cache = {};
-
#####################################################################
# Constants
#####################################################################
}
}
-init_page() if !$ENV{MOD_PERL};
-
#####################################################################
# Subroutines and Methods
#####################################################################
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;
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;
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;
_cleanup() unless $ENV{MOD_PERL};
}
+init_page() if !$ENV{MOD_PERL};
+
1;
__END__
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,
$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.
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)) {
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)) {
$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;
}
$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,
});
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;
$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,
});
# Custom Fields
map { $_->name } Bugzilla->active_custom_fields
);
- Bugzilla::Hook::process("bug-fields", {'fields' => \@fields} );
+ Bugzilla::Hook::process('bug_fields', {'fields' => \@fields} );
return @fields;
}
}
# 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
$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;
}
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);
}
# 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) {
--- /dev/null
+# -*- 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.
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,
# 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__
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
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
=back
-=head2 auth-login_methods
+=head2 auth_login_methods
This allows you to add new login types to Bugzilla.
(See 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
=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.
=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.
=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.
=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
=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
=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
=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
=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.
=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
=back
-=head2 enter_bug-entrydefaultvars
+=head2 enter_bug_entrydefaultvars
This happens right before the template is loaded on enter_bug.cgi.
=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
=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.
=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
=back
-=head2 mailer-before_send
+=head2 mailer_before_send
Called right before L<Bugzilla::Mailer> sends a message to the MTA.
=back
-=head2 object-before_create
+=head2 object_before_create
This happens at the beginning of L<Bugzilla::Object/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
=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.
=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
=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.
=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,
=back
-=head2 product-confirm_delete
+=head2 product_confirm_delete
Called before displaying the confirmation message when deleting a product.
=back
-=head2 sanitycheck-check
+=head2 sanitycheck_check
This hook allows for extra sanity checks to be added, for use by
F<sanitycheck.cgi>.
=back
-=head2 sanitycheck-repair
+=head2 sanitycheck_repair
This hook allows for extra sanity check repairs to be made, for use by
F<sanitycheck.cgi>.
=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
=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
=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.
See L<Bugzilla::WebService::Constants/WS_ERROR_CODE> for an example.
=back
+
+=head1 SEE ALSO
+
+L<Bugzilla::Extension>
# 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
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;
},
);
- 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 {
},
);
- 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
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 {
our @EXPORT_OK = qw(
bin_loc
get_version_and_os
+ extension_code_files
+ extension_requirement_packages
indicate_progress
install_string
include_languages
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};
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.
# 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;
}
# 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;
# 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;
}
}
# 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;
}
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';
superclass => __PACKAGE__,
function => 'Bugzilla::Object->set' });
- Bugzilla::Hook::process('object-before_set',
+ Bugzilla::Hook::process('object_before_set',
{ object => $self, field => $field,
value => $value });
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 });
}
$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 });
# 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) {
$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;
# 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};
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(@_);
}
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 });
},
};
- 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;
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
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;
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;
$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());
$vars->{'group'} = \@groups;
-Bugzilla::Hook::process("enter_bug-entrydefaultvars", { vars => $vars });
+Bugzilla::Hook::process('enter_bug_entrydefaultvars', { vars => $vars });
$vars->{'default'} = \%default;
--- /dev/null
+# -*- 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;
--- /dev/null
+# -*- 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;
--- /dev/null
+# -*- 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;
--- /dev/null
+# -*- 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;
--- /dev/null
+# -*- 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;
--- /dev/null
+# -*- 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;
--- /dev/null
+# -*- 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;
--- /dev/null
+# -*- 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;
--- /dev/null
+[%#
+ # 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",
+}
+%]
--- /dev/null
+[%#
+ # 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 %]
--- /dev/null
+# 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;
--- /dev/null
+[%# -*- 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 <[% login FILTER html %]> 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 %]
--- /dev/null
+[%# 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 %]
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
use Bugzilla ();
use Bugzilla::Constants ();
use Bugzilla::CGI ();
+use Bugzilla::Extension ();
use Bugzilla::Install::Requirements ();
use Bugzilla::Mailer ();
use Bugzilla::Template ();
$rl->handler($file, $file);
}
+# And now pre-load all extensions
+$Bugzilla::extension_packages = Bugzilla::Extension->load_all();
package Bugzilla::ModPerl::ResponseHandler;
use strict;
}
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);
$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;
# Repair hook
###########################################################################
-Bugzilla::Hook::process("sanitycheck-repair", { status => \&Status });
+Bugzilla::Hook::process('sanitycheck_repair', { status => \&Status });
###########################################################################
# Checks
# Check hook
###########################################################################
-Bugzilla::Hook::process("sanitycheck-check", { status => \&Status });
+Bugzilla::Hook::process('sanitycheck_check', { status => \&Status });
###########################################################################
# End
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
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',