configure.pl: comments that delimit Conf settings must now start in
[BackupPC.git] / cgi-bin / BackupPC_Admin
index 419ccb9..99cc594 100755 (executable)
@@ -1,4 +1,4 @@
-#!/usr/bin/perl -T
+#!/bin/perl -T
 #============================================================= -*-perl-*-w
 #
 # BackupPC_Admin: Apache/CGI interface for BackupPC.
@@ -14,7 +14,8 @@
 #   user name.
 #
 #   Also, this script needs to run as the BackupPC user.  To accomplish
-#   this the script is typically installed as setuid to the BackupPC user.
+#   this the script is typically installed as setuid to the BackupPC user,
+#   or it can run under mod_perl with httpd running as the BackupPC user.
 #
 # AUTHOR
 #   Craig Barratt  <cbarratt@users.sourceforge.net>
@@ -38,7 +39,7 @@
 #
 #========================================================================
 #
-# Version 1.5.0beta0, released 30 Jun 2002.
+# Version 1.6.0_CVS, released 10 Dec 2002.
 #
 # See http://backuppc.sourceforge.net.
 #
@@ -50,6 +51,7 @@ use lib "/usr/local/BackupPC/lib";
 use BackupPC::Lib;
 use BackupPC::FileZIO;
 use BackupPC::Attrib qw(:all);
+use BackupPC::View;
 use Data::Dumper;
 
 use vars qw($Cgi %In $MyURL $User %Conf $TopDir $BinDir $bpc);
@@ -124,7 +126,7 @@ my %ActionDispatch = (
     "browse"                    => \&Action_Browse,
     $Lang->{Restore}            => \&Action_Restore,
     "RestoreFile"               => \&Action_RestoreFile,
-    $Lang->{hostInfo}           => \&Action_HostInfo,
+    "hostInfo"                  => \&Action_HostInfo,
     "generalInfo"               => \&Action_GeneralInfo,
     "restoreInfo"               => \&Action_RestoreInfo,
 );
