# 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::Requirements;

# NOTE: This package MUST NOT "use" any Bugzilla modules other than
# Bugzilla::Constants, anywhere. We may "use" standard perl modules.
#
# Subroutines may "require" and "import" from modules, but they
# MUST NOT "use."

use 5.10.1;
use strict;
use warnings;

use Bugzilla::Constants;
use Bugzilla::Install::Util qw(install_string bin_loc
  extension_requirement_packages);
use List::Util qw(max);
use Term::ANSIColor;

use parent qw(Exporter);
our @EXPORT = qw(
  REQUIRED_MODULES
  OPTIONAL_MODULES
  FEATURE_FILES

  check_requirements
  check_graphviz
  have_vers
  install_command
  map_files_to_features
);

# This is how many *'s are in the top of each "box" message printed
# by checksetup.pl.
use constant TABLE_WIDTH => 71;

# Optional Apache modules that have no Perl component to them.
# If these are installed, Bugzilla has additional functionality.
#
# The keys are the names of the modules, the values are what the module
# is called in the output of "apachectl -t -D DUMP_MODULES".
use constant APACHE_MODULES => {
  mod_headers => 'headers_module',
  mod_env     => 'env_module',
  mod_expires => 'expires_module',
  mod_rewrite => 'rewrite_module',
  mod_version => 'version_module'
};

# These are all of the binaries that we could possibly use that can
# give us info about which Apache modules are installed.
# If we can't use "apachectl", the "httpd" binary itself takes the same
# parameters. Note that on Debian and Gentoo, there is an "apache2ctl",
# but it takes different parameters on each of those two distros, so we
# don't use apache2ctl.
use constant APACHE => qw(apachectl httpd apache2 apache);

# If we don't find any of the above binaries in the normal PATH,
# these are extra places we look.
use constant APACHE_PATH => [qw(
    /usr/sbin
    /usr/local/sbin
    /usr/libexec
    /usr/local/libexec
    )];

# The below two constants are subroutines so that they can implement
# a hook. Other than that they are actually constants.

# "package" is the perl package we're checking for. "module" is the name
# of the actual module we load with "require" to see if the package is
# installed or not. "version" is the version we need, or 0 if we'll accept
# any version.
#
# "blacklist" is an arrayref of regular expressions that describe versions that
# are 'blacklisted'--that is, even if the version is high enough, Bugzilla
# will refuse to say that it's OK to run with that version.
sub REQUIRED_MODULES {
  my @modules = (
    {
      package => 'CGI.pm',
      module  => 'CGI',

      # 3.51 fixes a security problem that affects Bugzilla.
      # (bug 591165)
      version => '3.51',
    },
    {package => 'Digest-SHA', module => 'Digest::SHA', version => 0},

    # 0.23 fixes incorrect handling of 1/2 & 3/4 timezones.
    {package => 'TimeDate', module => 'Date::Format', version => '2.23'},

    # 0.75 fixes a warning thrown with Perl 5.17 and newer.
    {package => 'DateTime', module => 'DateTime', version => '0.75'},

    # 1.64 fixes a taint issue preventing the local timezone from
    # being determined on some systems.
    {
      package => 'DateTime-TimeZone',
      module  => 'DateTime::TimeZone',
      version => '1.64'
    },

    # 1.54 is required for Perl 5.10+. It also makes DBD::Oracle happy.
    {
      package => 'DBI',
      module  => 'DBI',
      version => ($^V >= v5.13.3) ? '1.614' : '1.54'
    },

    # 2.24 contains several useful text virtual methods.
    {package => 'Template-Toolkit', module => 'Template', version => '2.24'},

    # 1.300011 has a debug mode for SMTP and automatically pass -i to sendmail.
    {package => 'Email-Sender', module => 'Email::Sender', version => '1.300011',},
    {
      package => 'Email-MIME',
      module  => 'Email::MIME',

      # This fixes a memory leak in walk_parts that affected jobqueue.pl.
      version => '1.904'
    },
    {
      package => 'URI',
      module  => 'URI',

      # Follows RFC 3986 to escape characters in URI::Escape.
      version => '1.55',
    },

    # 0.32 fixes several memory leaks in the XS version of some functions.
    {package => 'List-MoreUtils', module => 'List::MoreUtils', version => 0.32,},
    {
      package => 'Math-Random-ISAAC',
      module  => 'Math::Random::ISAAC',
      version => '1.0.1',
    },
    {
      package => 'JSON-XS',
      module  => 'JSON::XS',

      # 2.0 is the first version that will work with JSON::RPC.
      version => '2.01',
    },
  );

  if (ON_WINDOWS) {
    push(
      @modules,
      {
        package => 'Win32',
        module  => 'Win32',

        # 0.35 fixes a memory leak in GetOSVersion, which we use.
        version => 0.35,
      },
      {
        package => 'Win32-API',
        module  => 'Win32::API',

        # 0.55 fixes a bug with char* that might affect Bugzilla::RNG.
        version => '0.55',
      },
      {
        package => 'DateTime-TimeZone-Local-Win32',
        module  => 'DateTime::TimeZone::Local::Win32',

        # We require DateTime::TimeZone 1.64, so this version must match.
        version => '1.64',
      }
    );
  }

  my $extra_modules = _get_extension_requirements('REQUIRED_MODULES');
  push(@modules, @$extra_modules);
  return \@modules;
}

