bugzilla-4intranet/Bugzilla/Install/Filesystem.pm

696 lines
22 KiB
Perl

# 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.
#
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
# Bill Barry <after.fallout@gmail.com>
package Bugzilla::Install::Filesystem;
# NOTE: This package may "use" any modules that it likes,
# and localconfig is available. However, all functions in this
# package should assume that:
#
# * Templates are not available.
# * Files do not have the correct permissions.
# * The database does not exist.
use strict;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Install::Localconfig;
use Bugzilla::Install::Util qw(install_string);
use Bugzilla::Util;
use File::Find;
use File::Path;
use File::Basename;
use File::Copy qw(move);
use IO::File;
use POSIX ();
use base qw(Exporter);
our @EXPORT = qw(
update_filesystem
create_htaccess
fix_all_file_permissions
fix_file_permissions
);
use constant HT_DEFAULT_DENY => <<EOT;
# nothing in this directory is retrievable unless overridden by an .htaccess
# in a subdirectory
deny from all
EOT
# This looks like a constant because it effectively is, but
# it has to call other subroutines and read the current filesystem,
# so it's defined as a sub. This is not exported, so it doesn't have
# a perldoc. However, look at the various hashes defined inside this
# function to understand what it returns. (There are comments throughout.)
#
# The rationale for the file permissions is that there is a group the
# web server executes the scripts as, so the cgi scripts should not be writable
# by this group. Otherwise someone may find it possible to change the cgis
# when exploiting some security flaw somewhere (not necessarily in Bugzilla!)
sub FILESYSTEM
{
my $datadir = bz_locations()->{datadir};
my $attachdir = bz_locations()->{attachdir};
my $extensionsdir = bz_locations()->{extensionsdir};
my $webdotdir = bz_locations()->{webdotdir};
my $templatedir = bz_locations()->{templatedir};
my $libdir = bz_locations()->{libpath};
my $extlib = bz_locations()->{ext_libpath};
my $skinsdir = bz_locations()->{skinsdir};
my $localconfig = bz_locations()->{localconfig};
my $graphsdir = bz_locations()->{graphsdir};
# We want to set the permissions the same for all localconfig files
# across all PROJECTs, so we do something special with $localconfig,
# lower down in the permissions section.
if ($ENV{PROJECT})
{
$localconfig =~ s/\.\Q$ENV{PROJECT}\E$//;
}
my $ws_group = Bugzilla->localconfig->{webservergroup};
my $use_suexec = Bugzilla->localconfig->{use_suexec};
# The set of permissions that we use:
# FILES
# Executable by the web server
my $ws_executable = $ws_group ? 0750 : 0755;
# Executable by the owner only.
my $owner_executable = 0700;
# Readable by the web server.
my $ws_readable = ($ws_group && !$use_suexec) ? 0640 : 0644;
# Readable by the owner only.
my $owner_readable = 0600;
# Writeable by the web server.
my $ws_writeable = $ws_group ? 0660 : 0666;
# Script-readable files that should not be world-readable under suexec.
my $script_readable = $use_suexec ? 0640 : $ws_readable;
# DIRECTORIES
# Readable by the web server.
my $ws_dir_readable = ($ws_group && !$use_suexec) ? 0750 : 0755;
# Readable only by the owner.
my $owner_dir_readable = 0700;
# Writeable by the web server.
my $ws_dir_writeable = $ws_group ? 0770 : 01777;
# The web server can overwrite files owned by other users, in this directory.
my $ws_dir_full_control = $ws_group ? 0770 : 0777;
# Note: When being processed by checksetup, these have their permissions
# set in this order: %recurse_dirs, %all_files.
#
# Each is processed in alphabetical order of keys, so shorter keys
# will have their permissions set before longer keys (thus setting
# the permissions on parent directories before setting permissions
# on their children).
# --- FILE PERMISSIONS (Created files) --- #
my %files = (
# Set the permissions for localconfig the same across all PROJECTs.
$localconfig => { perms => $script_readable },
"$localconfig.*" => { perms => $script_readable },
"$localconfig.old" => { perms => $owner_readable },
"$datadir/params" => { perms => $ws_writeable },
);
# This sets the permissions for each item inside each of these directories, including the directory itself.
my %recurse_dirs = (
# Writeable directories
"$datadir/template" => { files => $ws_readable, dirs => $ws_dir_full_control },
$attachdir => { files => $ws_writeable, dirs => $ws_dir_writeable },
$webdotdir => { files => $ws_writeable, dirs => $ws_dir_writeable },
$graphsdir => { files => $ws_writeable, dirs => $ws_dir_writeable },
# Readable directories
"$datadir/mining" => { files => $ws_readable, dirs => $ws_dir_readable },
);
# --- FILES TO CREATE --- #
# The name of each directory that we should actually *create*,
# pointing at its default permissions.
my %create_dirs = (
$datadir => $ws_dir_full_control,
"$datadir/mining" => $ws_dir_readable,
$attachdir => $ws_dir_writeable,
$graphsdir => $ws_dir_writeable,
$webdotdir => $ws_dir_writeable,
);
# The name of each file, pointing at its default permissions and
# default contents.
my %create_files = (
# We create this file so that it always has the right owner
# and permissions. Otherwise, the webserver creates it as
# owned by itself, which can cause problems if jobqueue.pl
# or something else is not running as the webserver or root.
"$datadir/mailer.testfile" => { perms => $ws_writeable, contents => '' },
);
# Because checksetup controls the .htaccess creation separately
# by a localconfig variable, these go in a separate variable from
# %create_files.
my %htaccess = (
"$attachdir/.htaccess" => { perms => $ws_readable, contents => HT_DEFAULT_DENY },
"$libdir/Bugzilla/.htaccess" => { perms => $ws_readable, contents => HT_DEFAULT_DENY },
"$extlib/.htaccess" => { perms => $ws_readable, contents => HT_DEFAULT_DENY },
"$templatedir/.htaccess" => { perms => $ws_readable, contents => HT_DEFAULT_DENY },
'contrib/.htaccess' => { perms => $ws_readable, contents => HT_DEFAULT_DENY },
't/.htaccess' => { perms => $ws_readable, contents => HT_DEFAULT_DENY },
'.htaccess' => { perms => $ws_readable, contents => <<EOT
# Don't allow people to retrieve non-cgi executable files or our private data
<FilesMatch ^(.*\\.pm|.*\\.pl|.*localconfig.*)\$>
deny from all
</FilesMatch>
EOT
},
"$webdotdir/.htaccess" => { perms => $ws_readable, contents => <<EOT
# Restrict access to .dot files to the public webdot server at research.att.com
# if research.att.com ever changes their IP, or if you use a different
# webdot server, you'll need to edit this
<FilesMatch \\.dot\$>
Allow from 192.20.225.0/24
Deny from all
</FilesMatch>
# Allow access to .png and .svg files created by a local copy of 'dot'
<FilesMatch \\.(png|svg)\$>
Allow from all
</FilesMatch>
# And no directory listings, either.
Deny from all
EOT
},
# Even though $datadir may not (and should not) be accessible from the
# web server, we can't know for sure, so create the .htaccess anyway.
# It's harmless if it isn't accessible...
"$datadir/.htaccess" => { perms => $ws_readable, contents => <<EOT
# Nothing in this directory is retrievable unless overridden by an .htaccess
# in a subdirectory.
deny from all
EOT
},
"$graphsdir/.htaccess" => { perms => $ws_readable, contents => <<EOT
# Allow access to .png and .gif files.
<FilesMatch (\\.gif|\\.png)\$>
Allow from all
</FilesMatch>
# And no directory listings, either.
Deny from all
EOT
},
);
my %all_files = (%create_files, %htaccess, %files);
return {
create_dirs => \%create_dirs,
recurse_dirs => \%recurse_dirs,
create_files => \%create_files,
htaccess => \%htaccess,
all_files => \%all_files,
};
}
sub update_filesystem
{
my ($params) = @_;
my $fs = FILESYSTEM();
my %dirs = %{$fs->{create_dirs}};
my %files = %{$fs->{create_files}};
my $datadir = bz_locations->{datadir};
my $graphsdir = bz_locations->{graphsdir};
# If the graphs/ directory doesn't exist, we're upgrading from
# a version old enough that we need to update the $datadir/mining
# format.
if (-d "$datadir/mining" && !-d $graphsdir)
{
_update_old_charts($datadir);
}
# By sorting the dirs, we assure that shorter-named directories
# (meaning parent directories) are always created before their
# child directories.
foreach my $dir (sort keys %dirs)
{
unless (-d $dir)
{
print "Creating $dir directory...\n";
mkdir $dir or die "mkdir $dir failed: $!";
# For some reason, passing in the permissions to "mkdir"
# doesn't work right, but doing a "chmod" does.
chmod $dirs{$dir}, $dir or warn "Cannot chmod $dir: $!";
}
}
# Move the testfile if we can't write to it, so that we can re-create
# it with the correct permissions below.
my $testfile = "$datadir/mailer.testfile";
if (-e $testfile and !-w $testfile)
{
_rename_file($testfile, "$testfile.old");
}
# If old-params.txt exists in the root directory, move it to datadir.
my $oldparamsfile = "old_params.txt";
if (-e $oldparamsfile)
{
_rename_file($oldparamsfile, "$datadir/$oldparamsfile");
}
_create_files(%files);
if (-e 'index.html')
{
my $templatedir = bz_locations()->{templatedir};
print <<EOT;
*** It appears that you still have an old index.html hanging around.
Either the contents of this file should be moved into a template and
placed in the '$templatedir/en/custom' directory, or you should delete
the file.
EOT
}
# Delete old files that no longer need to exist
# 2001-04-29 jake@bugzilla.org - Remove oldemailtech
# http://bugzilla.mozilla.org/show_bugs.cgi?id=71552
if (-d 'shadow')
{
print "Removing shadow directory...\n";
rmtree("shadow");
}
if (-e "$datadir/versioncache")
{
print "Removing versioncache...\n";
unlink "$datadir/versioncache";
}
if (-e "$datadir/duplicates.rdf")
{
print "Removing duplicates.rdf...\n";
unlink "$datadir/duplicates.rdf";
unlink "$datadir/duplicates-old.rdf";
}
if (-e "$datadir/duplicates")
{
print "Removing duplicates directory...\n";
rmtree("$datadir/duplicates");
}
}
sub create_htaccess
{
_create_files(%{FILESYSTEM()->{htaccess}});
# Repair old .htaccess files
my $htaccess = new IO::File('.htaccess', 'r') || die ".htaccess: $!";
my $old_data;
{ local $/; $old_data = <$htaccess>; }
$htaccess->close;
my $repaired = 0;
if ($old_data =~ s/\|localconfig\|/\|.*localconfig.*\|/)
{
$repaired = 1;
}
if ($old_data !~ /\(\.\*\\\.pm\|/)
{
$old_data =~ s/\(/(.*\\.pm\|/;
$repaired = 1;
}
if ($repaired)
{
print "Repairing .htaccess...\n";
$htaccess = new IO::File('.htaccess', 'w') || die $!;
print $htaccess $old_data;
$htaccess->close;
}
my $webdot_dir = bz_locations()->{webdotdir};
# The public webdot IP address changed.
my $webdot = new IO::File("$webdot_dir/.htaccess", 'r')
|| die "$webdot_dir/.htaccess: $!";
my $webdot_data;
{ local $/; $webdot_data = <$webdot>; }
$webdot->close;
if ($webdot_data =~ /192\.20\.225\.10/)
{
print "Repairing $webdot_dir/.htaccess...\n";
$webdot_data =~ s/192\.20\.225\.10/192.20.225.0\/24/g;
$webdot = new IO::File("$webdot_dir/.htaccess", 'w') || die $!;
print $webdot $webdot_data;
$webdot->close;
}
}
sub _rename_file
{
my ($from, $to) = @_;
print "Renaming $from to $to...\n";
if (-e $to)
{
warn "$to already exists, not moving\n";
}
else
{
move($from, $to) or warn $!;
}
}
# A helper for the above functions.
sub _create_files
{
my (%files) = @_;
# It's not necessary to sort these, but it does make the
# output of checksetup.pl look a bit nicer.
foreach my $file (sort keys %files)
{
unless (-e $file)
{
print "Creating $file...\n";
my $info = $files{$file};
my $fh = new IO::File($file, O_WRONLY | O_CREAT, $info->{perms})
|| die $!;
print $fh $info->{contents} if $info->{contents};
$fh->close;
}
}
}
# If you ran a REALLY old version of Bugzilla, your chart files are in the
# wrong format. This code is a little messy, because it's very old, and
# when moving it into this module, I couldn't test it so I left it almost
# completely alone.
sub _update_old_charts
{
my ($datadir) = @_;
print "Updating old chart storage format...\n";
foreach my $in_file (glob("$datadir/mining/*"))
{
# Don't try and upgrade image or db files!
next if $in_file =~ /\.(gif|png|db|orig)$/i;
rename("$in_file", "$in_file.orig") or next;
open(IN, "$in_file.orig") or next;
open(OUT, '>', $in_file) or next;
# Fields in the header
my @declared_fields;
# Fields we changed to half way through by mistake
# This list comes from an old version of collectstats.pl
# This part is only for people who ran later versions of 2.11 (devel)
my @intermediate_fields = qw(DATE UNCONFIRMED NEW ASSIGNED REOPENED RESOLVED VERIFIED CLOSED);
# Fields we actually want (matches the current collectstats.pl)
my @out_fields = qw(
DATE NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED VERIFIED CLOSED
FIXED INVALID WONTFIX LATER REMIND DUPLICATE WORKSFORME MOVED
);
while (<IN>)
{
if (/^# fields?: (.*)\s$/)
{
@declared_fields = map uc, (split /\||\r/, $1);
print OUT "# fields: ", join('|', @out_fields), "\n";
}
elsif (/^(\d+\|.*)/)
{
my @data = split(/\||\r/, $1);
my %data;
if (@data == @declared_fields)
{
# old format
for my $i (0 .. $#declared_fields)
{
$data{$declared_fields[$i]} = $data[$i];
}
}
elsif (@data == @intermediate_fields)
{
# Must have changed over at this point
for my $i (0 .. $#intermediate_fields)
{
$data{$intermediate_fields[$i]} = $data[$i];
}
}
elsif (@data == @out_fields)
{
# This line's fine - it has the right number of entries
for my $i (0 .. $#out_fields)
{
$data{$out_fields[$i]} = $data[$i];
}
}
else
{
print "Oh dear, input line $. of $in_file had " .
scalar(@data) . " fields\nThis was unexpected.",
" You may want to check your data files.\n";
}
print OUT join('|',
map { defined ($data{$_}) ? ($data{$_}) : "" } @out_fields),
"\n";
}
else
{
print OUT;
}
}
close(IN);
close(OUT);
}
}
sub fix_file_permissions
{
my ($file) = @_;
return if ON_WINDOWS;
my $perms = FILESYSTEM()->{all_files}->{$file}->{perms};
# Note that _get_owner_and_group is always silent here.
my ($owner_id, $group_id) = _get_owner_and_group();
_fix_perms($file, $owner_id, $group_id, $perms);
}
sub fix_all_file_permissions
{
my ($output) = @_;
# _get_owner_and_group also checks that the webservergroup is valid.
my ($owner_id, $group_id) = _get_owner_and_group($output);
return if ON_WINDOWS;
my $fs = FILESYSTEM();
my %files = %{$fs->{all_files}};
my %dirs = %{$fs->{create_dirs}};
my %recurse_dirs = %{$fs->{recurse_dirs}};
print get_text('install_file_perms_fix') . "\n" if $output;
foreach my $dir (sort keys %dirs)
{
next unless -d $dir;
_fix_perms($dir, $owner_id, $group_id, $dirs{$dir});
}
foreach my $pattern (sort keys %recurse_dirs)
{
my $perms = $recurse_dirs{$pattern};
# %recurse_dirs supports globs
foreach my $dir (glob $pattern)
{
next unless -d $dir;
_fix_perms_recursively($dir, $owner_id, $group_id, $perms);
}
}
foreach my $file (sort keys %files)
{
# %files supports globs
foreach my $filename (glob $file)
{
# Don't touch directories.
next if -d $filename || !-e $filename;
_fix_perms($filename, $owner_id, $group_id, $files{$file}->{perms});
}
}
}
sub _get_owner_and_group
{
my ($output) = @_;
my $group_id = _check_web_server_group($output);
return () if ON_WINDOWS;
my $owner_id = POSIX::getuid();
$group_id = POSIX::getgid() unless defined $group_id;
return ($owner_id, $group_id);
}
sub _fix_perms
{
my ($name, $owner, $group, $perms) = @_;
#printf ("Changing $name to %o\n", $perms);
# The webserver should never try to chown files.
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE)
{
chown $owner, $group, $name
or warn install_string('chown_failed', { path => $name, error => $! }) . "\n";
}
chmod $perms, $name
or warn install_string('chmod_failed', { path => $name, error => $! }) . "\n";
}
sub _fix_perms_recursively
{
my ($dir, $owner_id, $group_id, $perms) = @_;
# Set permissions on the directory itself.
_fix_perms($dir, $owner_id, $group_id, $perms->{dirs});
# Now recurse through the directory and set the correct permissions
# on subdirectories and files.
find({ no_chdir => 1, wanted => sub
{
my $name = $File::Find::name;
if (-d $name)
{
_fix_perms($name, $owner_id, $group_id, $perms->{dirs});
}
else
{
_fix_perms($name, $owner_id, $group_id, $perms->{files});
}
}}, $dir);
}
sub _check_web_server_group
{
my ($output) = @_;
my $group = Bugzilla->localconfig->{webservergroup};
my $filename = bz_locations()->{localconfig};
my $group_id;
# If we are on Windows, webservergroup does nothing
if (ON_WINDOWS && $group && $output)
{
print "\n\n" . get_text('install_webservergroup_windows') . "\n\n";
}
# If we're not on Windows, make sure that webservergroup isn't
# empty.
elsif (!ON_WINDOWS && !$group && $output)
{
print "\n\n" . get_text('install_webservergroup_empty') . "\n\n";
}
# If we're not on Windows, make sure we are actually a member of
# the webservergroup.
elsif (!ON_WINDOWS && $group)
{
$group_id = getgrnam($group);
ThrowCodeError('invalid_webservergroup', { group => $group })
unless defined $group_id;
# If on unix, see if we need to print a warning about a webservergroup
# that we can't chgrp to
if ($output && $< != 0 && !grep($_ eq $group_id, split(" ", $))))
{
print "\n\n" . get_text('install_webservergroup_not_in') . "\n\n";
}
}
return $group_id;
}
1;
__END__
=head1 NAME
Bugzilla::Install::Filesystem - Fix up the filesystem during
installation.
=head1 DESCRIPTION
This module is used primarily by L<checksetup.pl> to modify the
filesystem during installation, including creating the data/ directory.
=head1 SUBROUTINES
=over
=item C<update_filesystem()>
Description: Creates all the directories and files that Bugzilla
needs to function but doesn't ship with. Also does
any updates to these files as necessary during an
upgrade.
Returns: nothing
=item C<create_htaccess()>
Description: Creates all of the .htaccess files for Apache,
in the various Bugzilla directories. Also updates
the .htaccess files if they need updating.
Params: none
Returns: nothing
=item C<fix_all_file_permissions($output)>
Description: Sets all the file permissions on all of Bugzilla's files
to what they should be. Note that permissions are different
depending on whether or not C<$webservergroup> is set
in F<localconfig>.
Params: C<$output> - C<true> if you want this function to print
out information about what it's doing.
Returns: nothing
=item C<fix_file_permissions>
Given the name of a file, its permissions will be fixed according to
how they are supposed to be set in Bugzilla's current configuration.
If it fails to set the permissions, a warning will be printed to STDERR.
=back