# 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.14.0; use strict; use warnings; use Bugzilla::Constants; use Bugzilla::Install::Util qw(install_string bin_loc success extension_requirement_packages); use List::Util qw(max); use Term::ANSIColor; use CPAN::Meta; use CPAN::Meta::Prereqs; use CPAN::Meta::Requirements; use Module::Metadata; use parent qw(Exporter); use autodie; our @EXPORT = qw( FEATURE_FILES load_cpan_meta check_cpan_requirements check_cpan_feature check_all_cpan_features check_webdotbase check_font_file map_files_to_features ); our $checking_for_indent = 0; # 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 )]; # 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'], csp => ['Bugzilla/CGI/ContentSecurityPolicy.pm'], psgi => ['app.psgi'], 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'], markdown => ['Bugzilla/Markdown.pm', 't/100markdown.t'], memcached => ['Bugzilla/Memcache.pm'], auth_delegation => ['auth.cgi'], ); sub load_cpan_meta { my $dir = bz_locations()->{libpath}; my @meta_json = map { File::Spec->catfile($dir, $_) } qw( MYMETA.json META.json ); my ($file) = grep { -f $_ } @meta_json; if ($file) { open my $meta_fh, '<', $file or die "unable to open $file: $!"; my $str = do { local $/ = undef; scalar <$meta_fh> }; # detaint $str =~ /^(.+)$/s; $str = $1; close $meta_fh; return CPAN::Meta->load_json_string($str); } else { ThrowCodeError('cpan_meta_missing'); } } sub check_all_cpan_features { my ($meta, $dirs, $output) = @_; my %report; local $checking_for_indent = 2; print "\nOptional features:\n" if $output; my @features = sort { $a->identifier cmp $b->identifier } $meta->features; foreach my $feature (@features) { next if $feature->identifier eq 'features'; printf "Feature '%s': %s\n", $feature->identifier, $feature->description if $output; my $result = check_cpan_feature($feature, $dirs, $output); print "\n" if $output; $report{$feature->identifier} = { description => $feature->description, result => $result, }; } print install_string('all_optional_features_require'), "\n" if $output; my $features = check_cpan_feature($meta->feature('features'), $dirs, $output); print "\n" if $output; $report{features} = { description => $meta->feature('features')->description, result => $features, }; return \%report; } sub check_cpan_feature { my ($feature, $dirs, $output) = @_; return _check_prereqs($feature->prereqs, $dirs, $output); } sub check_cpan_requirements { my ($meta, $dirs, $output) = @_; my $result = _check_prereqs($meta->effective_prereqs, $dirs, $output); print colored(install_string('installation_failed'), COLOR_ERROR), "\n" if !$result->{ok} && $output; return $result; } sub _check_prereqs { my ($prereqs, $dirs, $output) = @_; $dirs //= \@INC; my $reqs = $prereqs->merged_requirements(['configure', 'runtime'], ['requires']); my @found; my @missing; foreach my $module (sort $reqs->required_modules) { my $ok = _check_module($reqs, $module, $dirs, $output); if ($ok) { push @found, $module; } else { push @missing, $module; } } return { ok => (@missing == 0), found => \@found, missing => \@missing }; } sub _check_module { my ($reqs, $module, $dirs, $output) = @_; my $required_version = $reqs->requirements_for_module($module); if ($module eq 'perl') { my $ok = $reqs->accepts_module($module, $]); _checking_for({package => "perl", found => $], wanted => $required_version, ok => $ok}) if $output; return $ok; } else { my $metadata = Module::Metadata->new_from_module($module, inc => $dirs); my $version = eval { $metadata->version }; my $ok = $metadata && $version && $reqs->accepts_module($module, $version || 0); _checking_for({package => $module, $version ? ( found => $version ) : (), wanted => $required_version, ok => $ok}) if $output; return $ok; } } 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_webdotbase { my ($output) = @_; my $webdotbase = Bugzilla->localconfig->{'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; } sub check_font_file { my ($output) = @_; my $font_file = Bugzilla->localconfig->{'font_file'}; my $readable; $readable = 1 if -r $font_file; my $ttf; $ttf = 1 if $font_file =~ /\.(ttf|otf)$/; if ($output) { _checking_for({ package => 'Font file', ok => $readable && $ttf}); } if (!$readable) { print install_string('bad_font_file', { file => $font_file }), "\n"; } elsif (!$ttf) { print install_string('bad_font_file_name', { file => $font_file }), "\n"; } return $readable && $ttf; } 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 ? "$wanted" : install_string('any'); my $str = sprintf "%s %20s %-11s $ok_string $black_string\n", ( ' ' x $checking_for_indent ) . install_string('checking_for'), $package, "($want_string)"; print $ok ? $str : colored($str, COLOR_ERROR); } # 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 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 required perl modules are installed, like C does. =item B =over =item C<$meta> - A C object. =item C<$dirs> - the include dirs to search for modules, defaults to @INC. =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 - if all the requirements are met, this is true. =item C - an arrayref of found modules =item C - an arrayref of missing modules =back =back =item C =over =item B This checks that the optional Perl modules required for a feature are installed. =item B =over =item C<$feature> - A C object. =item C<$dirs> - the include dirs to search for modules, defaults to @INC. =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 - if all the requirements are met, this is true. =item C - an arrayref of found modules =item C - an arrayref of missing modules =back =item C =over =item B This checks which optional Perl modules are currently installed which can enable optional features. =item B =over =item C<$meta> - A C object. =item C<$dirs> - the include dirs to search for modules, defaults to @INC. =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 keyed on the feature name. The values are hashrefs containing C and C keys. C is the English description of the feature. C is a hashref in the same format as the return value of C, described previously. =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: Checks if the font file specified in the C parameter is a valid-looking font file. 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 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. =item C Load MYMETA.json or META.json from the bugzilla directory, and a return a L object. =back =back