sub OPTIONAL_MODULES {
  my @modules = (
    {
      package => 'GD',
      module  => 'GD',
      version => '1.20',
      feature => [qw(graphical_reports new_charts old_charts)],
    },
    {
      package => 'Chart',
      module  => 'Chart::Lines',

      # Versions below 2.4.1 cannot be compared accurately, see
      # https://rt.cpan.org/Public/Bug/Display.html?id=28218.
      version => '2.4.1',
      feature => [qw(new_charts old_charts)],
    },
    {
      package => 'Template-GD',

      # This module tells us whether or not Template-GD is installed
      # on Template-Toolkits after 2.14, and still works with 2.14 and lower.
      module  => 'Template::Plugin::GD::Image',
      version => 0,
      feature => ['graphical_reports'],
    },
    {
      package => 'GDTextUtil',
      module  => 'GD::Text',
      version => 0,
      feature => ['graphical_reports'],
    },
    {
      package => 'GDGraph',
      module  => 'GD::Graph',
      version => 0,
      feature => ['graphical_reports'],
    },
    {
      package => 'MIME-tools',

      # MIME::Parser is packaged as MIME::Tools on ActiveState Perl
      module  => ON_WINDOWS ? 'MIME::Tools' : 'MIME::Parser',
      version => '5.406',
      feature => ['moving'],
    },
    {
      package => 'libwww-perl',
      module  => 'LWP::UserAgent',
      version => 0,
      feature => ['updates'],
    },
    {
      package => 'XML-Twig',
      module  => 'XML::Twig',
      version => 0,
      feature => ['moving', 'updates'],
    },
    {
      package => 'PatchReader',
      module  => 'PatchReader',

      # 0.9.6 fixes two notable bugs and significantly improves the UX.
      version => '0.9.6',
      feature => ['patch_viewer'],
    },
    {
      package => 'perl-ldap',
      module  => 'Net::LDAP',
      version => 0,
      feature => ['auth_ldap'],
    },
    {
      package => 'Authen-SASL',
      module  => 'Authen::SASL',
      version => 0,
      feature => ['smtp_auth'],
    },
    {
      package => 'Net-SMTP-SSL',
      module  => 'Net::SMTP::SSL',
      version => 1.01,
      feature => ['smtp_ssl'],
    },
    {
      package => 'RadiusPerl',
      module  => 'Authen::Radius',
      version => 0,
      feature => ['auth_radius'],
    },

    # XXX - Once we require XMLRPC::Lite 0.717 or higher, we can
    # remove SOAP::Lite from the list.
    {
      package => 'SOAP-Lite',
      module  => 'SOAP::Lite',

      # Fixes various bugs, including 542931 and 552353 + stops
      # throwing warnings with Perl 5.12.
      version => '0.712',

      # SOAP::Transport::HTTP 1.12 is bogus.
      blacklist => ['^1\.12$'],
      feature   => ['xmlrpc'],
    },

    # Since SOAP::Lite 1.0, XMLRPC::Lite is no longer included
    # and so it must be checked separately.
    {
      package => 'XMLRPC-Lite',
      module  => 'XMLRPC::Lite',
      version => '0.712',
      feature => ['xmlrpc'],
    },
    {
      package => 'JSON-RPC',
      module  => 'JSON::RPC',
      version => 0,
      feature => ['jsonrpc', 'rest'],
    },
    {
      package => 'Test-Taint',
      module  => 'Test::Taint',

      # 1.06 no longer throws warnings with Perl 5.10+.
      version => 1.06,
      feature => ['jsonrpc', 'xmlrpc', 'rest'],
    },
    {
      # We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber.
      package => 'HTML-Parser',
      module  => 'HTML::Parser',
      version => ($^V >= v5.13.3) ? '3.67' : '3.40',
      feature => ['html_desc'],
    },
    {
      package => 'HTML-Scrubber',
      module  => 'HTML::Scrubber',
      version => 0,
      feature => ['html_desc'],
    },
    {
      # we need version 2.21 of Encode for mime_name
      package => 'Encode',
      module  => 'Encode',
      version => 2.21,
      feature => ['detect_charset'],
    },
    {
      package => 'Encode-Detect',
      module  => 'Encode::Detect',
      version => 0,
      feature => ['detect_charset'],
    },

    # Inbound Email
    {
      package => 'Email-Reply',
      module  => 'Email::Reply',
      version => 0,
      feature => ['inbound_email'],
    },
    {
      package => 'HTML-FormatText-WithLinks',
      module  => 'HTML::FormatText::WithLinks',

      # We need 0.13 to set the "bold" marker to "*".
      version => '0.13',
      feature => ['inbound_email'],
    },

    # Mail Queueing
    {
      package => 'TheSchwartz',
      module  => 'TheSchwartz',

      # 1.07 supports the prioritization of jobs.
      version => 1.07,
      feature => ['jobqueue'],
    },
    {
      package => 'Daemon-Generic',
      module  => 'Daemon::Generic',
      version => 0,
      feature => ['jobqueue'],
    },

    # mod_perl
    {
      package => 'mod_perl',
      module  => 'mod_perl2',
      version => '1.999022',
      feature => ['mod_perl'],
    },
    {
      package => 'Apache-SizeLimit',
      module  => 'Apache2::SizeLimit',

      # 0.96 properly determines process size on Linux.
      version => '0.96',
      feature => ['mod_perl'],
    },

    # typesniffer
    {
      package => 'File-MimeInfo',
      module  => 'File::MimeInfo::Magic',
      version => '0',
      feature => ['typesniffer'],
    },
    {
      package => 'IO-stringy',
      module  => 'IO::Scalar',
      version => '0',
      feature => ['typesniffer'],
    },

    # memcached
    {
      package => 'Cache-Memcached',
      module  => 'Cache::Memcached',
      version => '0',
      feature => ['memcached'],
    },

    # Documentation
    {
      package => 'File-Copy-Recursive',
      module  => 'File::Copy::Recursive',
      version => 0,
      feature => ['documentation'],
    },
    {
      package => 'File-Which',
      module  => 'File::Which',
      version => 0,
      feature => ['documentation'],
    },
  );

  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
# to compile. It is used by t/001compile.t and mod_perl.pl.
use constant FEATURE_FILES => (
  jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'],
  xmlrpc  => [
    'Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi',
    'Bugzilla/WebService.pm',               'Bugzilla/WebService/*.pm'
  ],
  rest => [
    'Bugzilla/WebService/Server/REST.pm', 'rest.cgi',
    'Bugzilla/WebService/Server/REST/Resources/*.pm'
  ],
  moving        => ['importxml.pl'],
  auth_ldap     => ['Bugzilla/Auth/Verify/LDAP.pm'],
  auth_radius   => ['Bugzilla/Auth/Verify/RADIUS.pm'],
  documentation => ['docs/makedocs.pl'],
  inbound_email => ['email_in.pl'],
  jobqueue      => [
    'Bugzilla/Job/*',      'Bugzilla/JobQueue.pm',
    'Bugzilla/JobQueue/*', 'jobqueue.pl'
  ],
  patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'],
  updates      => ['Bugzilla/Update.pm'],
  memcached    => ['Bugzilla/Memcache.pm'],
);

# This implements the REQUIRED_MODULES and OPTIONAL_MODULES stuff
# described in in Bugzilla::Extension.
sub _get_extension_requirements {
  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);
    }
  }
  return \@modules;
}

