diff options
Diffstat (limited to 'Bugzilla/Install/Util.pm')
-rw-r--r-- | Bugzilla/Install/Util.pm | 918 |
1 files changed, 918 insertions, 0 deletions
diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm new file mode 100644 index 0000000..cbc41db --- /dev/null +++ b/Bugzilla/Install/Util.pm @@ -0,0 +1,918 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Install::Util; + +# The difference between this module and Bugzilla::Util is that this +# module may require *only* Bugzilla::Constants and built-in +# perl modules. + +use strict; + +use Bugzilla::Constants; + +use Encode; +use File::Basename; +use File::Spec; +use POSIX qw(setlocale LC_CTYPE); +use Scalar::Util qw(tainted); +use Term::ANSIColor qw(colored); +use PerlIO; + +use base qw(Exporter); +our @EXPORT_OK = qw( + bin_loc + get_version_and_os + extension_code_files + extension_package_directory + extension_requirement_packages + extension_template_directory + extension_web_directory + indicate_progress + install_string + include_languages + success + template_include_path + vers_cmp + init_console +); + +sub bin_loc { + my ($bin, $path) = @_; + # This module is not needed most of the time and is a bit slow, + # so we only load it when calling bin_loc(). + require ExtUtils::MM; + + # If the binary is a full path... + if ($bin =~ m{[/\\]}) { + return MM->maybe_command($bin) || ''; + } + + # Otherwise we look for it in the path in a cross-platform way. + my @path = $path ? @$path : File::Spec->path; + foreach my $dir (@path) { + next if !-d $dir; + my $full_path = File::Spec->catfile($dir, $bin); + # MM is an alias for ExtUtils::MM. maybe_command is nice + # because it checks .com, .bat, .exe (etc.) on Windows. + my $command = MM->maybe_command($full_path); + return $command if $command; + } + + return ''; +} + +sub get_version_and_os { + # Display version information + my @os_details = POSIX::uname; + # 0 is the name of the OS, 2 is the major version, + my $os_name = $os_details[0] . ' ' . $os_details[2]; + if (ON_WINDOWS) { + require Win32; + $os_name = Win32::GetOSName(); + } + # $os_details[3] is the minor version. + return { bz_ver => BUGZILLA_VERSION, + perl_ver => sprintf('%vd', $^V), + os_name => $os_name, + 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); + } + + my @additional; + my $datadir = bz_locations()->{'datadir'}; + my $addl_file = "$datadir/extensions/additional"; + if (-e $addl_file) { + open(my $fh, '<', $addl_file) || die "$addl_file: $!"; + @additional = map { trim($_) } <$fh>; + close($fh); + } + return (\@files, \@additional); +} + +# 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.) + # + # We check if Bugzilla.pm is already loaded, instead of doing a "require", + # because we *do* want the code lower down to run during the Requirements + # phase of checksetup.pl, instead of Bugzilla->extensions, and Bugzilla.pm + # actually *can* be loaded during the Requirements phase if all the + # requirements have already been installed. + if ($INC{'Bugzilla.pm'}) { + return Bugzilla->extensions; + } + my $packages = _cache()->{extension_requirement_packages}; + return $packages if $packages; + $packages = []; + my %package_map; + + my ($file_sets, $extra_packages) = extension_code_files('requirements only'); + foreach my $file_set (@$file_sets) { + 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"; + if ($package->can('package_dir')) { + $package->package_dir($file); + } + else { + extension_package_directory($package, $file); + } + $package_map{$file} = $package; + push(@$packages, $package); + } + foreach my $package (@$extra_packages) { + eval("require $package") || die $@; + 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; +} + +# Used in this file and in Bugzilla::Extension. +sub extension_template_directory { + my $extension = shift; + my $class = ref($extension) || $extension; + my $base_dir = extension_package_directory($class); + if ($base_dir eq bz_locations->{'extensionsdir'}) { + return bz_locations->{'templatedir'}; + } + return "$base_dir/template"; +} + +# Used in this file and in Bugzilla::Extension. +sub extension_web_directory { + my $extension = shift; + my $class = ref($extension) || $extension; + my $base_dir = extension_package_directory($class); + return "$base_dir/web"; +} + +# For extensions that are in the extensions/ dir, this both sets and fetches +# the name of the directory that stores an extension's "stuff". We need this +# when determining the template directory for extensions (or other things +# that are relative to the extension's base directory). +sub extension_package_directory { + my ($invocant, $file) = @_; + my $class = ref($invocant) || $invocant; + + # $file is set on the first invocation, store the value in the extension's + # package for retrieval on subsequent calls + my $var; + { + no warnings 'once'; + no strict 'refs'; + $var = \${"${class}::EXTENSION_PACKAGE_DIR"}; + } + if ($file) { + $$var = dirname($file); + } + my $value = $$var; + + # This is for extensions loaded from data/extensions/additional. + if (!$value) { + my $short_path = $class; + $short_path =~ s/::/\//g; + $short_path .= ".pm"; + my $long_path = $INC{$short_path}; + die "$short_path is not in \%INC" if !$long_path; + $value = $long_path; + $value =~ s/\.pm//; + } + return $value; +} + +sub indicate_progress { + my ($params) = @_; + my $current = $params->{current}; + my $total = $params->{total}; + my $every = $params->{every} || 1; + + print "." if !($current % $every); + if ($current == $total || $current % ($every * 60) == 0) { + print "$current/$total (" . int($current * 100 / $total) . "%)\n"; + } +} + +sub install_string { + my ($string_id, $vars) = @_; + _cache()->{install_string_path} ||= template_include_path(); + my $path = _cache()->{install_string_path}; + + my $string_template; + # Find the first template that defines this string. + foreach my $dir (@$path) { + my $base = "$dir/setup/strings"; + $string_template = _get_string_from_file($string_id, "$base.txt.pl") + if !defined $string_template; + last if defined $string_template; + } + + die "No language defines the string '$string_id'" + if !defined $string_template; + + utf8::decode($string_template) if !utf8::is_utf8($string_template); + + $vars ||= {}; + my @replace_keys = keys %$vars; + foreach my $key (@replace_keys) { + my $replacement = $vars->{$key}; + die "'$key' in '$string_id' is tainted: '$replacement'" + if tainted($replacement); + # We don't want people to start getting clever and inserting + # ##variable## into their values. So we check if any other + # key is listed in the *replacement* string, before doing + # the replacement. This is mostly to protect programmers from + # making mistakes. + if (grep($replacement =~ /##$key##/, @replace_keys)) { + die "Unsafe replacement for '$key' in '$string_id': '$replacement'"; + } + $string_template =~ s/\Q##$key##\E/$replacement/g; + } + + return $string_template; +} + +sub _wanted_languages { + my ($requested, @wanted); + + # Checking SERVER_SOFTWARE is the same as i_am_cgi() in Bugzilla::Util. + if (exists $ENV{'SERVER_SOFTWARE'}) { + my $cgi = eval { Bugzilla->cgi } || eval { require CGI; return CGI->new() }; + $requested = $cgi->http('Accept-Language') || ''; + my $lang = $cgi->cookie('LANG'); + push(@wanted, $lang) if $lang; + } + else { + $requested = get_console_locale(); + } + + push(@wanted, _sort_accept_language($requested)); + return \@wanted; +} + +sub _wanted_to_actual_languages { + my ($wanted, $supported) = @_; + + my @actual; + foreach my $lang (@$wanted) { + # If we support the language we want, or *any version* of + # the language we want, it gets pushed into @actual. + # + # Per RFC 1766 and RFC 2616, things like 'en' match 'en-us' and + # 'en-uk', but not the other way around. (This is unfortunately + # not very clearly stated in those RFC; see comment just over 14.5 + # in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4) + my @found = grep(/^\Q$lang\E(-.+)?$/i, @$supported); + push(@actual, @found) if @found; + } + + # We always include English at the bottom if it's not there, even if + # it wasn't selected by the user. + if (!grep($_ eq 'en', @actual)) { + push(@actual, 'en'); + } + + return \@actual; +} + +sub supported_languages { + my $cache = _cache(); + return $cache->{supported_languages} if $cache->{supported_languages}; + + my @dirs = glob(bz_locations()->{'templatedir'} . "/*"); + my @languages; + foreach my $dir (@dirs) { + # It's a language directory only if it contains "default" or + # "custom". This auto-excludes CVS directories as well. + next if (!-d "$dir/default" and !-d "$dir/custom"); + my $lang = basename($dir); + # Check for language tag format conforming to RFC 1766. + next unless $lang =~ /^[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?$/; + push(@languages, $lang); + } + + $cache->{supported_languages} = \@languages; + return \@languages; +} + +sub include_languages { + my ($params) = @_; + + # Basically, the way this works is that we have a list of languages + # that we *want*, and a list of languages that Bugzilla actually + # supports. If there is only one language installed, we take it. + my $supported = supported_languages(); + return @$supported if @$supported == 1; + + my $wanted; + if ($params->{language}) { + # We can pass several languages at once as an arrayref + # or a single language. + $wanted = $params->{language}; + $wanted = [$wanted] unless ref $wanted; + } + else { + $wanted = _wanted_languages(); + } + my $actual = _wanted_to_actual_languages($wanted, $supported); + return @$actual; +} + +# Used by template_include_path +sub _template_lang_directories { + my ($languages, $templatedir) = @_; + + my @add = qw(custom default); + my $project = bz_locations->{'project'}; + unshift(@add, $project) if $project; + + my @result; + foreach my $lang (@$languages) { + foreach my $dir (@add) { + my $full_dir = "$templatedir/$lang/$dir"; + if (-d $full_dir) { + trick_taint($full_dir); + push(@result, $full_dir); + } + } + } + return @result; +} + +# Used by template_include_path. +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. + # + # We use extension_requirement_packages instead of Bugzilla->extensions + # because this fucntion is called during the requirements phase of + # installation (so Bugzilla->extensions isn't available). + my $extensions = extension_requirement_packages(); + my @template_dirs; + foreach my $extension (@$extensions) { + my $dir; + # If there's a template_dir method available in the extension + # package, then call it. Note that this has to be defined in + # Config.pm for extensions that have a Config.pm, to be effective + # during the Requirements phase of checksetup.pl. + if ($extension->can('template_dir')) { + $dir = $extension->template_dir; + } + else { + $dir = extension_template_directory($extension); + } + if (-d $dir) { + push(@template_dirs, $dir); + } + } + + # Extensions may also contain *only* templates, in which case they + # won't show up in extension_requirement_packages. + foreach my $path (_extension_paths()) { + next if !-d $path; + if (!-e "$path/Extension.pm" and !-e "$path/Config.pm" + and -d "$path/template") + { + push(@template_dirs, "$path/template"); + } + } + + + push(@template_dirs, bz_locations()->{'templatedir'}); + return \@template_dirs; +} + +sub template_include_path { + my ($params) = @_; + my @used_languages = include_languages($params); + # Now, we add template directories in the order they will be searched: + my $template_dirs = _template_base_directories(); + + my @include_path; + foreach my $template_dir (@$template_dirs) { + my @lang_dirs = _template_lang_directories(\@used_languages, + $template_dir); + # Hooks get each set of extension directories separately. + if ($params->{hook}) { + push(@include_path, \@lang_dirs); + } + # Whereas everything else just gets a whole INCLUDE_PATH. + else { + push(@include_path, @lang_dirs); + } + } + return \@include_path; +} + +# This is taken straight from Sort::Versions 1.5, which is not included +# with perl by default. +sub vers_cmp { + my ($a, $b) = @_; + + # Remove leading zeroes - Bug 344661 + $a =~ s/^0*(\d.+)/$1/; + $b =~ s/^0*(\d.+)/$1/; + + my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g); + my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g); + + my ($A, $B); + while (@A and @B) { + $A = shift @A; + $B = shift @B; + if ($A eq '-' and $B eq '-') { + next; + } elsif ( $A eq '-' ) { + return -1; + } elsif ( $B eq '-') { + return 1; + } elsif ($A eq '.' and $B eq '.') { + next; + } elsif ( $A eq '.' ) { + return -1; + } elsif ( $B eq '.' ) { + return 1; + } elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) { + if ($A =~ /^0/ || $B =~ /^0/) { + return $A cmp $B if $A cmp $B; + } else { + return $A <=> $B if $A <=> $B; + } + } else { + $A = uc $A; + $B = uc $B; + return $A cmp $B if $A cmp $B; + } + } + @A <=> @B; +} + +sub no_checksetup_from_cgi { + print "Content-Type: text/html; charset=UTF-8\r\n\r\n"; + print install_string('no_checksetup_from_cgi'); + exit; +} + +###################### +# Helper Subroutines # +###################### + +# Used by install_string +sub _get_string_from_file { + my ($string_id, $file) = @_; + # If we already loaded the file, then use its copy from the cache. + if (my $strings = _cache()->{strings_from_file}->{$file}) { + return $strings->{$string_id}; + } + + # This module is only needed by checksetup.pl, + # so only load it when needed. + require Safe; + + return undef if !-e $file; + my $safe = new Safe; + $safe->rdo($file); + my %strings = %{$safe->varglob('strings')}; + _cache()->{strings_from_file}->{$file} = \%strings; + return $strings{$string_id}; +} + +# Make an ordered list out of a HTTP Accept-Language header (see RFC 2616, 14.4) +# We ignore '*' and <language-range>;q=0 +# For languages with the same priority q the order remains unchanged. +sub _sort_accept_language { + sub sortQvalue { $b->{'qvalue'} <=> $a->{'qvalue'} } + my $accept_language = $_[0]; + + # clean up string. + $accept_language =~ s/[^A-Za-z;q=0-9\.\-,]//g; + my @qlanguages; + my @languages; + foreach(split /,/, $accept_language) { + if (m/([A-Za-z\-]+)(?:;q=(\d(?:\.\d+)))?/) { + my $lang = $1; + my $qvalue = $2; + $qvalue = 1 if not defined $qvalue; + next if $qvalue == 0; + $qvalue = 1 if $qvalue > 1; + push(@qlanguages, {'qvalue' => $qvalue, 'language' => $lang}); + } + } + + return map($_->{'language'}, (sort sortQvalue @qlanguages)); +} + +sub get_console_locale { + require Locale::Language; + my $locale = setlocale(LC_CTYPE); + my $language; + # Some distros set e.g. LC_CTYPE = fr_CH.UTF-8. We clean it up. + if ($locale =~ /^([^\.]+)/) { + $locale = $1; + } + $locale =~ s/_/-/; + # It's pretty sure that there is no language pack of the form fr-CH + # installed, so we also include fr as a wanted language. + if ($locale =~ /^(\S+)\-/) { + $language = $1; + $locale .= ",$language"; + } + else { + $language = $locale; + } + + # Some OSs or distributions may have setlocale return a string of the form + # German_Germany.1252 (this example taken from a Windows XP system), which + # is unsuitable for our needs because Bugzilla works on language codes. + # We try and convert them here. + if ($language = Locale::Language::language2code($language)) { + $locale .= ",$language"; + } + + return $locale; +} + +sub set_output_encoding { + # If we've already set an encoding layer on STDOUT, don't + # add another one. + my @stdout_layers = PerlIO::get_layers(STDOUT); + return if grep(/^encoding/, @stdout_layers); + + my $encoding; + if (ON_WINDOWS and eval { require Win32::Console }) { + # Although setlocale() works on Windows, it doesn't always return + # the current *console's* encoding. So we use OutputCP here instead, + # when we can. + $encoding = Win32::Console::OutputCP(); + } + else { + my $locale = setlocale(LC_CTYPE); + if ($locale =~ /\.([^\.]+)$/) { + $encoding = $1; + } + } + $encoding = "cp$encoding" if ON_WINDOWS; + + $encoding = Encode::resolve_alias($encoding) if $encoding; + if ($encoding and $encoding !~ /utf-8/i) { + binmode STDOUT, ":encoding($encoding)"; + binmode STDERR, ":encoding($encoding)"; + } + else { + binmode STDOUT, ':utf8'; + binmode STDERR, ':utf8'; + } +} + +sub init_console { + eval { ON_WINDOWS && require Win32::Console::ANSI; }; + $ENV{'ANSI_COLORS_DISABLED'} = 1 if ($@ || !-t *STDOUT); + $SIG{__DIE__} = \&_console_die; + prevent_windows_dialog_boxes(); + set_output_encoding(); +} + +sub _console_die { + my ($message) = @_; + # $^S means "we are in an eval" + if ($^S) { + die $message; + } + # Remove newlines from the message before we color it, and then + # add them back in on display. Otherwise the ANSI escape code + # for resetting the color comes after the newline, and Perl thinks + # that it should put "at Bugzilla/Install.pm line 1234" after the + # message. + $message =~ s/\n+$//; + # We put quotes around the message to stringify any object exceptions, + # like Template::Exception. + die colored("$message", COLOR_ERROR) . "\n"; +} + +sub success { + my ($message) = @_; + print colored($message, COLOR_SUCCESS), "\n"; +} + +sub prevent_windows_dialog_boxes { + # This code comes from http://bugs.activestate.com/show_bug.cgi?id=82183 + # and prevents Perl modules from popping up dialog boxes, particularly + # during checksetup (since loading DBD::Oracle during checksetup when + # Oracle isn't installed causes a scary popup and pauses checksetup). + # + # Win32::API ships with ActiveState by default, though there could + # theoretically be a Windows installation without it, I suppose. + if (ON_WINDOWS and eval { require Win32::API }) { + # Call kernel32.SetErrorMode with arguments that mean: + # "The system does not display the critical-error-handler message box. + # Instead, the system sends the error to the calling process." and + # "A child process inherits the error mode of its parent process." + my $SetErrorMode = Win32::API->new('kernel32', 'SetErrorMode', + 'I', 'I'); + my $SEM_FAILCRITICALERRORS = 0x0001; + my $SEM_NOGPFAULTERRORBOX = 0x0002; + $SetErrorMode->Call($SEM_FAILCRITICALERRORS | $SEM_NOGPFAULTERRORBOX); + } +} + +# This is like request_cache, but it's used only by installation code +# for checksetup.pl and things like that. +our $_cache = {}; +sub _cache { + # 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; +} + +############################### +# Copied from Bugzilla::Util # +############################## + +sub trick_taint { + require Carp; + Carp::confess("Undef to trick_taint") unless defined $_[0]; + my $match = $_[0] =~ /^(.*)$/s; + $_[0] = $match ? $1 : undef; + return (defined($_[0])); +} + +sub trim { + my ($str) = @_; + if ($str) { + $str =~ s/^\s+//g; + $str =~ s/\s+$//g; + } + return $str; +} + +__END__ + +=head1 NAME + +Bugzilla::Install::Util - Utility functions that are useful both during +installation and afterwards. + +=head1 DESCRIPTION + +This module contains various subroutines that are used primarily +during installation. However, these subroutines can also be useful to +non-installation code, so they have been split out into this module. + +The difference between this module and L<Bugzilla::Util> is that this +module is safe to C<use> anywhere in Bugzilla, even during installation, +because it depends only on L<Bugzilla::Constants> and built-in perl modules. + +None of the subroutines are exported by default--you must explicitly +export them. + +=head1 SUBROUTINES + +=over + +=item C<bin_loc> + +On *nix systems, given the name of a binary, returns the path to that +binary, if the binary is in the C<PATH>. + +=item C<get_version_and_os> + +Returns a hash containing information about what version of Bugzilla we're +running, what perl version we're using, and what OS we're running on. + +=item C<get_console_locale> + +Returns the language to use based on the LC_CTYPE value returned by the OS. +If LC_CTYPE is of the form fr-CH, then fr is appended to the list. + +=item C<init_console> + +Sets the C<ANSI_COLORS_DISABLED> and C<HTTP_ACCEPT_LANGUAGE> environment variables. + +=item C<indicate_progress> + +=over + +=item B<Description> + +This prints out lines of dots as a long update is going on, to let the user +know where we are and that we're not frozen. A new line of dots will start +every 60 dots. + +Sample usage: C<indicate_progress({ total =E<gt> $total, current =E<gt> +$count, every =E<gt> 1 })> + +=item B<Sample Output> + +Here's some sample output with C<total = 1000> and C<every = 10>: + + ............................................................600/1000 (60%) + ........................................ + +=item B<Params> + +=over + +=item C<total> - The total number of items we're processing. + +=item C<current> - The number of the current item we're processing. + +=item C<every> - How often the function should print out a dot. +For example, if this is 10, the function will print out a dot every +ten items. Defaults to 1 if not specified. + +=back + +=item B<Returns>: nothing + +=back + +=item C<install_string> + +=over + +=item B<Description> + +This is a very simple method of templating strings for installation. +It should only be used by code that has to run before the Template Toolkit +can be used. (See the comments at the top of the various L<Bugzilla::Install> +modules to find out when it's safe to use Template Toolkit.) + +It pulls strings out of the F<strings.txt.pl> "template" and replaces +any variable surrounded by double-hashes (##) with a value you specify. + +This allows for localization of strings used during installation. + +=item B<Example> + +Let's say your template string looks like this: + + The ##animal## jumped over the ##plant##. + +Let's say that string is called 'animal_jump_plant'. So you call the function +like this: + + install_string('animal_jump_plant', { animal => 'fox', plant => 'tree' }); + +That will output this: + + The fox jumped over the tree. + +=item B<Params> + +=over + +=item C<$string_id> - The name of the string from F<strings.txt.pl>. + +=item C<$vars> - A hashref containing the replacement values for variables +inside of the string. + +=back + +=item B<Returns>: The appropriate string, with variables replaced. + +=back + +=item C<template_include_path> + +Used by L<Bugzilla::Template> and L</install_string> to determine the +directories where templates are installed. Templates can be installed +in many places. They're listed here in the basic order that they're +searched: + +=over + +=item extensions/C<$extension>/template/C<$language>/C<$project> + +=item extensions/C<$extension>/template/C<$language>/custom + +=item extensions/C<$extension>/template/C<$language>/default + +=item template/C<$language>/C<$project> + +=item template/C<$language>/custom + +=item template/C<$language>/default + +=back + +C<$project> has to do with installations that are using the C<$ENV{PROJECT}> +variable to have different "views" on a single Bugzilla. + +The F<default> directory includes templates shipped with Bugzilla. + +The F<custom> directory is a directory for local installations to override +the F<default> templates. Any individual template in F<custom> will +override a template of the same name and path in F<default>. + +C<$language> is a language code, C<en> being the default language shipped +with Bugzilla. Localizers ship other languages. + +C<$extension> is the name of any directory in the F<extensions/> directory. +Each extension has its own directory. + +Note that languages are sorted by the user's preference (as specified +in their browser, usually), and extensions are sorted alphabetically. + +=item C<include_languages> + +Used by L<Bugzilla::Template> to determine the languages' list which +are compiled with the browser's I<Accept-Language> and the languages +of installed templates. + +=item C<vers_cmp> + +=over + +=item B<Description> + +This is a comparison function, like you would use in C<sort>, except that +it compares two version numbers. So, for example, 2.10 would be greater +than 2.2. + +It's based on versioncmp from L<Sort::Versions>, with some Bugzilla-specific +fixes. + +=item B<Params>: C<$a> and C<$b> - The versions you want to compare. + +=item B<Returns> + +C<-1> if C<$a> is less than C<$b>, C<0> if they are equal, or C<1> if C<$a> +is greater than C<$b>. + +=back + +=back |