1
# -*- Mode: perl; indent-tabs-mode: nil -*-
3
# The contents of this file are subject to the Mozilla Public
4
# License Version 1.1 (the "License"); you may not use this file
5
# except in compliance with the License. You may obtain a copy of
6
# the License at http://www.mozilla.org/MPL/
8
# Software distributed under the License is distributed on an "AS
9
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
10
# implied. See the License for the specific language governing
11
# rights and limitations under the License.
13
# The Original Code is the Bugzilla Bug Tracking System.
15
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
16
# Bill Barry <after.fallout@gmail.com>
18
package Bugzilla::Install::Filesystem;
20
# NOTE: This package may "use" any modules that it likes,
21
# and localconfig is available. However, all functions in this
22
# package should assume that:
24
# * Templates are not available.
25
# * Files do not have the correct permissions.
26
# * The database does not exist.
30
use Bugzilla::Constants;
32
use Bugzilla::Install::Localconfig;
41
use base qw(Exporter);
45
fix_all_file_permissions
48
# This looks like a constant because it effectively is, but
49
# it has to call other subroutines and read the current filesystem,
50
# so it's defined as a sub. This is not exported, so it doesn't have
51
# a perldoc. However, look at the various hashes defined inside this
52
# function to understand what it returns. (There are comments throughout.)
54
# The rationale for the file permissions is that the web server generally
55
# runs as apache, so the cgi scripts should not be writable for apache,
56
# otherwise someone may find it possible to change the cgis when exploiting
57
# some security flaw somewhere (not necessarily in Bugzilla!)
59
my $datadir = bz_locations()->{'datadir'};
60
my $attachdir = bz_locations()->{'attachdir'};
61
my $extensionsdir = bz_locations()->{'extensionsdir'};
62
my $webdotdir = bz_locations()->{'webdotdir'};
63
my $templatedir = bz_locations()->{'templatedir'};
64
my $libdir = bz_locations()->{'libpath'};
65
my $skinsdir = bz_locations()->{'skinsdir'};
67
my $ws_group = Bugzilla->localconfig->{'webservergroup'};
69
# The set of permissions that we use:
72
# Executable by the web server
73
my $ws_executable = $ws_group ? 0750 : 0755;
74
# Executable by the owner only.
75
my $owner_executable = 0700;
76
# Readable by the web server.
77
my $ws_readable = $ws_group ? 0640 : 0644;
78
# Readable by the owner only.
79
my $owner_readable = 0600;
80
# Writeable by the web server.
81
my $ws_writeable = $ws_group ? 0660 : 0666;
84
# Readable by the web server.
85
my $ws_dir_readable = $ws_group ? 0750 : 0755;
86
# Readable only by the owner.
87
my $owner_dir_readable = 0700;
88
# Writeable by the web server.
89
my $ws_dir_writeable = $ws_group ? 0770 : 01777;
90
# The webserver can overwrite files owned by other users,
92
my $ws_dir_full_control = $ws_group ? 0770 : 0777;
94
# Note: When being processed by checksetup, these have their permissions
95
# set in this order: %all_dirs, %recurse_dirs, %all_files.
97
# Each is processed in alphabetical order of keys, so shorter keys
98
# will have their permissions set before longer keys (thus setting
99
# the permissions on parent directories before setting permissions
100
# on their children).
102
# --- FILE PERMISSIONS (Non-created files) --- #
104
'*' => { perms => $ws_readable },
105
'*.cgi' => { perms => $ws_executable },
106
'whineatnews.pl' => { perms => $ws_executable },
107
'collectstats.pl' => { perms => $ws_executable },
108
'checksetup.pl' => { perms => $owner_executable },
109
'importxml.pl' => { perms => $ws_executable },
110
'runtests.pl' => { perms => $owner_executable },
111
'testserver.pl' => { perms => $ws_executable },
112
'whine.pl' => { perms => $ws_executable },
113
'customfield.pl' => { perms => $owner_executable },
114
'email_in.pl' => { perms => $ws_executable },
116
'docs/makedocs.pl' => { perms => $owner_executable },
117
'docs/rel_notes.txt' => { perms => $ws_readable },
118
'docs/README.docs' => { perms => $owner_readable },
119
"$datadir/bugzilla-update.xml" => { perms => $ws_writeable },
120
"$datadir/params" => { perms => $ws_writeable },
121
"$datadir/mailer.testfile" => { perms => $ws_writeable },
124
# Directories that we want to set the perms on, but not
125
# recurse through. These are directories we didn't create
127
my %non_recurse_dirs = (
128
'.' => $ws_dir_readable,
129
docs => $ws_dir_readable,
132
# This sets the permissions for each item inside each of these
133
# directories, including the directory itself.
134
# 'CVS' directories are special, though, and are never readable by
137
# Writeable directories
138
"$datadir/template" => { files => $ws_readable,
139
dirs => $ws_dir_full_control },
140
$attachdir => { files => $ws_writeable,
141
dirs => $ws_dir_writeable },
142
$webdotdir => { files => $ws_writeable,
143
dirs => $ws_dir_writeable },
144
graphs => { files => $ws_writeable,
145
dirs => $ws_dir_writeable },
147
# Readable directories
148
"$datadir/mining" => { files => $ws_readable,
149
dirs => $ws_dir_readable },
150
"$datadir/duplicates" => { files => $ws_readable,
151
dirs => $ws_dir_readable },
152
"$libdir/Bugzilla" => { files => $ws_readable,
153
dirs => $ws_dir_readable },
154
$templatedir => { files => $ws_readable,
155
dirs => $ws_dir_readable },
156
images => { files => $ws_readable,
157
dirs => $ws_dir_readable },
158
css => { files => $ws_readable,
159
dirs => $ws_dir_readable },
160
js => { files => $ws_readable,
161
dirs => $ws_dir_readable },
162
skins => { files => $ws_readable,
163
dirs => $ws_dir_readable },
164
t => { files => $owner_readable,
165
dirs => $owner_dir_readable },
166
'docs/html' => { files => $ws_readable,
167
dirs => $ws_dir_readable },
168
'docs/pdf' => { files => $ws_readable,
169
dirs => $ws_dir_readable },
170
'docs/txt' => { files => $ws_readable,
171
dirs => $ws_dir_readable },
172
'docs/images' => { files => $ws_readable,
173
dirs => $ws_dir_readable },
174
'docs/lib' => { files => $owner_readable,
175
dirs => $owner_dir_readable },
176
'docs/xml' => { files => $owner_readable,
177
dirs => $owner_dir_readable },
180
# --- FILES TO CREATE --- #
182
# The name of each directory that we should actually *create*,
183
# pointing at its default permissions.
185
$datadir => $ws_dir_full_control,
186
"$datadir/mimedump-tmp" => $ws_dir_writeable,
187
"$datadir/mining" => $ws_dir_readable,
188
"$datadir/duplicates" => $ws_dir_readable,
189
$attachdir => $ws_dir_writeable,
190
$extensionsdir => $ws_dir_readable,
191
graphs => $ws_dir_writeable,
192
$webdotdir => $ws_dir_writeable,
193
'skins/custom' => $ws_dir_readable,
194
'skins/contrib' => $ws_dir_readable,
197
# The name of each file, pointing at its default permissions and
200
"$datadir/mail" => { perms => $ws_readable },
203
# Each standard stylesheet has an associated custom stylesheet that
204
# we create. Also, we create placeholders for standard stylesheets
205
# for contrib skins which don't provide them themselves.
206
foreach my $skin_dir ("$skinsdir/custom", <$skinsdir/contrib/*>) {
207
next unless -d $skin_dir;
208
next if basename($skin_dir) =~ /^cvs$/i;
209
foreach (<$skinsdir/standard/*.css>) {
210
my $standard_css_file = basename($_);
211
my $custom_css_file = "$skin_dir/$standard_css_file";
212
$create_files{$custom_css_file} = { perms => $ws_readable, contents => <<EOT
214
* Custom rules for $standard_css_file.
215
* The rules you put here override rules in that stylesheet.
222
# Because checksetup controls the creation of index.html separately
223
# from all other files, it gets its very own hash.
225
'index.html' => { perms => $ws_readable, contents => <<EOT
226
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
229
<meta http-equiv="Refresh" content="0; URL=index.cgi">
232
<h1>I think you are looking for <a href="index.cgi">index.cgi</a></h1>
239
# Because checksetup controls the .htaccess creation separately
240
# by a localconfig variable, these go in a separate variable from
242
my $ht_default_deny = <<EOT;
243
# nothing in this directory is retrievable unless overridden by an .htaccess
249
"$attachdir/.htaccess" => { perms => $ws_readable,
250
contents => $ht_default_deny },
251
"$libdir/Bugzilla/.htaccess" => { perms => $ws_readable,
252
contents => $ht_default_deny },
253
"$templatedir/.htaccess" => { perms => $ws_readable,
254
contents => $ht_default_deny },
256
'.htaccess' => { perms => $ws_readable, contents => <<EOT
257
# Don't allow people to retrieve non-cgi executable files or our private data
258
<FilesMatch ^(.*\\.pm|.*\\.pl|.*localconfig.*)\$>
264
"$webdotdir/.htaccess" => { perms => $ws_readable, contents => <<EOT
265
# Restrict access to .dot files to the public webdot server at research.att.com
266
# if research.att.com ever changes their IP, or if you use a different
267
# webdot server, you'll need to edit this
268
<FilesMatch \\.dot\$>
269
Allow from 192.20.225.0/24
273
# Allow access to .png files created by a local copy of 'dot'
274
<FilesMatch \\.png\$>
278
# And no directory listings, either.
283
# Even though $datadir may not (and should not) be in the webtree,
284
# we can't know for sure, so create the .htaccess anyway. It's harmless
285
# if it's not accessible...
286
"$datadir/.htaccess" => { perms => $ws_readable, contents => <<EOT
287
# Nothing in this directory is retrievable unless overridden by an .htaccess
288
# in a subdirectory; the only exception is duplicates.rdf, which is used by
289
# duplicates.xul and must be loadable over the web
291
<Files duplicates.rdf>
300
my %all_files = (%create_files, %htaccess, %index_html, %files);
301
my %all_dirs = (%create_dirs, %non_recurse_dirs);
304
create_dirs => \%create_dirs,
305
recurse_dirs => \%recurse_dirs,
306
all_dirs => \%all_dirs,
308
create_files => \%create_files,
309
htaccess => \%htaccess,
310
index_html => \%index_html,
311
all_files => \%all_files,
315
sub update_filesystem {
317
my $fs = FILESYSTEM();
318
my %dirs = %{$fs->{create_dirs}};
319
my %files = %{$fs->{create_files}};
321
my $datadir = bz_locations->{'datadir'};
322
# If the graphs/ directory doesn't exist, we're upgrading from
323
# a version old enough that we need to update the $datadir/mining
325
if (-d "$datadir/mining" && !-d 'graphs') {
326
_update_old_charts($datadir);
329
# By sorting the dirs, we assure that shorter-named directories
330
# (meaning parent directories) are always created before their
332
foreach my $dir (sort keys %dirs) {
334
print "Creating $dir directory...\n";
335
mkdir $dir || die $!;
336
# For some reason, passing in the permissions to "mkdir"
337
# doesn't work right, but doing a "chmod" does.
338
chmod $dirs{$dir}, $dir || die $!;
342
_create_files(%files);
343
if ($params->{index_html}) {
344
_create_files(%{$fs->{index_html}});
346
elsif (-e 'index.html') {
347
my $templatedir = bz_locations()->{'templatedir'};
350
*** It appears that you still have an old index.html hanging around.
351
Either the contents of this file should be moved into a template and
352
placed in the '$templatedir/en/custom' directory, or you should delete
358
# Delete old files that no longer need to exist
360
# 2001-04-29 jake@bugzilla.org - Remove oldemailtech
361
# http://bugzilla.mozilla.org/show_bugs.cgi?id=71552
363
print "Removing shadow directory...\n";
367
if (-e "$datadir/versioncache") {
368
print "Removing versioncache...\n";
369
unlink "$datadir/versioncache";
374
sub create_htaccess {
375
_create_files(%{FILESYSTEM()->{htaccess}});
377
# Repair old .htaccess files
378
my $htaccess = new IO::File('.htaccess', 'r') || die ".htaccess: $!";
380
{ local $/; $old_data = <$htaccess>; }
384
if ($old_data =~ s/\|localconfig\|/\|.*localconfig.*\|/) {
387
if ($old_data !~ /\(\.\*\\\.pm\|/) {
388
$old_data =~ s/\(/(.*\\.pm\|/;
392
print "Repairing .htaccess...\n";
393
$htaccess = new IO::File('.htaccess', 'w') || die $!;
394
print $htaccess $old_data;
399
my $webdot_dir = bz_locations()->{'webdotdir'};
400
# The public webdot IP address changed.
401
my $webdot = new IO::File("$webdot_dir/.htaccess", 'r')
402
|| die "$webdot_dir/.htaccess: $!";
404
{ local $/; $webdot_data = <$webdot>; }
406
if ($webdot_data =~ /192\.20\.225\.10/) {
407
print "Repairing $webdot_dir/.htaccess...\n";
408
$webdot_data =~ s/192\.20\.225\.10/192.20.225.0\/24/g;
409
$webdot = new IO::File("$webdot_dir/.htaccess", 'w') || die $!;
410
print $webdot $webdot_data;
415
# A helper for the above functions.
419
# It's not necessary to sort these, but it does make the
420
# output of checksetup.pl look a bit nicer.
421
foreach my $file (sort keys %files) {
423
print "Creating $file...\n";
424
my $info = $files{$file};
425
my $fh = new IO::File($file, O_WRONLY | O_CREAT, $info->{perms})
427
print $fh $info->{contents} if $info->{contents};
433
# If you ran a REALLY old version of Bugzilla, your chart files are in the
434
# wrong format. This code is a little messy, because it's very old, and
435
# when moving it into this module, I couldn't test it so I left it almost
437
sub _update_old_charts {
439
print "Updating old chart storage format...\n";
440
foreach my $in_file (glob("$datadir/mining/*")) {
441
# Don't try and upgrade image or db files!
442
next if (($in_file =~ /\.gif$/i) ||
443
($in_file =~ /\.png$/i) ||
444
($in_file =~ /\.db$/i) ||
445
($in_file =~ /\.orig$/i));
447
rename("$in_file", "$in_file.orig") or next;
448
open(IN, "$in_file.orig") or next;
449
open(OUT, '>', $in_file) or next;
451
# Fields in the header
454
# Fields we changed to half way through by mistake
455
# This list comes from an old version of collectstats.pl
456
# This part is only for people who ran later versions of 2.11 (devel)
457
my @intermediate_fields = qw(DATE UNCONFIRMED NEW ASSIGNED REOPENED
458
RESOLVED VERIFIED CLOSED);
460
# Fields we actually want (matches the current collectstats.pl)
461
my @out_fields = qw(DATE NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED
462
VERIFIED CLOSED FIXED INVALID WONTFIX LATER REMIND
463
DUPLICATE WORKSFORME MOVED);
466
if (/^# fields?: (.*)\s$/) {
467
@declared_fields = map uc, (split /\||\r/, $1);
468
print OUT "# fields: ", join('|', @out_fields), "\n";
470
elsif (/^(\d+\|.*)/) {
471
my @data = split(/\||\r/, $1);
473
if (@data == @declared_fields) {
475
for my $i (0 .. $#declared_fields) {
476
$data{$declared_fields[$i]} = $data[$i];
479
elsif (@data == @intermediate_fields) {
480
# Must have changed over at this point
481
for my $i (0 .. $#intermediate_fields) {
482
$data{$intermediate_fields[$i]} = $data[$i];
485
elsif (@data == @out_fields) {
486
# This line's fine - it has the right number of entries
487
for my $i (0 .. $#out_fields) {
488
$data{$out_fields[$i]} = $data[$i];
492
print "Oh dear, input line $. of $in_file had " .
493
scalar(@data) . " fields\nThis was unexpected.",
494
" You may want to check your data files.\n";
498
map { defined ($data{$_}) ? ($data{$_}) : "" } @out_fields),
512
sub fix_all_file_permissions {
515
my $ws_group = Bugzilla->localconfig->{'webservergroup'};
516
my $group_id = _check_web_server_group($ws_group, $output);
518
return if ON_WINDOWS;
520
my $fs = FILESYSTEM();
521
my %files = %{$fs->{all_files}};
522
my %dirs = %{$fs->{all_dirs}};
523
my %recurse_dirs = %{$fs->{recurse_dirs}};
525
print get_text('install_file_perms_fix') . "\n" if $output;
527
my $owner_id = POSIX::getuid();
528
$group_id = POSIX::getgid() unless defined $group_id;
530
foreach my $dir (sort keys %dirs) {
532
_fix_perms($dir, $owner_id, $group_id, $dirs{$dir});
535
foreach my $dir (sort keys %recurse_dirs) {
537
# Set permissions on the directory itself.
538
my $perms = $recurse_dirs{$dir};
539
_fix_perms($dir, $owner_id, $group_id, $perms->{dirs});
540
# Now recurse through the directory and set the correct permissions
541
# on subdirectories and files.
542
find({ no_chdir => 1, wanted => sub {
543
my $name = $File::Find::name;
545
_fix_perms($name, $owner_id, $group_id, $perms->{dirs});
548
_fix_perms($name, $owner_id, $group_id, $perms->{files});
553
foreach my $file (sort keys %files) {
554
# %files supports globs
555
foreach my $filename (glob $file) {
556
# Don't touch directories.
557
next if -d $filename || !-e $filename;
558
_fix_perms($filename, $owner_id, $group_id,
559
$files{$file}->{perms});
563
_fix_cvs_dirs($owner_id, '.');
566
# A helper for fix_all_file_permissions
568
my ($owner_id, $dir) = @_;
569
my $owner_gid = POSIX::getgid();
570
find({ no_chdir => 1, wanted => sub {
571
my $name = $File::Find::name;
572
if ($File::Find::dir =~ /\/CVS/ || $_ eq '.cvsignore'
573
|| (-d $name && $_ eq 'CVS')) {
574
_fix_perms($name, $owner_id, $owner_gid, 0700);
580
my ($name, $owner, $group, $perms) = @_;
581
#printf ("Changing $name to %o\n", $perms);
582
chown $owner, $group, $name
583
|| warn "Failed to change ownership of $name: $!";
585
|| warn "Failed to change permissions of $name: $!";
588
sub _check_web_server_group {
589
my ($group, $output) = @_;
591
my $filename = bz_locations()->{'localconfig'};
594
# If we are on Windows, webservergroup does nothing
595
if (ON_WINDOWS && $group && $output) {
596
print "\n\n" . get_text('install_webservergroup_windows') . "\n\n";
599
# If we're not on Windows, make sure that webservergroup isn't
601
elsif (!ON_WINDOWS && !$group && $output) {
602
print "\n\n" . get_text('install_webservergroup_empty') . "\n\n";
605
# If we're not on Windows, make sure we are actually a member of
606
# the webservergroup.
607
elsif (!ON_WINDOWS && $group) {
608
$group_id = getgrnam($group);
609
ThrowCodeError('invalid_webservergroup', { group => $group })
610
unless defined $group_id;
612
# If on unix, see if we need to print a warning about a webservergroup
613
# that we can't chgrp to
614
if ($output && $< != 0 && !grep($_ eq $group_id, split(" ", $)))) {
615
print "\n\n" . get_text('install_webservergroup_not_in') . "\n\n";
628
Bugzilla::Install::Filesystem - Fix up the filesystem during
633
This module is used primarily by L<checksetup.pl> to modify the
634
filesystem during installation, including creating the data/ directory.
640
=item C<update_filesystem({ index_html => 0 })>
642
Description: Creates all the directories and files that Bugzilla
643
needs to function but doesn't ship with. Also does
644
any updates to these files as necessary during an
647
Params: C<index_html> - Whether or not we should create
648
the F<index.html> file.
652
=item C<create_htaccess()>
654
Description: Creates all of the .htaccess files for Apache,
655
in the various Bugzilla directories. Also updates
656
the .htaccess files if they need updating.
662
=item C<fix_all_file_permissions($output)>
664
Description: Sets all the file permissions on all of Bugzilla's files
665
to what they should be. Note that permissions are different
666
depending on whether or not C<$webservergroup> is set
669
Params: C<$output> - C<true> if you want this function to print
670
out information about what it's doing.