sub check_requirements {
  my ($output) = @_;

  print "\n", install_string('checking_modules'), "\n" if $output;
  my $root    = ROOT_USER;
  my $missing = _check_missing(REQUIRED_MODULES, $output);

  print "\n", install_string('checking_dbd'), "\n" if $output;
  my $have_one_dbd = 0;
  my $db_modules   = DB_MODULE;
  foreach my $db (keys %$db_modules) {
    my $dbd = $db_modules->{$db}->{dbd};
    $have_one_dbd = 1 if have_vers($dbd, $output);
  }

  print "\n", install_string('checking_optional'), "\n" if $output;
  my $missing_optional = _check_missing(OPTIONAL_MODULES, $output);

  my $missing_apache = _missing_apache_modules(APACHE_MODULES, $output);

  # If we're running on Windows, reset the input line terminator so that
  # console input works properly - loading CGI tends to mess it up
  $/ = "\015\012" if ON_WINDOWS;

  my $pass = !scalar(@$missing) && $have_one_dbd;
  return {
    pass        => $pass,
    one_dbd     => $have_one_dbd,
    missing     => $missing,
    optional    => $missing_optional,
    apache      => $missing_apache,
    any_missing => !$pass || scalar(@$missing_optional),
  };
}

# A helper for check_requirements
sub _check_missing {
  my ($modules, $output) = @_;

  my @missing;
  foreach my $module (@$modules) {
    unless (have_vers($module, $output)) {
      push(@missing, $module);
    }
  }

  return \@missing;
}

