* Fixed stupid last-minute change in octal size conversion in
[BackupPC.git] / cgi-bin / BackupPC_Admin
index 63f03af..bc4f1e9 100755 (executable)
@@ -1,4 +1,4 @@
-#!/bin/perl -T
+#!/bin/perl
 #============================================================= -*-perl-*-w
 #
 # BackupPC_Admin: Apache/CGI interface for BackupPC.
 #
 #========================================================================
 #
-# Version 1.6.0_CVS, released 10 Dec 2002.
+# Version 2.0.2, released 6 Oct 2003.
 #
 # See http://backuppc.sourceforge.net.
 #
 #========================================================================
 
 use strict;
+no  utf8;
 use CGI;
-use lib "/usr/local/BackupPC/lib";
+use lib "/usr/local/BackupPC2.0.2/lib";
 use BackupPC::Lib;
 use BackupPC::FileZIO;
 use BackupPC::Attrib qw(:all);
@@ -65,18 +66,9 @@ use vars qw ($Lang);
 $Cgi = new CGI;
 %In = $Cgi->Vars;
 
-#
-# We require that Apache pass in $ENV{SCRIPT_NAME} and $ENV{REMOTE_USER}.
-# The latter requires .ht_access style authentication.  Replace this
-# code if you are using some other type of authentication, and have
-# a different way of getting the user name.
-#
-$MyURL  = $ENV{SCRIPT_NAME};
-$User   = $ENV{REMOTE_USER};
-
 if ( !defined($bpc) ) {
     ErrorExit($Lang->{BackupPC__Lib__new_failed__check_apache_error_log})
-       if ( !($bpc = BackupPC::Lib->new) );
+       if ( !($bpc = BackupPC::Lib->new(undef, undef, 1)) );
     $TopDir = $bpc->TopDir();
     $BinDir = $bpc->BinDir();
     %Conf   = $bpc->Conf();
@@ -89,6 +81,16 @@ if ( !defined($bpc) ) {
     $Lang   = $bpc->Lang();
 }
 
+#
+# We require that Apache pass in $ENV{SCRIPT_NAME} and $ENV{REMOTE_USER}.
+# The latter requires .ht_access style authentication.  Replace this
+# code if you are using some other type of authentication, and have
+# a different way of getting the user name.
+#
+$ENV{REMOTE_USER} = $Conf{BackupPCUser} if ( $ENV{REMOTE_USER} eq "" );
+$MyURL  = $ENV{SCRIPT_NAME};
+$User   = $ENV{REMOTE_USER};
+
 #
 # Clean up %ENV for taint checking
 #
@@ -100,14 +102,23 @@ $ENV{PATH} = $Conf{MyPath};
 #
 if ( $Conf{BackupPCUserVerify}
         && $> != (my $uid = (getpwnam($Conf{BackupPCUser}))[2]) ) {
-    ErrorExit(eval("qq{$Lang->{Wrong_user__my_userid_is___}}"));
+    ErrorExit(eval("qq{$Lang->{Wrong_user__my_userid_is___}}"), <<EOF);
+This script needs to run as the user specified in \$Conf{BackupPCUser},
+which is set to $Conf{BackupPCUser}.
+<p>
+This is an installation problem.  If you are using mod_perl then
+it appears that Apache is not running as user $Conf{BackupPCUser}.
+If you are not using mod_perl, then most like setuid is not working
+properly on BackupPC_Admin.  Check the permissions on
+$Conf{CgiDir}/BackupPC_Admin and look at the documentation.
+EOF
 }
 
 if ( !defined($Hosts) || $bpc->HostsMTime() != $HostsMTime ) {
     $HostsMTime = $bpc->HostsMTime();
     $Hosts = $bpc->HostInfoRead();
 
-    # turn operators list into a hash for quick lookups
+    # turn moreUsers list into a hash for quick lookups
     foreach my $host (keys %$Hosts) {
        $Hosts->{$host}{moreUsers} =
            {map {$_, 1} split(",", $Hosts->{$host}{moreUsers}) }
@@ -156,7 +167,8 @@ sub Action_Summary
         ErrorExit($Lang->{Only_privileged_users_can_view_PC_summaries} );
     }
     foreach my $host ( sort(keys(%Status)) ) {
-        my($fullDur, $incrCnt, $incrAge, $fullSize, $fullRate);
+        my($fullDur, $incrCnt, $incrAge, $fullSize, $fullRate, $reasonHilite);
+       my($shortErr);
         my @Backups = $bpc->BackupInfoRead($host);
         my $fullCnt = $incrCnt = 0;
         my $fullAge = $incrAge = -1;
@@ -193,10 +205,21 @@ sub Action_Summary
         $fullTot += $fullCnt;
         $incrTot += $incrCnt;
         $fullSize = sprintf("%.2f", $fullSize / 1000);
-       if (! $incrAge) { $incrAge = "&nbsp;"; }
+       $incrAge = "&nbsp;" if ( $incrAge eq "" );
+       $reasonHilite = $Conf{CgiStatusHilightColor}{$Status{$host}{reason}}
+                     || $Conf{CgiStatusHilightColor}{$Status{$host}{state}};
+       $reasonHilite = " bgcolor=\"$reasonHilite\"" if ( $reasonHilite ne "" );
+        if ( $Status{$host}{state} ne "Status_backup_in_progress"
+               && $Status{$host}{state} ne "Status_restore_in_progress"
+               && $Status{$host}{error} ne "" ) {
+           ($shortErr = $Status{$host}{error}) =~ s/(.{48}).*/$1.../;
+           $shortErr = " ($shortErr)";
+       }
+
         $str = <<EOF;
-<tr><td> ${HostLink($host)} </td>
-    <td align="center"> ${UserLink($Hosts->{$host}{user})} </td>
+<tr$reasonHilite><td> ${HostLink($host)} </td>
+    <td align="center"> ${UserLink(defined($Hosts->{$host})
+                                   ? $Hosts->{$host}{user} : "")} </td>
     <td align="center"> $fullCnt </td>
     <td align="center"> $fullAge </td>
     <td align="center"> $fullSize </td>
@@ -204,7 +227,7 @@ sub Action_Summary
     <td align="center"> $incrCnt </td>
     <td align="center"> $incrAge </td>
     <td align="center"> $Lang->{$Status{$host}{state}} </td>
-    <td> $Lang->{$Status{$host}{reason}} </td></tr>
+    <td> $Lang->{$Status{$host}{reason}}$shortErr </td></tr>
 EOF
         if ( @Backups == 0 ) {
             $hostCntNone++;
@@ -243,15 +266,16 @@ sub Action_StartStopBackup
     if ( $In{doit} ) {
         if ( $start ) {
            if ( $Hosts->{$host}{dhcp} ) {
-               $reply = $bpc->ServerMesg("backup $In{hostIP} $host"
-                                       . " $User $doFull");
+               $reply = $bpc->ServerMesg("backup $In{hostIP} ${EscURI($host)}"
+                                   . " $User $doFull");
                $str = eval("qq{$Lang->{Backup_requested_on_DHCP__host}}");
            } else {
-               $reply = $bpc->ServerMesg("backup $host $host $User $doFull");
+               $reply = $bpc->ServerMesg("backup ${EscURI($host)}"
+                                   . " ${EscURI($host)} $User $doFull");
                $str = eval("qq{$Lang->{Backup_requested_on__host_by__User}}");
            }
         } else {
-            $reply = $bpc->ServerMesg("stop $host $User $In{backoff}");
+            $reply = $bpc->ServerMesg("stop ${EscURI($host)} $User $In{backoff}");
             $str = eval("qq{$Lang->{Backup_stopped_dequeued_on__host_by__User}}");
         }
 
@@ -267,7 +291,7 @@ sub Action_StartStopBackup
             print (eval("qq{$Lang->{Are_you_sure_start}}"));
         } else {
             my $backoff = "";
-            GetStatusInfo("host($host)");
+            GetStatusInfo("host(${EscURI($host)})");
             if ( $StatusHost{backoffTime} > time ) {
                 $backoff = sprintf("%.1f",
                                   ($StatusHost{backoffTime} - time) / 3600);
@@ -311,12 +335,12 @@ EOF
     while ( @CmdQueue ) {
         my $req = pop(@CmdQueue);
         my $reqTime = timeStamp2($req->{reqTime});
-        (my $cmd = $req->{cmd}) =~ s/$BinDir\///;
+        (my $cmd = $req->{cmd}[0]) =~ s/$BinDir\///;
         $strCmd .= <<EOF;
 <tr><td> ${HostLink($req->{host})} </td>
     <td align="center"> $reqTime </td>
     <td align="center"> $req->{user} </td>
-    <td> $cmd </td></tr>
+    <td> $cmd $req->{cmd}[0] </td></tr>
 EOF
     }
     Header($Lang->{BackupPC__Queue_Summary});
@@ -348,21 +372,25 @@ sub Action_View
     } elsif ( $type eq "XferErrbad" ) {
         $file = "$TopDir/pc/$host/SmbLOG.bad";
         $file = "$TopDir/pc/$host/XferLOG.bad" if ( !-f $file && !-f "$file.z");
-        $comment = "(Extracting only Errors)";
+        $comment = $Lang->{Extracting_only_Errors};
     } elsif ( $type eq "XferErr" ) {
         $file = "$TopDir/pc/$host/SmbLOG$ext";
         $file = "$TopDir/pc/$host/XferLOG$ext" if ( !-f $file && !-f "$file.z");
-        $comment = "(Extracting only Errors)";
+        $comment = $Lang->{Extracting_only_Errors};
     } elsif ( $type eq "RestoreLOG" ) {
         $file = "$TopDir/pc/$host/RestoreLOG$ext";
     } elsif ( $type eq "RestoreErr" ) {
         $file = "$TopDir/pc/$host/RestoreLOG$ext";
-        $comment = "(Extracting only Errors)";
+        $comment = $Lang->{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) ) {
+           binmode(LOG);
             Header($Lang->{BackupPC__Documentation});
             print while ( <LOG> );
             close(LOG);
@@ -402,38 +430,41 @@ 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: \\>/
                         || /^\s*(\d+) \(\s*\d+\.\d kb\/s\) (.*)$/
                         || /^tar: dumped \d+ files/
-                        || /^added interface/i
-                        || /^restore tar file /i
-                        || /^restore directory /i
-                        || /^tarmode is now/i
-                        || /^Total bytes written/i
-                        || /^Domain=/i
-                        || /^Getting files newer than/i
-                        || /^Output is \/dev\/null/
+                        || /^\s*added interface/i
+                        || /^\s*restore tar file /i
+                        || /^\s*restore directory /i
+                        || /^\s*tarmode is now/i
+                        || /^\s*Total bytes written/i
+                        || /^\s*Domain=/i
+                        || /^\s*Getting files newer than/i
+                        || /^\s*Output is \/dev\/null/
                         || /^\([\d\.]* kb\/s\) \(average [\d\.]* kb\/s\)$/
                         || /^\s+directory \\/
-                        || /^Timezone is/
+                        || /^\s*Timezone is/
+                       || /^\s*creating lame (up|low)case table/i
                         || /^\.\//
-                        || /^  /
+                        || /^   /
                            ) {
                    $skipped++;
                    next;
                }
-               print(eval("qq{$Lang->{skipped__skipped_lines}}")) if ( $skipped );
+               print(eval("qq{$Lang->{skipped__skipped_lines}}"))
+                                                    if ( $skipped );
                $skipped = 0;
-                print ${EscapeHTML($_)};
+                print ${EscHTML($_)};
             }
         } elsif ( $linkHosts ) {
             while ( 1 ) {
                 $_ = $fh->readLine();
                 last if ( $_ eq "" );
-                my $s = ${EscapeHTML($_)};
+                my $s = ${EscHTML($_)};
                 $s =~ s/\b([\w-]+)\b/defined($Hosts->{$1})
                                         ? ${HostLink($1)} : $1/eg;
                 print $s;
@@ -445,14 +476,15 @@ sub Action_View
                 # remove any passwords and user names
                 s/(SmbSharePasswd.*=.*['"]).*(['"])/$1$2/ig;
                 s/(SmbShareUserName.*=.*['"]).*(['"])/$1$2/ig;
+                s/(RsyncdPasswd.*=.*['"]).*(['"])/$1$2/ig;
                 s/(ServerMesgSecret.*=.*['"]).*(['"])/$1$2/ig;
-                print ${EscapeHTML($_)};
+                print ${EscHTML($_)};
             }
         } else {
             while ( 1 ) {
                 $_ = $fh->readLine();
                 last if ( $_ eq "" );
-                print ${EscapeHTML($_)};
+                print ${EscHTML($_)};
             }
         }
         $fh->close();
@@ -476,7 +508,7 @@ sub Action_LOGlist
     my($url0, $hdr, $root, $str);
     if ( $host ne "" ) {
         $root = "$TopDir/pc/$host/LOG";
-        $url0 = "&host=$host";
+        $url0 = "&host=${EscURI($host)}";
         $hdr = "for host $host";
     } else {
         $root = "$TopDir/log/LOG";
@@ -556,7 +588,7 @@ sub Action_Browse
         last if ( $Backups[$i]{num} == $num );
     }
     if ( $i >= @Backups ) {
-        ErrorExit("Backup number $num for host ${EscapeHTML($host)} does"
+        ErrorExit("Backup number $num for host ${EscHTML($host)} does"
                . " not exist.");
     }
     my $backupTime = timeStamp2($Backups[$i]{startTime});
@@ -570,11 +602,15 @@ sub Action_Browse
            $share = (sort(keys(%$attr)))[0];
            $dir   = '/';
        } else {
-            ErrorExit(eval("qq{$Lang->{Directory___EscapeHTML}}"));
+            ErrorExit(eval("qq{$Lang->{Directory___EscHTML}}"));
        }
     }
+    $dir = "/$dir" if ( $dir !~ /^\// );
     my $relDir  = $dir;
     my $currDir = undef;
+    if ( $dir =~ m{(^|/)\.\.(/|$)} ) {
+        ErrorExit($Lang->{Nice_try__but_you_can_t_put});
+    }
 
     #
     # Loop up the directory tree until we hit the top.
@@ -595,18 +631,22 @@ sub Action_Browse
         # Loop over each of the files in this directory
         #
        foreach my $f ( sort(keys(%$attr)) ) {
-            my($dirOpen, $gotDir, $imgStr, $img);
+            my($dirOpen, $gotDir, $imgStr, $img, $path);
             my $fURI = $f;                             # URI escaped $f
             my $shareURI = $share;                     # URI escaped $share
-            my $path = "$relDir/$f";
+           if ( $relDir eq "" ) {
+               $path = "/$f";
+           } else {
+               ($path = "$relDir/$f") =~ s{//+}{/}g;
+           }
            if ( $shareURI eq "" ) {
-               $shareURI = $path;
-               $path  = "";
+               $shareURI = $f;
+               $path  = "/";
            }
             $path =~ s{^/+}{/};
-            $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;
+            $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 ) {
                 #
@@ -634,7 +674,7 @@ sub Action_Browse
                push(@DirStr, {needTick => 1,
                               tdArgs   => $BGcolor,
                               link     => <<EOF});
-<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>
+<a href="$MyURL?action=browse&host=${EscURI($host)}&num=$num&share=$shareURI&dir=$path">$imgStr</a><a href="$MyURL?action=browse&host=${EscURI($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;
@@ -686,15 +726,16 @@ EOF
                 } else {
                     $attrStr .= "<td colspan=\"5\" align=\"center\"> </td>\n";
                 }
+               (my $fDisp = "${EscHTML($f)}") =~ s/ /&nbsp;/g;
                 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&share=$shareURI&dir=$path">${EscapeHTML($f)}</a></td>
+<tr bgcolor="#ffffcc"><td><input type="checkbox" name="fcb$checkBoxCnt" value="$path">&nbsp;<a href="$MyURL?action=browse&host=${EscURI($host)}&num=$num&share=$shareURI&dir=$path">$fDisp</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&share=$shareURI&dir=$path">${EscapeHTML($f)}</a></td>
+<tr bgcolor="#ffffcc"><td><input type="checkbox" name="fcb$checkBoxCnt" value="$path">&nbsp;<a href="$MyURL?action=RestoreFile&host=${EscURI($host)}&num=$num&share=$shareURI&dir=$path">$fDisp</a></td>
 $attrStr
 </tr>
 EOF
@@ -708,11 +749,12 @@ EOF
         # Prune the last directory off $relDir, or at the very end
        # do the top-level directory.
         #
-       if ( $relDir eq "" ) {
+       if ( $relDir eq "" || $relDir eq "/" || $relDir !~ /(.*)\/(.*)/ ) {
            $currDir = $share;
            $share = "";
+           $relDir = "";
        } else {
-           $relDir =~ s/(.*)\/(.*)/$1/;
+           $relDir  = $1;
            $currDir = $2;
        }
     }
@@ -720,6 +762,7 @@ EOF
     my $dirDisplay = "$share/$dir";
     $dirDisplay =~ s{//+}{/}g;
     $dirDisplay =~ s{/+$}{}g;
+    $dirDisplay = "/" if ( $dirDisplay eq "" );
     my $filledBackup;
 
     if ( (my @mergeNums = @{$view->mergeNums}) > 1 ) {
@@ -750,20 +793,18 @@ EOF
     }
     my @otherDirs;
     foreach my $i ( $view->backupList($share, $dir) ) {
-        next if ( $i == $num );
         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, <<EOF);
-<a href="$MyURL?action=browse&host=$host&num=$i&share=$shareURI&dir=$path">$i</a>
-EOF
+        push(@otherDirs, "<a href=\"$MyURL?action=browse&host=${EscURI($host)}&num=$i"
+                       . "&share=$shareURI&dir=$path\">$i</a>");
+
     }
     if ( @otherDirs ) {
-       my $otherDirs  = join(", ", @otherDirs);
+       my $otherDirs  = join(",\n", @otherDirs);
         $filledBackup .= eval("qq{$Lang->{Visit_this_directory_in_backup}}");
     }
     print (eval("qq{$Lang->{Backup_browse_for__host}}"));
     Trailer();
 }
@@ -801,11 +842,11 @@ sub Action_Restore
 <input type="hidden" name="fcb$i" value="$In{'fcb' . $i}">
 EOF
         $fileListStr .= <<EOF;
-<li> ${EscapeHTML($name)}
+<li> ${EscHTML($name)}
 EOF
     }
     $hiddenStr .= "<input type=\"hidden\" name=\"fcbMax\" value=\"$In{fcbMax}\">\n";
-    $hiddenStr .= "<input type=\"hidden\" name=\"share\" value=\"${EscapeHTML($share)}\">\n";
+    $hiddenStr .= "<input type=\"hidden\" name=\"share\" value=\"${EscHTML($share)}\">\n";
     $badFileCnt++ if ( $In{pathHdr} =~ m{(^|/)\.\.(/|$)} );
     $badFileCnt++ if ( $In{num} =~ m{(^|/)\.\.(/|$)} );
     if ( @fileList == 0 ) {
@@ -847,78 +888,76 @@ EOF
         #
         # 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.
-        #
        my @fileListTrim = @fileList;
        if ( @fileListTrim > 10 ) {
            @fileListTrim = (@fileListTrim[0..9], '...');
        }
-       $bpc->ServerMesg(eval("qq{$Lang->{log_User__User_downloaded_tar_archive_for__host}}"));
+       $bpc->ServerMesg("log User $User downloaded tar archive for $host,"
+                      . " backup $num; files were: "
+                      . join(", ", @fileListTrim));
 
         my @pathOpts;
         if ( $In{relative} ) {
             @pathOpts = ("-r", $pathHdr, "-p", "");
         }
-       #
-       # 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);
+       print(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
+       #
+       # Fork the child off and manually copy the output to our stdout.
+       # This is necessary to ensure the output gets to the correct place
+       # under mod_perl.
+       #
+       $bpc->cmdSystemOrEval(["$BinDir/BackupPC_tarCreate",
+                "-h", $host,
+                "-n", $num,
+                "-s", $share,
+                @pathOpts,
+                @fileList
+           ],
+           sub { print(@_); }
        );
-       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.
-        #
        my @fileListTrim = @fileList;
        if ( @fileListTrim > 10 ) {
            @fileListTrim = (@fileListTrim[0..9], '...');
        }
-       $bpc->ServerMesg(eval("qq{$Lang->{log_User__User_downloaded_zip_archive_for__host}}"));
+       $bpc->ServerMesg("log User $User downloaded zip archive for $host,"
+                      . " backup $num; files were: "
+                      . join(", ", @fileListTrim));
 
         my @pathOpts;
         if ( $In{relative} ) {
             @pathOpts = ("-r", $pathHdr, "-p", "");
         }
-       #
-       # 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);
+       print(STDOUT <<EOF);
 Content-Type: application/zip
 Content-Transfer-Encoding: binary
 Content-Disposition: attachment; filename=\"restore.zip\"
 
 EOF
        $In{compressLevel} = 5 if ( $In{compressLevel} !~ /^\d+$/ );
-       local(@ARGV);
-       @ARGV = (
-             "-h", $host,
-             "-n", $num,
-             "-c", $In{compressLevel},
-             "-s", $share,
-             @pathOpts,
-             @fileList
-        );
-        do "$BinDir/BackupPC_zipCreate";
+       #
+       # Fork the child off and manually copy the output to our stdout.
+       # This is necessary to ensure the output gets to the correct place
+       # under mod_perl.
+       #
+       $bpc->cmdSystemOrEval(["$BinDir/BackupPC_zipCreate",
+                "-h", $host,
+                "-n", $num,
+                "-c", $In{compressLevel},
+                "-s", $share,
+                @pathOpts,
+                @fileList
+           ],
+           sub { print(@_); }
+       );
     } elsif ( $In{type} == 3 ) {
         #
         # Do restore directly onto host
@@ -944,7 +983,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}}"));
@@ -986,13 +1025,14 @@ EOF
                          [qw(*RestoreReq)]);
         $dump->Indent(1);
         if ( open(REQ, ">$TopDir/pc/$hostDest/$reqFileName") ) {
+           binmode(REQ);
             print(REQ $dump->Dump);
             close(REQ);
         } else {
             ErrorExit(eval("qq{$Lang->{Can_t_open_create}}"));
         }
-       $reply = $bpc->ServerMesg("restore $ipAddr"
-                       . " $hostDest $User $reqFileName");
+       $reply = $bpc->ServerMesg("restore ${EscURI($ipAddr)}"
+                       . " ${EscURI($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}}"));
@@ -1108,7 +1148,7 @@ sub restoreFile
     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)}");
+        ErrorExit("Can't restore bad file ${EscHTML($dir)}");
     }
     my $f = BackupPC::FileZIO->open($a->{fullPath}, 0, $a->{compress});
     my $data;
@@ -1170,7 +1210,7 @@ sub Action_HostInfo
         }
         $In{host} = $host;
     }
-    GetStatusInfo("host($host)");
+    GetStatusInfo("host(${EscURI($host)})");
     $bpc->ConfigRead($host);
     %Conf = $bpc->Conf();
     my $Privileged = CheckPermission($host);
@@ -1204,7 +1244,7 @@ sub Action_HostInfo
                   (1 - $Backups[$i]{sizeNewComp} / $Backups[$i]{sizeNew}));
         }
         my $age = sprintf("%.1f", (time - $Backups[$i]{startTime}) / (24*3600));
-        my $browseURL = "$MyURL?action=browse&host=$host&num=$Backups[$i]{num}";
+        my $browseURL = "$MyURL?action=browse&host=${EscURI($host)}&num=$Backups[$i]{num}";
         my $filled = $Backups[$i]{noFill} ? $Lang->{No} : $Lang->{Yes};
         $filled .= " ($Backups[$i]{fillFromNum}) "
                             if ( $Backups[$i]{fillFromNum} ne "" );
@@ -1232,9 +1272,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;
@@ -1252,8 +1290,8 @@ EOF
         $errStr .= <<EOF;
 <tr><td align="center"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
     <td align="center"> $ltype </td>
-    <td align="center"> <a href="$MyURL?action=view&type=XferLOG&num=$Backups[$i]{num}&host=$host">XferLOG</a>,
-                      <a href="$MyURL?action=view&type=XferErr&num=$Backups[$i]{num}&host=$host">Errors</a> </td>
+    <td align="center"> <a href="$MyURL?action=view&type=XferLOG&num=$Backups[$i]{num}&host=${EscURI($host)}">$Lang->{XferLOG}</a>,
+                      <a href="$MyURL?action=view&type=XferErr&num=$Backups[$i]{num}&host=${EscURI($host)}">$Lang->{Errors}</a> </td>
     <td align="right">  $Backups[$i]{xferErrs} </td>
     <td align="right">  $Backups[$i]{xferBadFile} </td>
     <td align="right">  $Backups[$i]{xferBadShare} </td>
@@ -1274,7 +1312,7 @@ EOF
        my $Restores_Result = $Lang->{failed};
        if ($Restores[$i]{result} ne "failed") { $Restores_Result = $Lang->{success}; }
        $restoreStr  .= <<EOF;
-<tr><td align="center"><a href="$MyURL?action=restoreInfo&num=$Restores[$i]{num}&host=$host">$Restores[$i]{num}</a> </td>
+<tr><td align="center"><a href="$MyURL?action=restoreInfo&num=$Restores[$i]{num}&host=${EscURI($host)}">$Restores[$i]{num}</a> </td>
     <td align="center"> $Restores_Result </td>
     <td align="right"> $startTime </td>
     <td align="right"> $duration </td>
@@ -1336,8 +1374,10 @@ EOF
     }
     $statusStr .= eval("qq{$Lang->{Last_status_is_state_StatusHost_state_reason_as_of_startTime}}");
 