@@ -132,7 +134,7 @@ my %ActionDispatch = (
 #
 # Set default actions, then call sub handler
 #
-$In{action} ||= $Lang->{hostInfo}    if ( defined($In{host}) );
+$In{action} ||= "hostInfo"    if ( defined($In{host}) );
 $In{action}   = "generalInfo" if ( !defined($ActionDispatch{$In{action}}) );
 $ActionDispatch{$In{action}}();
 exit(0);
@@ -201,8 +203,8 @@ sub Action_Summary
     <td align="center"> $fullRate </td>
     <td align="center"> $incrCnt </td>
     <td align="center"> $incrAge </td>
-    <td align="center"> $Status{$host}{state} </td>
-    <td> $Status{$host}{reason} </td></tr>
+    <td align="center"> $Lang->{$Status{$host}{state}} </td>
+    <td> $Lang->{$Status{$host}{reason}} </td></tr>
 EOF
         if ( @Backups == 0 ) {
             $hostCntNone++;
@@ -241,14 +243,15 @@ sub Action_StartStopBackup
     if ( $In{doit} ) {
         if ( $start ) {
            if ( $Hosts->{$host}{dhcp} ) {
-               $reply = $bpc->ServerMesg(eval("qq{$Lang->{backup__In_hostIP___host}}"));
+               $reply = $bpc->ServerMesg("backup $In{hostIP} $host"
+                                       . " $User $doFull");
                $str = eval("qq{$Lang->{Backup_requested_on_DHCP__host}}");
            } else {
-               $reply = $bpc->ServerMesg(eval("qq{$Lang->{backup__host__host__User__doFull}}"));
+               $reply = $bpc->ServerMesg("backup $host $host $User $doFull");
                $str = eval("qq{$Lang->{Backup_requested_on__host_by__User}}");
            }
         } else {
-            $reply = $bpc->ServerMesg(eval("qq{$Lang->{stop__host__User__In_backoff}}"));
+            $reply = $bpc->ServerMesg("stop $host $User $In{backoff}");
             $str = eval("qq{$Lang->{Backup_stopped_dequeued_on__host_by__User}}");
         }
 
@@ -260,8 +263,8 @@ sub Action_StartStopBackup
         if ( $start ) {
            my $ipAddr = ConfirmIPAddress($host);
 
-            Header($Lang->{BackupPC__Start_Backup_Confirm_on__host});
-            print (eval ("qq{$Lang->{Are_you_sure_start}}"));
+            Header(eval("qq{$Lang->{BackupPC__Start_Backup_Confirm_on__host}}"));
+            print (eval("qq{$Lang->{Are_you_sure_start}}"));
         } else {
             my $backoff = "";
             GetStatusInfo("host($host)");
@@ -357,6 +360,9 @@ sub Action_View
         $comment = "(Extracting only Errors)";
     } elsif ( $host ne "" && $type eq "config" ) {
         $file = "$TopDir/pc/$host/config.pl";
+        $file = "$TopDir/conf/$host.pl"
+                    if ( $host ne "config" && -f "$TopDir/conf/$host.pl"
+                                           && !-f $file );
     } elsif ( $type eq "docs" ) {
         $file = "$BinDir/../doc/BackupPC.html";
         if ( open(LOG, $file) ) {
@@ -399,7 +405,8 @@ sub Action_View
             while ( 1 ) {
                 $_ = $fh->readLine();
                 if ( $_ eq "" ) {
-                   print(eval ("qq{$Lang->{skipped__skipped_lines}}"));
+                   print(eval ("qq{$Lang->{skipped__skipped_lines}}"))
+                                                   if ( $skipped );
                    last;
                }
                 if ( /smb: \\>/
@@ -417,11 +424,13 @@ sub Action_View
                         || /^\s+directory \\/
                         || /^Timezone is/
                         || /^\.\//
+                        || /^  /
                            ) {
                    $skipped++;
                    next;
                }
-               print(eval("qq{$Lang->{skipped__skipped_lines}}")) if ( $skipped );
+               print(eval("qq{$Lang->{skipped__skipped_lines}}"))
+                                                    if ( $skipped );
                $skipped = 0;
                 print ${EscapeHTML($_)};
             }
@@ -532,148 +541,91 @@ EOF
 sub Action_Browse
 {
     my $Privileged = CheckPermission($In{host});
-    my($i, $dirStr, $fileStr, $mangle);
-    my($numF, $compressF, $mangleF, $fullDirF);
-    my $checkBoxCnt = 0;      # checkbox counter
+    my($i, $dirStr, $fileStr, $attr);
+    my $checkBoxCnt = 0;
 
     if ( !$Privileged ) {
         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_browse_backup_files}}"));
     }
-    my $host = $In{host};
-    my $num  = $In{num};
-    my $dir  = $In{dir};
-    if ( $host eq "" ) {
-        ErrorExit($Lang->{Empty_host_name});
-    }
+    my $host  = $In{host};
+    my $num   = $In{num};
+    my $share = $In{share};
+    my $dir   = $In{dir};
+
+    ErrorExit($Lang->{Empty_host_name}) if ( $host eq "" );
     #
     # Find the requested backup and the previous filled backup
     #
     my @Backups = $bpc->BackupInfoRead($host);
     for ( $i = 0 ; $i < @Backups ; $i++ ) {
-        if ( !$Backups[$i]{noFill} ) {
-            $numF      = $Backups[$i]{num};
-            $mangleF   = $Backups[$i]{mangle};
-            $compressF = $Backups[$i]{compress};
-        }
         last if ( $Backups[$i]{num} == $num );
     }
     if ( $i >= @Backups ) {
         ErrorExit("Backup number $num for host ${EscapeHTML($host)} does"
                . " not exist.");
     }
-    if ( !$Backups[$i]{noFill} ) {
-        # no need to back-fill a filled backup
-        $numF = $mangleF = $compressF = undef;
-    }
     my $backupTime = timeStamp2($Backups[$i]{startTime});
     my $backupAge = sprintf("%.1f", (time - $Backups[$i]{startTime})
                                     / (24 * 3600));
-    $mangle = $Backups[$i]{mangle};
+    my $view = BackupPC::View->new($bpc, $host, \@Backups);
+
     if ( $dir eq "" || $dir eq "." || $dir eq ".." ) {
-        if ( !opendir(DIR, "$TopDir/pc/$host/$num") ) {
-            ErrorExit(eval("qq{$Lang->{Can_t_browse_bad_directory_name}}"));
-        }
-        #
-        # Read this directory and find the first directory
-        #
-       foreach my $f ( readdir(DIR) ) {
-           next if ( $f eq "." || $f eq ".." );
-           if ( -d "$TopDir/pc/$host/$num/$f" ) {
-               $dir = "/$f";
-               last;
-           }
-       }
-        closedir(DIR);
-       if ( $dir eq "" || $dir eq "." || $dir eq ".." ) {
-            ErrorExit(eval("qq{$Lang->{Directory___EscapeHTML}}"));
-       }
-    }
-    my $relDir   = $dir;
-    my $fullDir  = "$TopDir/pc/$host/$num/$relDir";
-    if ( defined($numF) ) {
-       # get full path to filled backup
-       if ( $mangle && !$mangleF ) {
-           $fullDirF = "$TopDir/pc/$host/$numF/"
-                               . $bpc->fileNameUnmangle($relDir);
+       $attr = $view->dirAttrib($num, "", "");
+       if ( keys(%$attr) > 0 ) {
+           $share = (sort(keys(%$attr)))[0];
+           $dir   = '/';
        } else {
-           $fullDirF = "$TopDir/pc/$host/$numF/$relDir";
+            ErrorExit(eval("qq{$Lang->{Directory___EscapeHTML}}"));
        }
     }
-    my $currDir    = undef;
-    #
-    # Read attributes for the directory and optionally for the filled backup
-    #
-    my $attr  = BackupPC::Attrib->new({ compress => $Backups[$i]{compress}});
-    my $attrF = BackupPC::Attrib->new({ compress => $compressF})
-                                                    if ( defined($numF) );
-    $attr->read($fullDir) if ( -f $attr->fileName($fullDir) );
-    if ( defined($numF) && -f $attrF->fileName($fullDirF)
-                        && $attrF->read($fullDirF) ) {
-        $attr->merge($attrF);
-    }
+    my $relDir  = $dir;
+    my $currDir = undef;
+
     #
     # Loop up the directory tree until we hit the top.
     #
     my(@DirStrPrev);
     while ( 1 ) {
-        my($fLast, $fum, $fLastum, @DirStr);
+        my($fLast, $fLastum, @DirStr);
 
-        if ( $fullDir =~ m{(^|/)\.\.(/|$)} || !opendir(DIR, $fullDir) ) {
+       $attr = $view->dirAttrib($num, $share, $relDir);
+        if ( !defined($attr) ) {
             ErrorExit(eval("qq{$Lang->{Can_t_browse_bad_directory_name2}}"));
         }
-        #
-        # Read this directory and optionally the corresponding filled directory
-        #
-        my @Dir = readdir(DIR);
-        closedir(DIR);
-        if ( defined($numF) && opendir(DIR, $fullDirF) ) {
-            if ( $mangle == $mangleF ) {
-                @Dir = (@Dir, readdir(DIR));
-            } else {
-                foreach my $f ( readdir(DIR) ) {
-                   next if ( $f eq "." || $f eq ".." );
-                   push(@Dir, $bpc->fileNameMangle($f));
-                }
-            }
-            closedir(DIR);
-        }
+
         my $fileCnt = 0;          # file counter
         $fLast = $dirStr = "";
+
         #
         # Loop over each of the files in this directory
         #
-       my(@DirUniq);
-        foreach my $f ( sort({uc($a) cmp uc($b)} @Dir) ) {
-            next if ( $f eq "." || $f eq ".."
-                   || $f eq $fLast || ($mangle && $f eq "attrib") );
-            $fLast = $f;
-           push(@DirUniq, $f);
-       }
-       while ( defined(my $f = shift(@DirUniq)) ) {
-            my $path = "$relDir/$f";
+       foreach my $f ( sort(keys(%$attr)) ) {
             my($dirOpen, $gotDir, $imgStr, $img);
-            $fum = $mangle ? $bpc->fileNameUnmangle($f) : $f;  # unmangled $f
-            my $fumURI = $fum;                                 # URI escaped $f
+            my $fURI = $f;                             # URI escaped $f
+            my $shareURI = $share;                     # URI escaped $share
+            my $path = "$relDir/$f";
+           if ( $shareURI eq "" ) {
+               $shareURI = $path;
+               $path  = "";
+           }
             $path =~ s{^/+}{/};
-            $path =~ s/([^\w.\/-])/uc sprintf("%%%02x", ord($1))/eg;
-            $fumURI =~ s/([^\w.\/-])/uc sprintf("%%%02x", ord($1))/eg;
-            $dirOpen = 1 if ( defined($currDir) && $f eq $currDir );
-            if ( -d "$fullDir/$f" ) {
+            $path     =~ s/([^\w.\/-])/uc sprintf("%%%02x", ord($1))/eg;
+            $fURI     =~ s/([^\w.\/-])/uc sprintf("%%%02x", ord($1))/eg;
+            $shareURI =~ s/([^\w.\/-])/uc sprintf("%%%02x", ord($1))/eg;
+            $dirOpen  = 1 if ( defined($currDir) && $f eq $currDir );
+            if ( $attr->{$f}{type} == BPC_FTYPE_DIR ) {
                 #
                 # Display directory if it exists in current backup.
                 # First find out if there are subdirs
                 #
-                my @s = (defined($numF) && -d "$fullDirF/$f")
-                            ? stat("$fullDirF/$f")
-                            : stat("$fullDir/$f");
                my($bold, $unbold, $BGcolor);
                $img |= 1 << 6;
-               $img |= 1 << 5 if ( $s[3] > 2 );
+               $img |= 1 << 5 if ( $attr->{$f}{nlink} > 2 );
                if ( $dirOpen ) {
                    $bold = "<b>";
                    $unbold = "</b>";
                    $img |= 1 << 2;
-                   $img |= 1 << 3 if ( $s[3] > 2 );
+                   $img |= 1 << 3 if ( $attr->{$f}{nlink} > 2 );
                }
                my $imgFileName = sprintf("%07b.gif", $img);
                $imgStr = "<img src=\"$Conf{CgiImageDirURL}/$imgFileName\" align=\"absmiddle\" width=\"9\" height=\"19\" border=\"0\">";
@@ -682,12 +634,12 @@ sub Action_Browse
                } else {
                    $BGcolor = "";
                }
-               my $dirName = $fum;
+               my $dirName = $f;
                $dirName =~ s/ /&nbsp;/g;
                push(@DirStr, {needTick => 1,
                               tdArgs   => $BGcolor,
                               link     => <<EOF});
-<a href="$MyURL?action=browse&host=$host&num=$num&dir=$path">$imgStr</a><a href="$MyURL?action=browse&host=$host&num=$num&dir=$path" style="font-size:13px;font-family:arial;text-decoration:none;line-height:15px">&nbsp;$bold$dirName$unbold</a></td></tr>
+<a href="$MyURL?action=browse&host=$host&num=$num&share=$shareURI&dir=$path">$imgStr</a><a href="$MyURL?action=browse&host=$host&num=$num&share=$shareURI&dir=$path" style="font-size:13px;font-family:arial;text-decoration:none;line-height:15px">&nbsp;$bold$dirName$unbold</a></td></tr>
 EOF
                 $fileCnt++;
                 $gotDir = 1;
@@ -722,29 +674,32 @@ EOF
                 # This is the selected directory, so display all the files
                 #
                 my $attrStr;
-                if ( defined($a = $attr->get($fum)) ) {
+                if ( defined($a = $attr->{$f}) ) {
                     my $mtimeStr = $bpc->timeStamp($a->{mtime});
-                    my $typeStr  = $attr->fileType2Text($a->{type});
+                   # UGH -> fix this
+                    my $typeStr  = BackupPC::Attrib::fileType2Text(undef,
+                                                                  $a->{type});
                     my $modeStr  = sprintf("0%o", $a->{mode} & 07777);
                     $attrStr .= <<EOF;
     <td align="center">$typeStr</td>
-    <td align="right">$modeStr</td>
+    <td align="center">$modeStr</td>
+    <td align="center">$a->{backupNum}</td>
     <td align="right">$a->{size}</td>
     <td align="right">$mtimeStr</td>
 </tr>
 EOF
                 } else {
-                    $attrStr .= "<td colspan=\"4\" align=\"center\"> </td>\n";
+                    $attrStr .= "<td colspan=\"5\" align=\"center\"> </td>\n";
                 }
                 if ( $gotDir ) {
                     $fileStr .= <<EOF;
-<tr bgcolor="#ffffcc"><td><input type="checkbox" name="fcb$checkBoxCnt" value="$path">&nbsp;<a href="$MyURL?action=browse&host=$host&num=$num&dir=$path">${EscapeHTML($fum)}</a></td>
+<tr bgcolor="#ffffcc"><td><input type="checkbox" name="fcb$checkBoxCnt" value="$path">&nbsp;<a href="$MyURL?action=browse&host=$host&num=$num&share=$shareURI&dir=$path">${EscapeHTML($f)}</a></td>
 $attrStr
 </tr>
 EOF
                 } else {
                     $fileStr .= <<EOF;
-<tr bgcolor="#ffffcc"><td><input type="checkbox" name="fcb$checkBoxCnt" value="$path">&nbsp;<a href="$MyURL?action=RestoreFile&host=$host&num=$num&dir=$path">${EscapeHTML($fum)}</a></td>
+<tr bgcolor="#ffffcc"><td><input type="checkbox" name="fcb$checkBoxCnt" value="$path">&nbsp;<a href="$MyURL?action=RestoreFile&host=$host&num=$num&share=$shareURI&dir=$path">${EscapeHTML($f)}</a></td>
 $attrStr
 </tr>
 EOF
@@ -753,19 +708,28 @@ EOF
             }
         }
        @DirStrPrev = @DirStr;
-        last if ( $relDir eq "" );
+        last if ( $relDir eq "" && $share eq "" );
         # 
-        # Prune the last directory off $relDir
+        # Prune the last directory off $relDir, or at the very end
+       # do the top-level directory.
         #
-        $relDir =~ s/(.*)\/(.*)/$1/;
-        $currDir = $2;
-        $fullDir = "$TopDir/pc/$host/$num/$relDir";
-        $fullDirF = "$TopDir/pc/$host/$numF/$relDir" if ( defined($numF) );
+       if ( $relDir eq "" ) {
+           $currDir = $share;
+           $share = "";
+       } else {
+           $relDir =~ s/(.*)\/(.*)/$1/;
+           $currDir = $2;
+       }
     }
-    my $dirDisplay = $mangle ? $bpc->fileNameUnmangle($dir) : $dir;
-    $dirDisplay =~ s{//}{/}g;
+    $share = $currDir;
+    my $dirDisplay = "$share/$dir";
+    $dirDisplay =~ s{//+}{/}g;
+    $dirDisplay =~ s{/+$}{}g;
     my $filledBackup;
-    if ( defined($numF) ) {
+
+    if ( (my @mergeNums = @{$view->mergeNums}) > 1 ) {
+       shift(@mergeNums);
+       my $numF = join(", #", @mergeNums);
         $filledBackup = eval("qq{$Lang->{This_display_is_merged_with_backup}}");
     }
     Header(eval("qq{$Lang->{Browse_backup__num_for__host}}"));
@@ -789,6 +753,20 @@ EOF
     } else {
        $fileStr = eval("qq{$Lang->{The_directory_is_empty}}");
     }
+    my @otherDirs;
+    foreach my $i ( $view->backupList($share, $dir) ) {
+        my $path = $dir;
+        my $shareURI = $share;
+        $path =~ s/([^\w.\/-])/uc sprintf("%%%02x", ord($1))/eg;
+        $shareURI =~ s/([^\w.\/-])/uc sprintf("%%%02x", ord($1))/eg;
+        push(@otherDirs, "<a href=\"$MyURL?action=browse&host=$host&num=$i"
+                       . "&share=$shareURI&dir=$path\">$i</a>");
+
+    }
+    if ( @otherDirs ) {
+       my $otherDirs  = join(",\n", @otherDirs);
+        $filledBackup .= eval("qq{$Lang->{Visit_this_directory_in_backup}}");
+    }
  
     print (eval("qq{$Lang->{Backup_browse_for__host}}"));
     Trailer();
@@ -796,21 +774,18 @@ EOF
 
 sub Action_Restore
 {
-    my($str, $reply, $i);
+    my($str, $reply);
     my $Privileged = CheckPermission($In{host});
     if ( !$Privileged ) {
         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_restore_backup_files}}"));
     }
-    my $host = $In{host};
-    my $num  = $In{num};
-    my(@fileList, $fileListStr, $hiddenStr, $share, $pathHdr, $badFileCnt);
+    my $host  = $In{host};
+    my $num   = $In{num};
+    my $share = $In{share};
+    my(@fileList, $fileListStr, $hiddenStr, $pathHdr, $badFileCnt);
     my @Backups = $bpc->BackupInfoRead($host);
-    for ( $i = 0 ; $i < @Backups ; $i++ ) {
-        last if ( $Backups[$i]{num} == $num );
-    }
-    my $mangle = $Backups[$i]{mangle};
-    ServerConnect();
 
+    ServerConnect();
     if ( !defined($Hosts->{$host}) ) {
         ErrorExit(eval("qq{$Lang->{Bad_host_name}}"));
     }
@@ -818,19 +793,14 @@ sub Action_Restore
         next if ( !defined($In{"fcb$i"}) );
         (my $name = $In{"fcb$i"}) =~ s/%([0-9A-F]{2})/chr(hex($1))/eg;
         $badFileCnt++ if ( $name =~ m{(^|/)\.\.(/|$)} );
-        if ( $name =~ m{^/+(.*?)(/.*)} ) {
-            $share = $1;
-            $name  = $mangle ? $bpc->fileNameUnmangle($2) : $2;
-            if ( @fileList == 0 ) {
-                $pathHdr = $name;
-            } else {
-                while ( substr($name, 0, length($pathHdr)) ne $pathHdr ) {
-                    $pathHdr = substr($pathHdr, 0, rindex($pathHdr, "/"));
-                }
-            }
-        }
+       if ( @fileList == 0 ) {
+           $pathHdr = $name;
+       } else {
+           while ( substr($name, 0, length($pathHdr)) ne $pathHdr ) {
+               $pathHdr = substr($pathHdr, 0, rindex($pathHdr, "/"));
+           }
+       }
         push(@fileList, $name);
-        $share = $mangle ? $bpc->fileNameUnmangle($share) : $share;
         $hiddenStr .= <<EOF;
 <input type="hidden" name="fcb$i" value="$In{'fcb' . $i}">
 EOF
@@ -839,6 +809,7 @@ EOF
 EOF
     }
     $hiddenStr .= "<input type=\"hidden\" name=\"fcbMax\" value=\"$In{fcbMax}\">\n";
+    $hiddenStr .= "<input type=\"hidden\" name=\"share\" value=\"${EscapeHTML($share)}\">\n";
     $badFileCnt++ if ( $In{pathHdr} =~ m{(^|/)\.\.(/|$)} );
     $badFileCnt++ if ( $In{num} =~ m{(^|/)\.\.(/|$)} );
     if ( @fileList == 0 ) {
@@ -879,76 +850,71 @@ EOF
     } elsif ( $In{type} == 1 ) {
         #
         # Provide the selected files via a tar archive.
+       #
+       # We no longer use fork/exec (as in v1.5.0) since some mod_perls
+       # do not correctly preserve the stdout connection to the client
+       # browser, so we execute BackupPC_tarCreate in-line.
         #
-        $SIG{CHLD} = 'IGNORE';
-        my $pid = fork();
-        if ( !defined($pid) ) {
-            $bpc->ServerMesg(eval("qq{$Lang->{log_Can_t_fork_for_tar_restore_request_by__User}}"));
-            ErrorExit($Lang->{Can_t_fork_for_tar_restore});
-        }
-        if ( $pid ) {
-            #
-            # This is the parent.
-            #
-            my @fileListTrim = @fileList;
-            if ( @fileListTrim > 10 ) {
-                @fileListTrim = (@fileListTrim[0..9], '...');
-            }
-            $bpc->ServerMesg(eval("qq{$Lang->{log_User__User_downloaded_tar_archive_for__host}}"));
-            return;
-        }
-        #
-        # This is the child.  Print the headers and run BackupPC_tarCreate.
-        #
+       my @fileListTrim = @fileList;
+       if ( @fileListTrim > 10 ) {
+           @fileListTrim = (@fileListTrim[0..9], '...');
+       }
+       $bpc->ServerMesg(eval("qq{$Lang->{log_User__User_downloaded_tar_archive_for__host}}"));
+
         my @pathOpts;
         if ( $In{relative} ) {
             @pathOpts = ("-r", $pathHdr, "-p", "");
         }
-        $bpc->ServerDisconnect();
-        print "Content-Type: application/x-gtar\n";
-        print "Content-Transfer-Encoding: binary\n";
-        print "Content-Disposition: attachment; filename=\"restore.tar\"\n\n";
-        exec("$BinDir/BackupPC_tarCreate",
+       #
+       # We use syswrite since BackupPC_tarCreate uses syswrite too.
+       # Need to test this with mod_perl: PaulL says it doesn't work.
+       #
+       syswrite(STDOUT, <<EOF);
+Content-Type: application/x-gtar
+Content-Transfer-Encoding: binary
+Content-Disposition: attachment; filename=\"restore.tar\"
+
+EOF
+       local(@ARGV);
+       @ARGV = (
              "-h", $host,
              "-n", $num,
              "-s", $share,
              @pathOpts,
              @fileList
-        );
+       );
+       do "$BinDir/BackupPC_tarCreate";
     } elsif ( $In{type} == 2 ) {
         #
         # Provide the selected files via a zip archive.
+       #
+       # We no longer use fork/exec (as in v1.5.0) since some mod_perls
+       # do not correctly preserve the stdout connection to the client
+       # browser, so we execute BackupPC_tarCreate in-line.
         #
-        $SIG{CHLD} = 'IGNORE';
-        my $pid = fork();
-        if ( !defined($pid) ) {
-            $bpc->ServerMesg(eval("qq{$Lang->{log_Can_t_fork_for_zip_restore_request_by__User}}"));
-            ErrorExit($Lang->{Can_t_fork_for_zip_restore});
-        }
-        if ( $pid ) {
-            #
-            # This is the parent.
-            #
-            my @fileListTrim = @fileList;
-            if ( @fileListTrim > 10 ) {
-                @fileListTrim = (@fileListTrim[0..9], '...');
-            }
-            $bpc->ServerMesg(eval("qq{$Lang->{log_User__User_downloaded_zip_archive_for__host}}"));
-            return;
-        }
-        #
-        # This is the child.  Print the headers and run BackupPC_tarCreate.
-        #
+       my @fileListTrim = @fileList;
+       if ( @fileListTrim > 10 ) {
+           @fileListTrim = (@fileListTrim[0..9], '...');
+       }
+       $bpc->ServerMesg(eval("qq{$Lang->{log_User__User_downloaded_zip_archive_for__host}}"));
+
         my @pathOpts;
         if ( $In{relative} ) {
             @pathOpts = ("-r", $pathHdr, "-p", "");
         }
-        $bpc->ServerDisconnect();
-        print "Content-Type: application/zip\n";
-        print "Content-Transfer-Encoding: binary\n";
-        print "Content-Disposition: attachment; filename=\"restore.zip\"\n\n";
+       #
+       # We use syswrite since BackupPC_tarCreate uses syswrite too.
+       # Need to test this with mod_perl: PaulL says it doesn't work.
+       #
+       syswrite(STDOUT, <<EOF);
+Content-Type: application/zip
+Content-Transfer-Encoding: binary
+Content-Disposition: attachment; filename=\"restore.zip\"
+
+EOF
        $In{compressLevel} = 5 if ( $In{compressLevel} !~ /^\d+$/ );
-        exec("$BinDir/BackupPC_zipCreate",
+       local(@ARGV);
+       @ARGV = (
              "-h", $host,
              "-n", $num,
              "-c", $In{compressLevel},
@@ -956,6 +922,7 @@ EOF
              @pathOpts,
              @fileList
         );
+        do "$BinDir/BackupPC_zipCreate";
     } elsif ( $In{type} == 3 ) {
         #
         # Do restore directly onto host
@@ -981,7 +948,7 @@ EOF
         Trailer();
     } elsif ( $In{type} == 4 ) {
        if ( !defined($Hosts->{$In{hostDest}}) ) {
-           ErrorExit(eval("qq{$Lang->{Host_doesn_t_exist}}"));
+           ErrorExit(eval("qq{$Lang->{Host__doesn_t_exist}}"));
        }
        if ( !CheckPermission($In{hostDest}) ) {
            ErrorExit(eval("qq{$Lang->{You_don_t_have_permission_to_restore_onto_host}}"));
@@ -1028,7 +995,8 @@ EOF
         } else {
             ErrorExit(eval("qq{$Lang->{Can_t_open_create}}"));
         }
-       $reply = $bpc->ServerMesg(eval("qq{$Lang->{restore__ipAddr}}"));
+       $reply = $bpc->ServerMesg("restore $ipAddr"
+                       . " $hostDest $User $reqFileName");
        $str = eval("qq{$Lang->{Restore_requested_to_host__hostDest__backup___num}}");
         Header(eval("qq{$Lang->{Restore_Requested_on__hostDest}}"));
        print (eval("qq{$Lang->{Reply_from_server_was___reply}}"));
@@ -1038,14 +1006,14 @@ EOF
 
 sub Action_RestoreFile
 {
-    restoreFile($In{host}, $In{num}, $In{dir});
+    restoreFile($In{host}, $In{num}, $In{share}, $In{dir});
 }
 
 sub restoreFile
 {
-    my($host, $num, $dir, $skipHardLink, $origName) = @_;
+    my($host, $num, $share, $dir, $skipHardLink, $origName) = @_;
     my($Privileged) = CheckPermission($host);
-    my($i, $numF, $mangleF, $compressF, $mangle, $compress, $dirUM);
+
     #
     # Some common content (media) types from www.iana.org (via MIME::Types).
     #
@@ -1131,59 +1099,22 @@ sub restoreFile
        'xwd'  => 'image/x-xwindowdump',
        'z'    => 'application/x-compress',
        'zip'  => 'application/zip',
+        %{$Conf{CgiExt2ContentType}},       # add site-specific values
     };
-
     if ( !$Privileged ) {
         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_restore_backup_files2}}"));
     }
     ServerConnect();
-    my @Backups = $bpc->BackupInfoRead($host);
-    if ( $host eq "" ) {
-        ErrorExit($Lang->{Empty_host_name});
-    }
+    ErrorExit($Lang->{Empty_host_name}) if ( $host eq "" );
+
     $dir = "/" if ( $dir eq "" );
-    for ( $i = 0 ; $i < @Backups ; $i++ ) {
-        if ( !$Backups[$i]{noFill} ) {
-            $numF      = $Backups[$i]{num};
-            $mangleF   = $Backups[$i]{mangle};
-            $compressF = $Backups[$i]{compress};
-        }
-        last if ( $Backups[$i]{num} == $num );
-    }
-    $mangle = $Backups[$i]{mangle};
-    $compress = $Backups[$i]{compress};
-    if ( !$Backups[$i]{noFill} ) {
-        # no need to back-fill a filled backup
-        $numF = $mangleF = $compressF = undef;
-    }
-    my $fullPath = "$TopDir/pc/$host/$num/$dir";
-    $fullPath =~ s{/+}{/}g;
-    if ( !-f $fullPath && defined($numF) ) {
-       my $dirF = $dir;
-       my $fullPathF;
-       if ( $mangle && !$mangleF ) {
-           $fullPathF = "$TopDir/pc/$host/$numF/"
-                               . $bpc->fileNameUnmangle($dir);
-       } else {
-           $fullPathF = "$TopDir/pc/$host/$numF/$dir";
-       }
-       if ( -f $fullPathF ) {
-           $fullPath = $fullPathF;
-           $compress = $compressF;
-       }
-    }
-    if ( $fullPath =~ m{(^|/)\.\.(/|$)} || !-f $fullPath ) {
-        ErrorExit(eval("qq{$Lang->{Can_t_restore_bad_file}}"));
+    my @Backups = $bpc->BackupInfoRead($host);
+    my $view = BackupPC::View->new($bpc, $host, \@Backups);
+    my $a = $view->fileAttrib($num, $share, $dir);
+    if ( $dir =~ m{(^|/)\.\.(/|$)} || !defined($a) ) {
+        ErrorExit("Can't restore bad file ${EscapeHTML($dir)}");
     }
-    my $dirUM = $mangle ? $bpc->fileNameUnmangle($dir) : $dir;
-    my $attr = BackupPC::Attrib->new({compress => $compress});
-    my $fullDir = $fullPath;
-    $fullDir =~ s{(.*)/.*}{$1};
-    my $fileName = $1 if ( $dirUM =~ /.*\/(.*)/ );
-    $attr->read($fullDir) if ( -f $attr->fileName($fullDir) );
-    my $a = $attr->get($fileName);
-
-    my $f = BackupPC::FileZIO->open($fullPath, 0, $compress);
+    my $f = BackupPC::FileZIO->open($a->{fullPath}, 0, $a->{compress});
     my $data;
     if ( !$skipHardLink && $a->{type} == BPC_FTYPE_HARDLINK ) {
        #
@@ -1196,20 +1127,15 @@ sub restoreFile
        $f->close;
        $linkName =~ s/^\.\///;
        my $share = $1 if ( $dir =~ /^\/?(.*?)\// );
-       restoreFile($host, $num,
-               "$share/" . ($mangle ? $bpc->fileNameMangle($linkName)
-                                    : $linkName), 1, $dir);
+       restoreFile($host, $num, $share, $linkName, 1, $dir);
        return;
     }
-    $dirUM =~ s{//}{/}g;
-    $fullPath =~ s{//}{/}g;
-    $bpc->ServerMesg("log User $User recovered file $dirUM ($fullPath)");
+    $bpc->ServerMesg("log User $User recovered file $host/$num:$share/$dir ($a->{fullPath})");
     $dir = $origName if ( defined($origName) );
-    $dirUM = $mangle ? $bpc->fileNameUnmangle($dir) : $dir;
-    my $ext = $1 if ( $dirUM =~ /\.([^\/\.]+)$/ );
+    my $ext = $1 if ( $dir =~ /\.([^\/\.]+)$/ );
     my $contentType = $Ext2ContentType->{lc($ext)}
                                    || "application/octet-stream";
-    $fileName = $1 if ( $dirUM =~ /.*\/(.*)/ );
+    my $fileName = $1 if ( $dir =~ /.*\/(.*)/ );
     $fileName =~ s/"/\\"/g;
     print "Content-Type: $contentType\n";
     print "Content-Transfer-Encoding: binary\n";
@@ -1310,9 +1236,7 @@ EOF
     <td align="right">  $MBNew </td>
 </tr>
 EOF
-        $Backups[$i]{compress} ||= "off";
-       my $is_compress = $Lang->{off};
-       if ($Backups[$i]{compress} ne "off") {$is_compress = $Lang->{on}; }
+       my $is_compress = $Backups[$i]{compress} || $Lang->{off};
        if (! $ExistComp) { $ExistComp = "&nbsp;"; }
        if (! $MBExistComp) { $MBExistComp = "&nbsp;"; }
         $compStr .= <<EOF;
@@ -1410,7 +1334,7 @@ EOF
                 $StatusHost{startTime} : $StatusHost{endTime});
     my $reason = "";
     if ( $StatusHost{reason} ne "" ) {
-        $reason = " ($StatusHost{reason})";
+        $reason = " ($Lang->{$StatusHost{reason}})";
     }
     $statusStr .= eval("qq{$Lang->{Last_status_is_state_StatusHost_state_reason_as_of_startTime}}");
 
@@ -1487,7 +1411,7 @@ EOF
         $jobStr .= "</tr>\n";
     }
     foreach my $host ( sort(keys(%Status)) ) {
-        next if ( $Status{$host}{reason} ne "backup failed" );
+        next if ( $Status{$host}{reason} ne "Reason_backup_failed" );
         my $startTime = timeStamp2($Status{$host}{startTime});
         my($errorTime, $XferViewStr);
         if ( $Status{$host}{errorTime} > 0 ) {
@@ -1681,8 +1605,12 @@ sub ErrorExit
                             if ( defined($bpc) );
     if ( !defined($Lang->{Error}) ) {
        Header("BackupPC: Error");
+        $mesg = <<EOF if ( !defined($mesg) );
+There is some problem with the BackupPC installation.
+Please check the permissions on BackupPC_Admin.
+EOF
        print <<EOF;
-${h1("Error: Language strings not defined!!")}
+${h1("Error: Unable to read config.pl or language strings!!")}
 <p>$mesg</p>
 EOF
        Trailer();
@@ -1817,7 +1745,6 @@ sub genPoolInfo
     my $poolTime   = timeStamp2($info->{"${name}Time"});
     $info->{"${name}FileCntRm"} = $info->{"${name}FileCntRm"} + 0;
     return eval("qq{$Lang->{Pool_Stat}}");
-
 }
 
 ###########################################################################
@@ -1839,8 +1766,10 @@ sub Header
         { link => "?action=queue",             name => $Lang->{Current_queues} },
         { link => "?action=view&type=docs",    name => $Lang->{Documentation},
                                                priv => 1},
+        { link => "http://backuppc.sourceforge.net/faq", name => "FAQ",
+                                               priv => 1},
         { link => "http://backuppc.sourceforge.net", name => "SourceForge",
-                                                     priv => 1},
+                                               priv => 1},
     );
     print $Cgi->header();
     print <<EOF;
@@ -1848,7 +1777,7 @@ sub Header
 <html><head>
 <title>$title</title>
 $Conf{CgiHeaders}
-</head><body>
+</head><body bgcolor="$Conf{CgiBodyBgColor}">
 <table cellpadding="0" cellspacing="0" border="0">
 <tr valign="top"><td valign="top" bgcolor="$Conf{CgiNavBarBgColor}" width="10%">
 EOF
@@ -1888,7 +1817,7 @@ EOF
     <tr><td>$Lang->{Host_or_User_name}</td>
     <tr><td><form action="$MyURL" method="get"><small>
     <input type="text" name="host" size="10" maxlength="64">
-    <input type="hidden" name="action" value="$Lang->{hostInfo}"><input type="submit" value="$Lang->{Go}" name="ignore">
+    <input type="hidden" name="action" value="hostInfo"><input type="submit" value="$Lang->{Go}" name="ignore">
     </small></form></td></tr>
 </table>
 EOF