sub _missing_apache_modules {
  my ($modules, $output) = @_;
  my $apachectl = _get_apachectl();
  return [] if !$apachectl;
  my $command  = "$apachectl -t -D DUMP_MODULES";
  my $cmd_info = `$command 2>&1`;

  # If apachectl returned a value greater than 0, then there was an
  # error parsing Apache's configuration, and we can't check modules.
  my $retval = $?;
  if ($retval > 0) {
    print STDERR install_string('apachectl_failed',
      {command => $command, root => ROOT_USER}), "\n";
    return [];
  }
  my @missing;
  foreach my $module (sort keys %$modules) {
    my $ok = _check_apache_module($module, $modules->{$module}, $cmd_info, $output);
    push(@missing, $module) if !$ok;
  }
  return \@missing;
}

sub _get_apachectl {
  foreach my $bin_name (APACHE) {
    my $bin = bin_loc($bin_name);
    return $bin if $bin;
  }

  # Try again with a possibly different path.
  foreach my $bin_name (APACHE) {
    my $bin = bin_loc($bin_name, APACHE_PATH);
    return $bin if $bin;
  }
  return undef;
}

sub _check_apache_module {
  my ($module, $config_name, $mod_info, $output) = @_;
  my $ok;
  if ($mod_info =~ /^\s+\Q$config_name\E\b/m) {
    $ok = 1;
  }
  if ($output) {
    _checking_for({package => $module, ok => $ok});
  }
  return $ok;
}

