# 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. # 2.28-3.007 are broken, see https://bugzilla.mozilla.org/show_bug.cgi?id=1560873 { package => 'Template-Toolkit', module => 'Template', version => '2.24', blacklist => ['^2.2[89]$', '^3.00[0-7]$'] }, # 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 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 An arrayref of hashrefs that describes the perl modules required by Bugzilla. The hashes have three keys: =over =item C - The name of the Perl package that you'd find on CPAN for this requirement. =item C - The name of a module that can be passed to the C command in C to install this module. =item C - The version of this module that we require, or C<0> if any version is acceptable. =back =item C 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, plus a C item--an arrayref of strings that describe what features require this module. =item C 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, so shell patterns work). =back =head1 SUBROUTINES =over 4 =item C =over =item B This checks what optional or required perl modules are installed, like C does. =item B =over =item C<$output> - C if you want the function to print out information about what it's doing, and the versions of everything installed. =back =item B A hashref containing these values: =over =item C - Whether or not we have all the mandatory requirements. =item C - 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. =item C - The same as C, but for optional modules. =item C - The name of each optional Apache module that is missing. =item C - True if at least one C module is installed. =item C - True if there are any missing Perl modules, even optional modules. =back =back =item C Description: Checks if the graphviz binary specified in the C 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 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. 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 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. Returns: nothing =item C 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 =over =item print_module_instructions =back