# -*- 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 package Bugzilla::Extension; use strict; use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Install::Util qw( extension_code_files extension_template_directory extension_package_directory); use File::Basename; use File::Spec; #################### # 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) = @_; 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). During checksetup.pl, extensions are loaded # once by Bugzilla::Install::Requirements, and then later again via # Bugzilla->extensions (because of hooks). my $map = Bugzilla->request_cache->{extension_requirement_package_map}; if ($config_file) { if ($map and defined $map->{$config_file}) { $package = $map->{$config_file}; } else { my $name = require $config_file; if ($name =~ /^\d+$/) { ThrowCodeError('extension_must_return_name', { extension => $config_file, returned => $name }); } $package = "${class}::$name"; } __do_call($package, 'modify_inc', $config_file); } if ($map and defined $map->{$extension_file}) { $package = $map->{$extension_file}; $package->modify_inc($extension_file) if !$config_file; } else { my $name = require $extension_file; if ($name =~ /^\d+$/) { ThrowCodeError('extension_must_return_name', { extension => $extension_file, returned => $name }); } $package = "${class}::$name"; $package->modify_inc($extension_file) if !$config_file; } $class->_validate_package($package, $extension_file); return $package; } sub _validate_package { my ($class, $package, $extension_file) = @_; # For extensions from data/extensions/additional, we don't have a file # name, so we fake it. if (!$extension_file) { $extension_file = $package; $extension_file =~ s/::/\//g; $extension_file .= '.pm'; } 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 }); } } sub load_all { my $class = shift; my ($file_sets, $extra_packages) = extension_code_files(); my @packages; foreach my $file_set (@$file_sets) { my $package = $class->load(@$file_set); push(@packages, $package); } # Extensions from data/extensions/additional foreach my $package (@$extra_packages) { # Don't load an "additional" extension if we already have an extension # loaded with that name. next if grep($_ eq $package, @packages); # Untaint the package name $package =~ /([\w:]+)/; $package = $1; eval("require $package") || die $@; $package->_validate_package($package); push(@packages, $package); } return \@packages; } # Modifies @INC so that extensions can use modules like # "use Bugzilla::Extension::Foo::Bar", when Bar.pm is in the lib/ # directory of the extension. sub modify_inc { my ($class, $file) = @_; __do_call($class, 'package_dir', $file); unshift(@INC, sub { __do_call($class, 'my_inc', @_) }); } # This is what gets put into @INC by modify_inc. sub my_inc { my ($class, undef, $file) = @_; my $lib_dir = __do_call($class, 'lib_dir'); my @class_parts = split('::', $class); my ($vol, $dir, $file_name) = File::Spec->splitpath($file); my @dir_parts = File::Spec->splitdir($dir); # Validate that this is a sub-package of Bugzilla::Extension::Foo ($class). for (my $i = 0; $i < scalar(@class_parts); $i++) { return if !@dir_parts; if (File::Spec->case_tolerant) { return if lc($class_parts[$i]) ne lc($dir_parts[0]); } else { return if $class_parts[$i] ne $dir_parts[0]; } shift(@dir_parts); } # For Bugzilla::Extension::Foo::Bar, this would look something like # extensions/Example/lib/Bar.pm my $resolved_path = File::Spec->catfile($lib_dir, @dir_parts, $file_name); open(my $fh, '<', $resolved_path); return $fh; } #################### # Instance Methods # #################### use constant enabled => 1; sub lib_dir { my $invocant = shift; my $package_dir = __do_call($invocant, 'package_dir'); return File::Spec->catdir($package_dir, 'lib'); } sub template_dir { return extension_template_directory(@_); } sub package_dir { return extension_package_directory(@_); } ###################### # Helper Subroutines # ###################### # In order to not conflict with extensions' private subroutines, any helpers # here should start with a double underscore. # This is for methods that can optionally be overridden in Config.pm. # It falls back to the local implementation if $class cannot do # the method. This is necessary because Config.pm is not a subclass of # Bugzilla::Extension. sub __do_call { my ($class, $method, @args) = @_; if ($class->can($method)) { return $class->$method(@args); } my $function_ref; { no strict 'refs'; $function_ref = \&{$method}; } return $function_ref->($class, @args); } 1; __END__ =head1 NAME Bugzilla::Extension - Base class for Bugzilla Extensions. =head1 SYNOPSIS The following would be in F or F: 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 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 Using F There is a script, L, that will set up the framework of a new extension for you. To use it, pick a name for your extension and, in the base bugzilla directory, do: C But replace C with the name you picked for your extension. That will create a new directory in the F directory with the name of your extension. The directory will contain a full framework for a new extension, with helpful comments in each file describing things about them. =head2 Example Extension There is a sample extension in F 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 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 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. =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 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. =back =head2 The Extension C. The "name" of an extension shows up in several places: =over =item 1 The name of the package: C =item 2 In a C constant that B 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 end with C<< __PACKAGE__->NAME; >>. =back The name must be identical in all of those locations. =head2 Hooks In L, there is a L. 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 that gets an argument named C: sub foo_start { my ($self, $args) = @_; my $bar = $args->{bar}; print "I got $bar!\n"; } And that would go into your extension's code file--the file that was described in the L 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 . =head3 Adding New Hooks To Bugzilla If you need a new hook for your extension and you want that hook to be added to Bugzilla itself, see our development process at L. In order for a new hook to be accepted into Bugzilla, it has to work, it must have documentation in L, and it must have example code in F. One question that is often asked about new hooks is, "Is this the most flexible way to implement this hook?" That is, the more power extension authors get from a hook, the more likely it is to be accepted into Bugzilla. Hooks that only hook a very specific part of Bugzilla will not be accepted if their functionality can be accomplished equally well with a more generic hook. =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 will make sure that those modules are installed, when you run L. To do this, you need to specify a constant called C in your extension. This constant has the same format as L. 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. =head3 If Your Extension Needs Certain Modules In Order To Compile If your extension needs a particular Perl module in order to I, then you have a "chicken and egg" problem--in order to read C, we have to compile your extension. In order to compile your extension, we need to already have the modules in C! To get around this problem, Bugzilla allows you to have an additional file, besides F, called F, that contains just C. If you have a F, it must also contain the C constant, instead of your main F containing the C constant. The contents of the file would look something like this for an extension named C: 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 a subclass of C, because at the time that module requirements are being checked in L, C cannot be loaded. Also, just like F, it ends with C<< __PACKAGE__->NAME; >>. Note also that it has the exact same C name as F. This file may not use any Perl modules other than L, L, L, and modules that ship with Perl itself. If you want to define both C and C, they must both be in F or both in F. Every time your extension is loaded by Bugzilla, F will be read and then F will be read, so your methods in F will have access to everything in F. 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 is of course not available if your extension is a single file named C. If any of this is confusing, just look at the code of the Example extension. It uses this method to specify requirements. =head2 Libraries Extensions often want to have their own Perl modules. Your extension can load any Perl module in its F directory. (So, if your extension is F, then your Perl modules go into F.) However, the C name of your libraries will not work quite like normal Perl modules do. F is loaded as C. Or, to say it another way, C loads F, which should have C as its package name. This allows any place in Bugzilla to load your modules, which is important for some hooks. It even allows other extensions to load your modules. It even allows you to install your modules into the global Perl install as F, if you'd like, which helps allow CPAN distribution of Bugzilla extensions. B If you want to C or C a module that's in F at the top level of your F, you must have a F (see above) with at least the C constant defined in it. =head2 Templates Extensions store templates in a C