sub print_module_instructions {
  my ($check_results, $output) = @_;

  # First we print the long explanatory messages.

  if (scalar @{$check_results->{missing}}) {
    print install_string('modules_message_required');
  }

  if (!$check_results->{one_dbd}) {
    print install_string('modules_message_db');
  }

  if (my @missing = @{$check_results->{optional}} and $output) {
    print install_string('modules_message_optional');

    # Now we have to determine how large the table cols will be.
    my $longest_name = max(map(length($_->{package}), @missing));

    # The first column header is at least 11 characters long.
    $longest_name = 11 if $longest_name < 11;

    # The table is TABLE_WIDTH characters long. There are seven mandatory
    # characters (* and space) in the string. So, we have a total
    # of TABLE_WIDTH - 7 characters to work with.
    my $remaining_space = (TABLE_WIDTH - 7) - $longest_name;
    print '*' x TABLE_WIDTH . "\n";
    printf "* \%${longest_name}s * %-${remaining_space}s *\n", 'MODULE NAME',
      'ENABLES FEATURE(S)';
    print '*' x TABLE_WIDTH . "\n";
    foreach my $package (@missing) {
      printf "* \%${longest_name}s * %-${remaining_space}s *\n", $package->{package},
        _translate_feature($package->{feature});
    }
  }

  if (my @missing = @{$check_results->{apache}}) {
    print install_string('modules_message_apache');
    my $missing_string = join(', ', @missing);
    my $size           = TABLE_WIDTH - 7;
    printf "*    \%-${size}s *\n", $missing_string;
    my $spaces = TABLE_WIDTH - 2;
    print "*", (' ' x $spaces), "*\n";
  }

  my $need_module_instructions = (
         (!$output and @{$check_results->{missing}})
      or ($output  and $check_results->{any_missing})
  ) ? 1 : 0;

  if ($need_module_instructions or @{$check_results->{apache}}) {

    # If any output was required, we want to close the "table"
    print "*" x TABLE_WIDTH . "\n";
  }

  # And now we print the actual installation commands.

  if (my @missing = @{$check_results->{optional}} and $output) {
    print install_string('commands_optional') . "\n\n";
    foreach my $module (@missing) {
      my $command = install_command($module);
      printf "%15s: $command\n", $module->{package};
    }
    print "\n";
  }

  if (!$check_results->{one_dbd}) {
    print install_string('commands_dbd') . "\n";
    my %db_modules = %{DB_MODULE()};
    foreach my $db (keys %db_modules) {
      my $command = install_command($db_modules{$db}->{dbd});
      printf "%10s: \%s\n", $db_modules{$db}->{name}, $command;
    }
    print "\n";
  }

  if (my @missing = @{$check_results->{missing}}) {
    print colored(install_string('commands_required'), COLOR_ERROR), "\n";
    foreach my $package (@missing) {
      my $command = install_command($package);
      print "    $command\n";
    }
  }

  if ( $output
    && $check_results->{any_missing}
    && !ON_ACTIVESTATE
    && !$check_results->{hide_all})
  {
    print install_string('install_all', {perl => $^X});
  }
  if (!$check_results->{pass}) {
    print colored(install_string('installation_failed'), COLOR_ERROR), "\n\n";
  }
}

sub _translate_feature {
  my $features = shift;
  my @strings;
  foreach my $feature (@$features) {
    push(@strings, install_string("feature_$feature"));
  }
  return join(', ', @strings);
}

sub check_graphviz {
  my ($output) = @_;

  my $webdotbase = Bugzilla->params->{'webdotbase'};
  return 1 if $webdotbase =~ /^https?:/;

  my $return;
  $return = 1 if -x $webdotbase;

  if ($output) {
    _checking_for({package => 'GraphViz', ok => $return});
  }

  if (!$return) {
    print install_string('bad_executable', {bin => $webdotbase}), "\n";
  }

  my $webdotdir = bz_locations()->{'webdotdir'};

  # Check .htaccess allows access to generated images
  if (-e "$webdotdir/.htaccess") {
    my $htaccess = new IO::File("$webdotdir/.htaccess", 'r')
      || die "$webdotdir/.htaccess: " . $!;
    if (!grep(/png/, $htaccess->getlines)) {
      print STDERR install_string('webdot_bad_htaccess', {dir => $webdotdir}), "\n";
    }
    $htaccess->close;
  }

  return $return;
}

