* Couple of minor updates to BackupPC_Admin, documentation and ChangeLog
[BackupPC.git] / cgi-bin / BackupPC_Admin
index 85d8807..e068f98 100755 (executable)
@@ -39,7 +39,7 @@
 #
 #========================================================================
 #
-# Version 1.6.0_CVS, released 10 Dec 2002.
+# Version 2.0.0beta2, released 11 May 2003.
 #
 # See http://backuppc.sourceforge.net.
 #
@@ -76,7 +76,7 @@ $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();
@@ -100,14 +100,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 +165,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 +203,20 @@ 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}};
+       $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 +224,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 +263,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 +288,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 +332,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,16 +369,16 @@ 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"
@@ -432,13 +453,13 @@ sub Action_View
                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;
@@ -451,13 +472,13 @@ sub Action_View
                 s/(SmbSharePasswd.*=.*['"]).*(['"])/$1$2/ig;
                 s/(SmbShareUserName.*=.*['"]).*(['"])/$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();
@@ -481,7 +502,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";
@@ -561,7 +582,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});
@@ -575,9 +596,10 @@ 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;
 
@@ -600,18 +622,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") =~ s{//+}{/}g;
+           if ( $relDir eq "" ) {
+               $path = "/$f";
+           } else {
+               ($path = "$relDir/$f") =~ s{//+}{/}g;
+           }
            if ( $shareURI eq "" ) {
-               $shareURI = $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 ) {
                 #
@@ -639,7 +665,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;
@@ -691,15 +717,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
@@ -713,12 +740,12 @@ EOF
         # Prune the last directory off $relDir, or at the very end
        # do the top-level directory.
         #
-       if ( $relDir eq "" || $relDir eq "/" ) {
+       if ( $relDir eq "" || $relDir eq "/" || $relDir !~ /(.*)\/(.*)/ ) {
            $currDir = $share;
            $share = "";
            $relDir = "";
        } else {
-           $relDir =~ s/(.*)\/(.*)/$1/;
+           $relDir  = $1;
            $currDir = $2;
        }
     }
@@ -726,6 +753,7 @@ EOF
     my $dirDisplay = "$share/$dir";
     $dirDisplay =~ s{//+}{/}g;
     $dirDisplay =~ s{/+$}{}g;
+    $dirDisplay = "/" if ( $dirDisplay eq "" );
     my $filledBackup;
 
     if ( (my @mergeNums = @{$view->mergeNums}) > 1 ) {
@@ -760,7 +788,7 @@ EOF
         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"
+        push(@otherDirs, "<a href=\"$MyURL?action=browse&host=${EscURI($host)}&num=$i"
                        . "&share=$shareURI&dir=$path\">$i</a>");
 
     }
@@ -768,7 +796,6 @@ EOF
        my $otherDirs  = join(",\n", @otherDirs);
         $filledBackup .= eval("qq{$Lang->{Visit_this_directory_in_backup}}");
     }
     print (eval("qq{$Lang->{Backup_browse_for__host}}"));
     Trailer();
 }
@@ -806,11 +833,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 ) {
@@ -852,10 +879,6 @@ 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], '...');
@@ -866,33 +889,30 @@ EOF
         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], '...');
@@ -903,27 +923,28 @@ EOF
         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
@@ -996,8 +1017,8 @@ EOF
         } 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}}"));
@@ -1113,7 +1134,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;
@@ -1175,7 +1196,7 @@ sub Action_HostInfo
         }
         $In{host} = $host;
     }
-    GetStatusInfo("host($host)");
+    GetStatusInfo("host(${EscURI($host)})");
     $bpc->ConfigRead($host);
     %Conf = $bpc->Conf();
     my $Privileged = CheckPermission($host);
@@ -1209,7 +1230,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 "" );
@@ -1255,8 +1276,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>
@@ -1277,7 +1298,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>
@@ -1339,8 +1360,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 ) {
@@ -1388,7 +1411,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
@@ -1396,23 +1419,23 @@ 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}{userReq}
+                       || $Status{$host}{reason} ne "Reason_no_ping") );
         my $startTime = timeStamp2($Status{$host}{startTime});
         my($errorTime, $XferViewStr);
         if ( $Status{$host}{errorTime} > 0 ) {
@@ -1424,8 +1447,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 = "";
@@ -1434,11 +1457,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);
@@ -1467,13 +1491,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();
 }
 
@@ -1549,7 +1567,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;
     }
@@ -1574,23 +1592,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
 {
@@ -1602,6 +1620,16 @@ sub ErrorExit
     $Conf{CgiNavBarBgColor}  ||= "#ddeeee";
     $Conf{CgiHeaderBgColor}  ||= "#99cc33";
 
+    if ( 1 || !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}) ) {
@@ -1681,7 +1709,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;
 }
@@ -1715,13 +1743,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}}");
@@ -1788,20 +1816,20 @@ 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();
     }
@@ -1809,7 +1837,7 @@ EOF
     if ( defined($Hosts) && %$Hosts > 0 ) {
         NavSectionStart(0);
         foreach my $host ( GetUserHosts() ) {
-            NavLink("?host=$host", $host);
+            NavLink("?host=${EscURI($host)}", $host);
         }
         NavSectionEnd();
     }