]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 346552: Move checksetup's permission-fixing code into a module
authormkanat%bugzilla.org <>
Tue, 1 Aug 2006 05:18:25 +0000 (05:18 +0000)
committermkanat%bugzilla.org <>
Tue, 1 Aug 2006 05:18:25 +0000 (05:18 +0000)
Patch By Max Kanat-Alexander <mkanat@bugzilla.org> (module owner) a=myk

Bugzilla/Install/Filesystem.pm
Bugzilla/Template.pm
checksetup.pl

index 111e68c9d2e984381d94db512745a57968d07654..7fc7b73059c259b9ea611d72468386651e7d79bd 100644 (file)
@@ -30,12 +30,15 @@ use Bugzilla::Constants;
 use Bugzilla::Install::Localconfig;
 
 use Fcntl;
+use File::Find;
 use IO::File;
+use POSIX ();
 
 use base qw(Exporter);
 our @EXPORT = qw(
     update_filesystem
     create_htaccess
+    fix_all_file_permissions
 );
 
 # This looks like a constant because it effectively is, but
@@ -43,6 +46,11 @@ our @EXPORT = qw(
 # 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 the web server generally 
+# runs as apache, so the cgi scripts should not be writable for apache,
+# 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'};
@@ -53,24 +61,132 @@ sub FILESYSTEM {
 
     my $ws_group      = read_localconfig()->{'webservergroup'};
 
-    # The name of each directory, pointing at its default permissions.
-    my %dirs = (
-        $datadir                => 0770,
-        "$datadir/mimedump-tmp" => 01777,
-        "$datadir/mining"       => 0700,
-        "$datadir/duplicates"   => $ws_group ? 0770 : 01777,
-        $attachdir              => 0770,
-        $extensionsdir          => 0770,
-        graphs                  => 0770,
-        $webdotdir              => 0700,
-        'skins/custom'          => 0700,
+    # 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 ? 0640 : 0644;
+    # Readable by the owner only.
+    my $owner_readable = 0600;
+    # Writeable by the web server.
+    my $ws_writeable = $ws_group ? 0660 : 0666;
+
+    # DIRECTORIES
+    # Readable by the web server.
+    my $ws_dir_readable  = $ws_group ? 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;
+
+    # Note: When being processed by checksetup, these have their permissions
+    # set in this order: %all_dirs, %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 (Non-created files) --- #
+    my %files = (
+        '*'               => { perms => $ws_readable },
+        '*.cgi'           => { perms => $ws_executable },
+        'whineatnews.pl'  => { perms => $ws_executable },
+        'collectstats.pl' => { perms => $ws_executable },
+        'checksetup.pl'   => { perms => $owner_executable },
+        'importxml.pl'    => { perms => $ws_executable },
+        'runtests.pl'     => { perms => $owner_executable },
+        'testserver.pl'   => { perms => $ws_executable },
+        'whine.pl'        => { perms => $ws_executable },
+        'customfield.pl'  => { perms => $owner_executable },
+
+        'docs/html/makedocs.pl' => { perms => $owner_executable },
+        'docs/rel_notes.txt'    => { perms => $ws_readable },
+        'docs/README.docs'      => { perms => $owner_readable },
+        "$datadir/bugzilla-update.xml" => { perms => $ws_writeable },
+        "$datadir/params" => { perms => $ws_writeable },
+    );
+
+    # Directories that we want to set the perms on, but not
+    # recurse through. These are directories we didn't create
+    # in checkesetup.pl.
+    my %non_recurse_dirs = (
+        '.'  => $ws_dir_readable,
+        docs => $ws_dir_readable,
+    );
+
+    # This sets the permissions for each item inside each of these 
+    # directories, including the directory itself. 
+    # 'CVS' directories are special, though, and are never readable by 
+    # the webserver.
+    my %recurse_dirs = (
+        # Writeable directories
+        "$datadir/template" => { files => $ws_readable, 
+                                  dirs => $ws_dir_writeable },
+         $attachdir         => { files => $ws_writeable,
+                                  dirs => $ws_dir_writeable },
+         $webdotdir         => { files => $ws_writeable,
+                                  dirs => $ws_dir_writeable },
+         graphs             => { files => $ws_writeable,
+                                  dirs => $ws_dir_writeable },
+
+         # Readable directories
+         "$datadir/mining"     => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         "$datadir/duplicates" => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         "$libdir/Bugzilla"    => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         $templatedir          => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         images                => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         css                   => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         js                    => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         skins                 => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         t                     => { files => $owner_readable,
+                                     dirs => $owner_dir_readable },
+         'docs/html'           => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         'docs/pdf'            => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         'docs/txt'            => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         'docs/images'         => { files => $ws_readable,
+                                     dirs => $ws_dir_readable },
+         'docs/xml'            => { files => $owner_readable,
+                                     dirs => $owner_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_writeable,
+        "$datadir/mimedump-tmp" => $ws_dir_writeable,
+        "$datadir/mining"       => $ws_dir_readable,
+        "$datadir/duplicates"   => $ws_dir_readable,
+        $attachdir              => $ws_dir_writeable,
+        $extensionsdir          => $ws_dir_readable,
+        graphs                  => $ws_dir_writeable,
+        $webdotdir              => $ws_dir_writeable,
+        'skins/custom'          => $ws_dir_readable,
     );
 
     # The name of each file, pointing at its default permissions and
     # default contents.
-    my %files = (
-        "$datadir/mail"    => {},
-        'skins/.cvsignore' => { contents => ".cvsignore\ncustom\n" },
+    my %create_files = (
+        "$datadir/mail"    => { perms => $ws_readable },
+        'skins/.cvsignore' => { perms    => $owner_readable,
+                                contents => ".cvsignore\ncustom\n" },
     );
 
     # Each standard stylesheet has an associated custom stylesheet that
@@ -78,7 +194,7 @@ sub FILESYSTEM {
     foreach my $standard (<skins/standard/*.css>) {
         my $custom = $standard;
         $custom =~ s|^skins/standard|skins/custom|;
-        $files{$custom} = { contents => <<EOT
+        $files{$custom} = { perms => $ws_readable, contents => <<EOT
 /*
  * Custom rules for $standard.
  * The rules you put here override rules in that stylesheet.
@@ -90,7 +206,7 @@ EOT
     # Because checksetup controls the creation of index.html separately
     # from all other files, it gets its very own hash.
     my %index_html = (
-        'index.html' => { contents => <<EOT
+        'index.html' => { perms => $ws_readable, contents => <<EOT
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html>
 <head>
@@ -106,8 +222,7 @@ EOT
 
     # Because checksetup controls the .htaccess creation separately
     # by a localconfig variable, these go in a separate variable from
-    # %files.
-    my $ht_perms = $ws_group ? 0640 : 0644;
+    # %create_files.
     my $ht_default_deny = <<EOT;
 # nothing in this directory is retrievable unless overridden by an .htaccess
 # in a subdirectory
@@ -115,14 +230,14 @@ deny from all
 EOT
 
     my %htaccess = (
-        "$attachdir/.htaccess"       => { perms    => $ht_perms,
+        "$attachdir/.htaccess"       => { perms    => $ws_readable,
                                           contents => $ht_default_deny },
-        "$libdir/Bugzilla/.htaccess" => { perms    => $ht_perms,
+        "$libdir/Bugzilla/.htaccess" => { perms    => $ws_readable,
                                           contents => $ht_default_deny },
-        "$templatedir/.htaccess"     => { perms    => $ht_perms,
+        "$templatedir/.htaccess"     => { perms    => $ws_readable,
                                           contents => $ht_default_deny },
 
-        '.htaccess' => { perms => $ht_perms, contents => <<EOT
+        '.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
@@ -130,7 +245,7 @@ EOT
 EOT
         },
 
-        "$webdotdir/.htaccess" => { perms => $ht_perms, contents => <<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
@@ -152,7 +267,7 @@ EOT
         # Even though $datadir may not (and should not) be in the webtree,
         # we can't know for sure, so create the .htaccess anyway. It's harmless
         # if it's not accessible...
-        "$datadir/.htaccess" => { perms    => $ht_perms, contents => <<EOT
+        "$datadir/.htaccess" => { perms    => $ws_readable, contents => <<EOT
 # Nothing in this directory is retrievable unless overridden by an .htaccess
 # in a subdirectory; the only exception is duplicates.rdf, which is used by
 # duplicates.xul and must be loadable over the web
@@ -166,19 +281,26 @@ EOT
         },
     );
 
+    my %all_files = (%create_files, %htaccess, %index_html, %files);
+    my %all_dirs  = (%create_dirs, %non_recurse_dirs);
+
     return {
-        dirs       => \%dirs,
-        files      => \%files,
-        htaccess   => \%htaccess,
-        index_html => \%index_html,
+        create_dirs  => \%create_dirs,
+        recurse_dirs => \%recurse_dirs,
+        all_dirs     => \%all_dirs,
+
+        create_files => \%create_files,
+        htaccess     => \%htaccess,
+        index_html   => \%index_html,
+        all_files    => \%all_files,
     };
 }
 
 sub update_filesystem {
     my ($params) = @_;
     my $fs = FILESYSTEM();
-    my %dirs  = %{$fs->{dirs}};
-    my %files = %{$fs->{files}};
+    my %dirs  = %{$fs->{create_dirs}};
+    my %files = %{$fs->{create_files}};
 
     my $datadir = bz_locations->{'datadir'};
     # If the graphs/ directory doesn't exist, we're upgrading from
@@ -339,6 +461,87 @@ sub _update_old_charts {
     } 
 }
 
+
+sub fix_all_file_permissions {
+    my ($output) = @_;
+
+    return if ON_WINDOWS;
+
+    my $fs = FILESYSTEM();
+    my %files = %{$fs->{all_files}};
+    my %dirs  = %{$fs->{all_dirs}};
+    my %recurse_dirs = %{$fs->{recurse_dirs}};
+
+    print "Fixing file permissions...\n" if $output;
+
+    my $owner_id = POSIX::getuid();
+    my $group_id = POSIX::getgid();
+    my $ws_group = read_localconfig()->{'webservergroup'};
+    if ($ws_group) {
+        my $ws_group_id = getgrnam($ws_group);
+        die "There is no such group: $ws_group. Check your \$webservergroup"
+            . " setting in localconfig" unless defined $ws_group_id;
+        $group_id = $ws_group_id;
+    }
+
+    foreach my $dir (sort keys %dirs) {
+        next unless -d $dir;
+        _fix_perms($dir, $owner_id, $group_id, $dirs{$dir});
+    }
+
+    foreach my $dir (sort keys %recurse_dirs) {
+        next unless -d $dir;
+        # Set permissions on the directory itself.
+        my $perms = $recurse_dirs{$dir};
+        _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);
+    }
+
+    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});
+        }
+    }
+
+    _fix_cvs_dirs($owner_id, '.');
+}
+
+# A helper for fix_all_file_permissions
+sub _fix_cvs_dirs {
+    my ($owner_id, $dir) = @_;
+    my $owner_gid = POSIX::getgid();
+    find({ no_chdir => 1, wanted => sub {
+        my $name = $File::Find::name;
+        if ($File::Find::dir =~ /\/CVS/ || $_ eq '.cvsignore'
+            || (-d $name && $_ eq 'CVS')) {
+            _fix_perms($name, $owner_id, $owner_gid, 0700);
+        }
+    }}, $dir);
+}
+
+sub _fix_perms {
+    my ($name, $owner, $group, $perms) = @_;
+    #printf ("Changing $name to %o\n", $perms);
+    chown $owner, $group, $name 
+        || warn "Failed to change ownership of $name: $!";
+    chmod $perms, $name
+        || warn "Failed to change permissions of $name: $!";
+}
+
 1;
 
 __END__
@@ -385,4 +588,16 @@ 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
+
 =back
index f3625ee229d8e921af40f29238f6aaba9efe4aff..756c3cc44ef0d423fb0d400677b728c036f3accb 100644 (file)
@@ -853,6 +853,7 @@ sub precompile_templates {
         # Precompile all the templates found in all the directories.
         %_templates_to_precompile = ();
         foreach my $subdir (qw(custom extension default), bz_locations()->{'project'}) {
+            next unless $subdir; # If 'project' is empty.
             $_current_path = File::Spec->catdir($templatedir, $dir, $subdir);
             next unless -d $_current_path;
             # Traverse the template hierarchy.
@@ -861,7 +862,6 @@ sub precompile_templates {
         # The sort isn't totally necessary, but it makes debugging easier
         # by making the templates always be compiled in the same order.
         foreach my $file (sort keys %_templates_to_precompile) {
-            my $path = File::Spec->catdir($templatedir, $dir);
             # Compile the template but throw away the result. This has the side-
             # effect of writing the compiled version to disk.
             $template->context->template($file);
@@ -876,7 +876,7 @@ sub _precompile_push {
     return if ($name =~ /\/CVS\//);
     return if ($name !~ /\.tmpl$/);
    
-    $name =~ s/\Q$_current_path\E\///; 
+    $name =~ s/\Q$_current_path\E\///;
     $_templates_to_precompile{$name} = 1;
 }
 
index 6eeeac0ed6626333afea951a4f6fd963f2596bea..a9cb775143d328684563511b0fbfc99907b05627 100755 (executable)
@@ -312,7 +312,8 @@ require Bugzilla::Install::Localconfig;
 import Bugzilla::Install::Localconfig qw(read_localconfig update_localconfig);
 
 require Bugzilla::Install::Filesystem;
-import Bugzilla::Install::Filesystem qw(update_filesystem create_htaccess);
+import Bugzilla::Install::Filesystem qw(update_filesystem create_htaccess
+                                        fix_all_file_permissions);
 
 require Bugzilla::DB;
 require Bugzilla::Template;
@@ -376,165 +377,10 @@ Bugzilla::Template::precompile_templates(!$silent)
     unless $switch{'no-templates'};
 
 ###########################################################################
-# Set proper rights
+# Set proper rights (--CHMOD--)
 ###########################################################################
 
-#
-# Here we use --CHMOD-- and friends to set the file permissions
-#
-# The rationale is that the web server generally runs as apache, so the cgi
-# scripts should not be writable for apache, otherwise someone may be possible
-# to change the cgi's when exploiting some security flaw somewhere (not
-# necessarily in Bugzilla!)
-#
-# Also, some *.pl files are executable, some are not.
-#
-# +++ Can anybody tell me what a Windows Perl would do with this code?
-#
-# Changes 03/14/00 by SML
-#
-# This abstracts out what files are executable and what ones are not.  It makes
-# for slightly neater code and lets us do things like determine exactly which
-# files are executable and which ones are not.
-#
-# Not all directories have permissions changed on them.  i.e., changing ./CVS
-# to be 0640 is bad.
-#
-# Fixed bug in chmod invocation.  chmod (at least on my linux box running perl
-# 5.005 needs a valid first argument, not 0.
-#
-# (end changes, 03/14/00 by SML)
-#
-# Changes 15/06/01 kiko@async.com.br
-# 
-# Fix file permissions for non-webservergroup installations (see
-# http://bugzilla.mozilla.org/show_bug.cgi?id=71555). I'm setting things
-# by default to world readable/executable for all files, and
-# world-writable (with sticky on) to data and graphs.
-#
-
-# These are the files which need to be marked executable
-my @executable_files = ('whineatnews.pl', 'collectstats.pl',
-   'checksetup.pl', 'importxml.pl', 'runtests.pl', 'testserver.pl',
-   'whine.pl', 'customfield.pl');
-
-# tell me if a file is executable.  All CGI files and those in @executable_files
-# are executable
-sub isExecutableFile {
-    my ($file) = @_;
-    if ($file =~ /\.cgi/) {
-        return 1;
-    }
-
-    my $exec_file;
-    foreach $exec_file (@executable_files) {
-        if ($file eq $exec_file) {
-            return 1;
-        }
-    }
-    return undef;
-}
-
-# fix file (or files - wildcards ok) permissions 
-sub fixPerms {
-    my ($file_pattern, $owner, $group, $umask, $do_dirs) = @_;
-    my @files = glob($file_pattern);
-    my $execperm = 0777 & ~ $umask;
-    my $normperm = 0666 & ~ $umask;
-    foreach my $file (@files) {
-        next if (!-e $file);
-        # do not change permissions on directories here unless $do_dirs is set
-        if (!(-d $file)) {
-            chown $owner, $group, $file;
-            # check if the file is executable.
-            if (isExecutableFile($file)) {
-                #printf ("Changing $file to %o\n", $execperm);
-                chmod $execperm, $file;
-            } else {
-                #printf ("Changing $file to %o\n", $normperm);
-                chmod $normperm, $file;
-            }
-        }
-        elsif ($do_dirs) {
-            chown $owner, $group, $file;
-            if ($file =~ /CVS$/) {
-                chmod 0700, $file;
-            }
-            else {
-                #printf ("Changing $file to %o\n", $execperm);
-                chmod $execperm, $file;
-                fixPerms("$file/.htaccess", $owner, $group, $umask, $do_dirs);
-                # do the contents of the directory
-                fixPerms("$file/*", $owner, $group, $umask, $do_dirs); 
-            }
-        }
-    }
-}
-
-if ($^O !~ /MSWin32/i) {
-    my $templatedir = bz_locations()->{'templatedir'};
-    if ($my_webservergroup) {
-        # Funny! getgrname returns the GID if fed with NAME ...
-        my $webservergid = getgrnam($my_webservergroup) 
-        or die("no such group: $my_webservergroup");
-        # chown needs to be called with a valid uid, not 0.  $< returns the
-        # caller's uid.  Maybe there should be a $bugzillauid, and call 
-        # with that userid.
-        fixPerms('.htaccess', $<, $webservergid, 027); # glob('*') doesn't catch dotfiles
-        fixPerms("$datadir/.htaccess", $<, $webservergid, 027);
-        fixPerms("$datadir/duplicates", $<, $webservergid, 027, 1);
-        fixPerms("$datadir/mining", $<, $webservergid, 027, 1);
-        fixPerms("$datadir/template", $<, $webservergid, 007, 1); # webserver will write to these
-        # webserver will write to attachdir.
-        fixPerms(bz_locations()->{'attachdir'}, $<, $webservergid, 007, 1);
-        fixPerms($webdotdir, $<, $webservergid, 007, 1);
-        fixPerms("$webdotdir/.htaccess", $<, $webservergid, 027);
-        fixPerms("$datadir/params", $<, $webservergid, 017);
-        # The web server must be the owner of bugzilla-update.xml.
-        fixPerms("$datadir/bugzilla-update.xml", $webservergid, $webservergid, 017);
-        fixPerms('*', $<, $webservergid, 027);
-        fixPerms('Bugzilla', $<, $webservergid, 027, 1);
-        fixPerms($templatedir, $<, $webservergid, 027, 1);
-        fixPerms('images', $<, $webservergid, 027, 1);
-        fixPerms('css', $<, $webservergid, 027, 1);
-        fixPerms('skins', $<, $webservergid, 027, 1);
-        fixPerms('js', $<, $webservergid, 027, 1);
-
-        # Don't use fixPerms here, because it won't change perms 
-        # on the directory unless it's using recursion
-        chown $<, $webservergid, $datadir;
-        chmod 0771, $datadir;
-        chown $<, $webservergid, 'graphs';
-        chmod 0770, 'graphs';
-    } else {
-        # get current gid from $( list
-        my $gid = (split " ", $()[0];
-        fixPerms('.htaccess', $<, $gid, 022); # glob('*') doesn't catch dotfiles
-        fixPerms("$datadir/.htaccess", $<, $gid, 022);
-        fixPerms("$datadir/duplicates", $<, $gid, 022, 1);
-        fixPerms("$datadir/mining", $<, $gid, 022, 1);
-        fixPerms("$datadir/template", $<, $gid, 000, 1); # webserver will write to these
-        fixPerms($webdotdir, $<, $gid, 000, 1);
-        chmod 01777, $webdotdir;
-        fixPerms("$webdotdir/.htaccess", $<, $gid, 022);
-        fixPerms("$datadir/params", $<, $gid, 011);
-        fixPerms("$datadir/bugzilla-update.xml", $gid, $gid, 011);
-        fixPerms('*', $<, $gid, 022);
-        fixPerms('Bugzilla', $<, $gid, 022, 1);
-        fixPerms($templatedir, $<, $gid, 022, 1);
-        fixPerms('images', $<, $gid, 022, 1);
-        fixPerms('css', $<, $gid, 022, 1);
-        fixPerms('skins', $<, $gid, 022, 1);
-        fixPerms('js', $<, $gid, 022, 1);
-        
-        # Don't use fixPerms here, because it won't change perms
-        # on the directory unless it's using recursion
-        chown $<, $gid, $datadir;
-        chmod 0777, $datadir;
-        chown $<, $gid, 'graphs';
-        chmod 01777, 'graphs';
-    }
-}
+fix_all_file_permissions(!$silent);
 
 ###########################################################################
 # Check GraphViz setup
@@ -562,9 +408,9 @@ if( Bugzilla->params->{'webdotbase'} && Bugzilla->params->{'webdotbase'} !~ /^ht
         }
         close HTACCESS;
     }
+    print "\n" unless $silent;
 }
 
-print "\n" unless $silent;
 
 ###########################################################################
 # Populate groups table