# This was originally clipped from the libnet Makefile.PL, adapted here for
# accurate version checking.
sub have_vers {
  my ($params, $output) = @_;
  my $module  = $params->{module};
  my $package = $params->{package};
  if (!$package) {
    $package = $module;
    $package =~ s/::/-/g;
  }
  my $wanted = $params->{version};

  eval "require $module;";

  # Don't let loading a module change the output-encoding of STDOUT
  # or STDERR. (CGI.pm tries to set "binmode" on these file handles when
  # it's loaded, and other modules may do the same in the future.)
  Bugzilla::Install::Util::set_output_encoding();

  # VERSION is provided by UNIVERSAL::, and can be called even if
  # the module isn't loaded. We eval'uate ->VERSION because it can die
  # when the version is not valid (yes, this happens from time to time).
  # In that case, we use an uglier method to get the version.
  my $vnum = eval { $module->VERSION };
  if ($@) {
    no strict 'refs';
    $vnum = ${"${module}::VERSION"};

    # If we come here, then the version is not a valid one.
    # We try to sanitize it.
    if ($vnum =~ /^((\d+)(\.\d+)*)/) {
      $vnum = $1;
    }
  }
  $vnum ||= -1;

  # Must do a string comparison as $vnum may be of the form 5.10.1.
  my $vok
    = ($vnum ne '-1' && version->new($vnum) >= version->new($wanted)) ? 1 : 0;
  my $blacklisted;
  if ($vok && $params->{blacklist}) {
    $blacklisted = grep($vnum =~ /$_/, @{$params->{blacklist}});
    $vok = 0 if $blacklisted;
  }

  if ($output) {
    _checking_for({
      package     => $package,
      ok          => $vok,
      wanted      => $wanted,
      found       => $vnum,
      blacklisted => $blacklisted
    });
  }

  return $vok ? 1 : 0;
}

sub _checking_for {
  my ($params) = @_;
  my ($package, $ok, $wanted, $blacklisted, $found)
    = @$params{qw(package ok wanted blacklisted found)};

  my $ok_string = $ok ? install_string('module_ok') : '';

  # If we're actually checking versions (like for Perl modules), then
  # we have some rather complex logic to determine what we want to
  # show. If we're not checking versions (like for GraphViz) we just
  # show "ok" or "not found".
  if (exists $params->{found}) {
    my $found_string;

    # We do a string compare in case it's non-numeric. We make sure
    # it's not a version object as negative versions are forbidden.
    if ($found && !ref($found) && $found eq '-1') {
      $found_string = install_string('module_not_found');
    }
    elsif ($found) {
      $found_string = install_string('module_found', {ver => $found});
    }
    else {
      $found_string = install_string('module_unknown_version');
    }
    $ok_string = $ok ? "$ok_string: $found_string" : $found_string;
  }
  elsif (!$ok) {
    $ok_string = install_string('module_not_found');
  }

  my $black_string = $blacklisted ? install_string('blacklisted') : '';
  my $want_string = $wanted ? "v$wanted" : install_string('any');

  my $str = sprintf "%s %20s %-11s $ok_string $black_string\n",
    install_string('checking_for'), $package, "($want_string)";
  print $ok ? $str : colored($str, COLOR_ERROR);
}

sub install_command {
  my $module = shift;
  my ($command, $package);

  if (ON_ACTIVESTATE) {
    $command = 'ppm install %s';
    $package = $module->{package};
  }
  else {
    $command = "$^X install-module.pl \%s";

    # Non-Windows installations need to use module names, because
    # CPAN doesn't understand package names.
    $package = $module->{module};
  }
  return sprintf $command, $package;
}