-    if ( $StatusHost{error} ne "" ) {
-        $statusStr .= eval("qq{$Lang->{Last_error_is____EscapeHTML_StatusHost_error}}");
+    if ( $StatusHost{state} ne "Status_backup_in_progress"
+           && $StatusHost{state} ne "Status_restore_in_progress"
+           && $StatusHost{error} ne "" ) {
+        $statusStr .= eval("qq{$Lang->{Last_error_is____EscHTML_StatusHost_error}}");
     }
     my $priorStr = "Pings";
     if ( $StatusHost{deadCnt} > 0 ) {
@@ -1385,7 +1425,7 @@ sub Action_GeneralInfo
     GetStatusInfo("info jobs hosts queueLen");
     my $Privileged = CheckPermission();
 
-    my($jobStr, $statusStr, $tarPidHdr);
+    my($jobStr, $statusStr);
     foreach my $host ( sort(keys(%Jobs)) ) {
         my $startTime = timeStamp2($Jobs{$host}{startTime});
         next if ( $host eq $bpc->trashJob
@@ -1393,23 +1433,24 @@ sub Action_GeneralInfo
         $Jobs{$host}{type} = $Status{$host}{type}
                     if ( $Jobs{$host}{type} eq "" && defined($Status{$host}));
         (my $cmd = $Jobs{$host}{cmd}) =~ s/$BinDir\///g;
+        (my $xferPid = $Jobs{$host}{xferPid}) =~ s/,/, /g;
         $jobStr .= <<EOF;
 <tr><td> ${HostLink($host)} </td>
     <td align="center"> $Jobs{$host}{type} </td>
-    <td align="center"> ${UserLink($Hosts->{$host}{user})} </td>
+    <td align="center"> ${UserLink(defined($Hosts->{$host})
+                                       ? $Hosts->{$host}{user} : "")} </td>
     <td> $startTime </td>
     <td> $cmd </td>
     <td align="center"> $Jobs{$host}{pid} </td>
-    <td align="center"> $Jobs{$host}{xferPid} </td>
+    <td align="center"> $xferPid </td>
 EOF
-        if ( $Jobs{$host}{tarPid} > 0 ) {
-            $jobStr .= "    <td align=\"center\"> $Jobs{$host}{tarPid} </td>\n";
-            $tarPidHdr ||= "<td align=\"center\"> tar PID </td>\n";
-        }
         $jobStr .= "</tr>\n";
     }
     foreach my $host ( sort(keys(%Status)) ) {
-        next if ( $Status{$host}{reason} ne "Reason_backup_failed" );
+        next if ( $Status{$host}{reason} ne "Reason_backup_failed"
+                   && $Status{$host}{reason} ne "Reason_restore_failed"
+                   && (!$Status{$host}{userReq}
+                       || $Status{$host}{reason} ne "Reason_no_ping") );
         my $startTime = timeStamp2($Status{$host}{startTime});
         my($errorTime, $XferViewStr);
         if ( $Status{$host}{errorTime} > 0 ) {
@@ -1421,8 +1462,8 @@ EOF
                 || -f "$TopDir/pc/$host/XferLOG.bad.z"
                 ) {
             $XferViewStr = <<EOF;
-<a href="$MyURL?action=view&type=XferLOGbad&host=$host">XferLOG</a>,
-<a href="$MyURL?action=view&type=XferErrbad&host=$host">XferErr</a>
+<a href="$MyURL?action=view&type=XferLOGbad&host=${EscURI($host)}">$Lang->{XferLOG}</a>,
+<a href="$MyURL?action=view&type=XferErrbad&host=${EscURI($host)}">$Lang->{Errors}</a>
 EOF
         } else {
             $XferViewStr = "";
@@ -1431,11 +1472,12 @@ EOF
         $statusStr .= <<EOF;
 <tr><td> ${HostLink($host)} </td>
     <td align="center"> $Status{$host}{type} </td>
-    <td align="center"> ${UserLink($Hosts->{$host}{user})} </td>
+    <td align="center"> ${UserLink(defined($Hosts->{$host})
+                                       ? $Hosts->{$host}{user} : "")} </td>
     <td align="right"> $startTime </td>
     <td> $XferViewStr </td>
     <td align="right"> $errorTime </td>
-    <td> ${EscapeHTML($shortErr)} </td></tr>
+    <td> ${EscHTML($shortErr)} </td></tr>
 EOF
     }
     my $now          = timeStamp2(time);
@@ -1464,13 +1506,7 @@ EOF
     }
 
     Header($Lang->{H_BackupPC_Server_Status});
-       #Header("H_BackupPC_Server_Status");
     print (eval ("qq{$Lang->{BackupPC_Server_Status}}"));
-
-    #Header($Lang->{BackupPC_Server_Status});
-
-    #my $trans_text = $Lang->{BackupPC_Server_Status};
-    #print eval ("qq{$trans_text}");
     Trailer();
 }
 
@@ -1546,7 +1582,7 @@ sub HostLink
     my($host) = @_;
     my($s);
     if ( defined($Hosts->{$host}) || defined($Status{$host}) ) {
-        $s = "<a href=\"$MyURL?host=$host\">$host</a>";
+        $s = "<a href=\"$MyURL?host=${EscURI($host)}\">$host</a>";
     } else {
         $s = $host;
     }
@@ -1571,23 +1607,23 @@ sub UserLink
     return \$s;
 }
 
-sub EscapeHTML
+sub EscHTML
 {
     my($s) = @_;
     $s =~ s/&/&amp;/g;
     $s =~ s/\"/&quot;/g;
     $s =~ s/>/&gt;/g;
     $s =~ s/</&lt;/g;
-    $s =~ s{([^[:print:]])}{sprintf("&\#x%02X", ord($1));}eg;
+    $s =~ s{([^[:print:]])}{sprintf("&\#x%02X;", ord($1));}eg;
     return \$s;
 }
 
-##sub URIEncode
-##{
-##    my($s) = @_;
-##    $s =~ s{(['"&%[:^print:]])}{sprintf("%%%02X", ord($1));}eg;
-##    return \$s;
-##}
+sub EscURI
+{
+    my($s) = @_;
+    $s =~ s{([^\w.\/-])}{sprintf("%%%02X", ord($1));}eg;
+    return \$s;
+}
 
 sub ErrorExit
 {
@@ -1599,12 +1635,26 @@ sub ErrorExit
     $Conf{CgiNavBarBgColor}  ||= "#ddeeee";
     $Conf{CgiHeaderBgColor}  ||= "#99cc33";
 
+    if ( !defined($ENV{REMOTE_USER}) ) {
+       $mesg .= <<EOF;
+<p>
+Note: \$ENV{REMOTE_USER} is not set, which could mean there is an
+installation problem.  BackupPC_Admin expects Apache to authenticate
+the user and pass their user name into this script as the REMOTE_USER
+environment variable.  See the documentation.
+EOF
+    }
+
     $bpc->ServerMesg("log User $User (host=$In{host}) got CGI error: $head")
                             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();
@@ -1663,7 +1713,8 @@ sub CheckPermission
     my($host) = @_;
     my $Privileged = 0;
 
-    return 0 if ( $User eq "" || ($host ne "" && !defined($Hosts->{$host})) );
+    return 0 if ( $User eq "" && $Conf{CgiAdminUsers} ne "*"
+              || $host ne "" && !defined($Hosts->{$host}) );
     if ( $Conf{CgiAdminUserGroup} ne "" ) {
         my($n,$p,$gid,$mem) = getgrnam($Conf{CgiAdminUserGroup});
         $Privileged ||= ($mem =~ /\b$User\b/);
@@ -1674,7 +1725,7 @@ sub CheckPermission
     }
     $PrivAdmin = $Privileged;
     $Privileged ||= $User eq $Hosts->{$host}{user};
-    $Privileged ||= defined($Hosts->{$host}{operators}{$User});
+    $Privileged ||= defined($Hosts->{$host}{moreUsers}{$User});
 
     return $Privileged;
 }
@@ -1708,13 +1759,13 @@ sub ConfirmIPAddress
     my($host) = @_;
     my $ipAddr = $host;
 
-    if ( $Hosts->{$host}{dhcp}
+    if ( defined($Hosts->{$host}) && $Hosts->{$host}{dhcp}
               && $ENV{REMOTE_ADDR} =~ /^(\d+[\.\d]*)$/ ) {
        $ipAddr = $1;
        my($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($ipAddr);
        if ( $netBiosHost ne $host ) {
            my($tryIP);
-           GetStatusInfo("host($host)");
+           GetStatusInfo("host(${EscURI($host)})");
            if ( defined($StatusHost{dhcpHostIP})
                        && $StatusHost{dhcpHostIP} ne $ipAddr ) {
                $tryIP = eval("qq{$Lang->{tryIP}}");
@@ -1739,7 +1790,6 @@ sub genPoolInfo
     my $poolTime   = timeStamp2($info->{"${name}Time"});
     $info->{"${name}FileCntRm"} = $info->{"${name}FileCntRm"} + 0;
     return eval("qq{$Lang->{Pool_Stat}}");
-
 }
 
 ###########################################################################
@@ -1761,8 +1811,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;
@@ -1780,28 +1832,28 @@ EOF
         my $host = $In{host};
         NavSectionTitle( eval("qq{$Lang->{Host_Inhost}}") );
         NavSectionStart();
-        NavLink("?host=$host", $Lang->{Home});
-        NavLink("?action=view&type=LOG&host=$host", $Lang->{LOG_file});
-        NavLink("?action=LOGlist&host=$host", $Lang->{Old_LOGs});
+        NavLink("?host=${EscURI($host)}", $Lang->{Home});
+        NavLink("?action=view&type=LOG&host=${EscURI($host)}", $Lang->{LOG_file});
+        NavLink("?action=LOGlist&host=${EscURI($host)}", $Lang->{Old_LOGs});
         if ( -f "$TopDir/pc/$host/SmbLOG.bad"
                     || -f "$TopDir/pc/$host/SmbLOG.bad.z"
                     || -f "$TopDir/pc/$host/XferLOG.bad"
                     || -f "$TopDir/pc/$host/XferLOG.bad.z" ) {
-            NavLink("?action=view&type=XferLOGbad&host=$host",
+            NavLink("?action=view&type=XferLOGbad&host=${EscURI($host)}",
                                 $Lang->{Last_bad_XferLOG});
-            NavLink("?action=view&type=XferErrbad&host=$host",
+            NavLink("?action=view&type=XferErrbad&host=${EscURI($host)}",
                                 $Lang->{Last_bad_XferLOG_errors_only});
         }
         if ( -f "$TopDir/pc/$host/config.pl" ) {
-            NavLink("?action=view&type=config&host=$host", $Lang->{Config_file});
+            NavLink("?action=view&type=config&host=${EscURI($host)}", $Lang->{Config_file});
         }
         NavSectionEnd();
     }
     NavSectionTitle($Lang->{Hosts});
     if ( defined($Hosts) && %$Hosts > 0 ) {
-        NavSectionStart(0);
+        NavSectionStart(1);
         foreach my $host ( GetUserHosts() ) {
-            NavLink("?host=$host", $host);
+            NavLink("?host=${EscURI($host)}", $host);
         }
         NavSectionEnd();
     }
@@ -1855,7 +1907,7 @@ sub NavSectionStart
 {
     my($padding) = @_;
 
-    $padding = 2 if ( !defined($padding) );
+    $padding = 1 if ( !defined($padding) );
     print <<EOF;
 <table cellpadding="$padding" cellspacing="0" border="0" width="100%">
 EOF