# This does a reverse mapping for FEATURE_FILES.
sub map_files_to_features {
  my %features = FEATURE_FILES;
  my %files;
  foreach my $feature (keys %features) {
    my @my_files = @{$features{$feature}};
    foreach my $pattern (@my_files) {
      foreach my $file (glob $pattern) {
        $files{$file} = $feature;
      }
    }
  }
  return \%files;
}

1;

__END__

=head1 NAME

Bugzilla::Install::Requirements - Functions and variables dealing
  with Bugzilla's perl-module requirements.

=head1 DESCRIPTION

This module is used primarily by C<checksetup.pl> to determine whether
or not all of Bugzilla's prerequisites are installed. (That is, all the
perl modules it requires.)

=head1 CONSTANTS

=over

=item C<REQUIRED_MODULES>

An arrayref of hashrefs that describes the perl modules required by 
Bugzilla. The hashes have three keys: 

=over

=item C<package> - The name of the Perl package that you'd find on
CPAN for this requirement. 

=item C<module> - The name of a module that can be passed to the
C<install> command in C<CPAN.pm> to install this module.

=item C<version> - The version of this module that we require, or C<0>
if any version is acceptable.

=back

=item C<OPTIONAL_MODULES>

An arrayref of hashrefs that describes the perl modules that add
additional features to Bugzilla if installed. Its hashes have all
the fields of L</REQUIRED_MODULES>, plus a C<feature> item--an arrayref
of strings that describe what features require this module.

=item C<FEATURE_FILES>

A hashref that describes what files should only be compiled if a certain
feature is enabled. The feature is the key, and the values are arrayrefs
of file names (which are passed to C<glob>, so shell patterns work).

=back


=head1 SUBROUTINES

=over 4

=item C<check_requirements>

=over

=item B<Description>

This checks what optional or required perl modules are installed, like
C<checksetup.pl> does.

=item B<Params>

=over

=item C<$output> - C<true> if you want the function to print out information
about what it's doing, and the versions of everything installed.

=back

=item B<Returns>

A hashref containing these values:

=over

=item C<pass> - Whether or not we have all the mandatory requirements.

=item C<missing> - An arrayref containing any required modules that
are not installed or that are not up-to-date. Each item in the array is
a hashref in the format of items from L</REQUIRED_MODULES>.

=item C<optional> - The same as C<missing>, but for optional modules.

=item C<apache> - The name of each optional Apache module that is missing.

=item C<have_one_dbd> - True if at least one C<DBD::> module is installed.

=item C<any_missing> - True if there are any missing Perl modules, even
optional modules.

=back

=back

=item C<check_graphviz($output)>

Description: Checks if the graphviz binary specified in the 
  C<webdotbase> parameter is a valid binary, or a valid URL.

Params:      C<$output> - C<$true> if you want the function to
                 print out information about what it's doing.

Returns:     C<1> if the check was successful, C<0> otherwise.

=item C<have_vers($module, $output)>

 Description: Tells you whether or not you have the appropriate
              version of the module requested. It also prints
              out a message to the user explaining the check
              and the result.

 Params:      C<$module> - A hashref, in the format of an item from 
                           L</REQUIRED_MODULES>.
              C<$output> - Set to true if you want this function to
                           print information to STDOUT about what it's
                           doing.

 Returns:   C<1> if you have the module installed and you have the
            appropriate version. C<0> otherwise.

=item C<install_command($module)>

 Description: Prints out the appropriate command to install the
              module specified, depending on whether you're
              on Windows or Linux.

 Params:      C<$module> - A hashref, in the format of an item from
                           L</REQUIRED_MODULES>.

 Returns:     nothing

=item C<map_files_to_features>

Returns a hashref where file names are the keys and the value is the feature
that must be enabled in order to compile that file.

=back

=head1 B<Methods in need of POD>

=over

=item print_module_instructions

=back
