* Fixed stupid last-minute change in octal size conversion in
[BackupPC.git] / cgi-bin / BackupPC_Admin
index cb3d26e..bc4f1e9 100755 (executable)
@@ -1,4 +1,4 @@
-#!/bin/perl -T
+#!/bin/perl
 #============================================================= -*-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>
 #
 #========================================================================
 #
-# Version 1.5.0_CVS, released 2 Aug 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);
+use BackupPC::View;
 use Data::Dumper;
 
 use vars qw($Cgi %In $MyURL $User %Conf $TopDir $BinDir $bpc);
@@ -63,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();
@@ -87,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
 #
@@ -98,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}) }
@@ -124,7 +137,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 +145,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);
@@ -154,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;
@@ -191,18 +205,29 @@ 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>
     <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}}$shortErr </td></tr>
 EOF
         if ( @Backups == 0 ) {
             $hostCntNone++;
@@ -241,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}}");
         }
 
@@ -265,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);
@@ -309,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});
@@ -346,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);
@@ -400,37 +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;
@@ -442,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();
@@ -473,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";
@@ -533,148 +568,99 @@ 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"
+        ErrorExit("Backup number $num for host ${EscHTML($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___EscHTML}}"));
        }
     }
-    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);
+    $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.
     #
     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";
-            my($dirOpen, $gotDir, $imgStr, $img);
-            $fum = $mangle ? $bpc->fileNameUnmangle($f) : $f;  # unmangled $f
-            my $fumURI = $fum;                                 # URI escaped $f
+       foreach my $f ( sort(keys(%$attr)) ) {
+            my($dirOpen, $gotDir, $imgStr, $img, $path);
+            my $fURI = $f;                             # URI escaped $f
+            my $shareURI = $share;                     # URI escaped $share
+           if ( $relDir eq "" ) {
+               $path = "/$f";
+           } else {
+               ($path = "$relDir/$f") =~ s{//+}{/}g;
+           }
+           if ( $shareURI eq "" ) {
+               $shareURI = $f;
+               $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\">";
@@ -683,12 +669,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=${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;
@@ -723,29 +709,33 @@ 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";
                 }
+               (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&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=${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&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=${EscURI($host)}&num=$num&share=$shareURI&dir=$path">$fDisp</a></td>
 $attrStr
 </tr>
 EOF
@@ -754,19 +744,30 @@ 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 "" || $relDir eq "/" || $relDir !~ /(.*)\/(.*)/ ) {
+           $currDir = $share;
+           $share = "";
+           $relDir = "";
+       } else {
+           $relDir  = $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;
+    $dirDisplay = "/" if ( $dirDisplay eq "" );
     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}}"));
@@ -790,28 +791,38 @@ 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=${EscURI($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();
 }
 
 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}}"));
     }
@@ -819,27 +830,23 @@ 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
         $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=\"${EscHTML($share)}\">\n";
     $badFileCnt++ if ( $In{pathHdr} =~ m{(^|/)\.\.(/|$)} );
     $badFileCnt++ if ( $In{num} =~ m{(^|/)\.\.(/|$)} );
     if ( @fileList == 0 ) {
@@ -880,83 +887,77 @@ EOF
     } elsif ( $In{type} == 1 ) {
         #
         # Provide the selected files via a tar archive.
-        #
-        $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("log User $User downloaded tar archive for $host,"
+                      . " backup $num; files were: "
+                      . join(", ", @fileListTrim));
+
         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",
-             "-h", $host,
-             "-n", $num,
-             "-s", $share,
-             @pathOpts,
-             @fileList
-        );
+       print(STDOUT <<EOF);
+Content-Type: application/x-gtar
+Content-Transfer-Encoding: binary
+Content-Disposition: attachment; filename=\"restore.tar\"
+
+EOF
+       #
+       # 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(@_); }
+       );
     } elsif ( $In{type} == 2 ) {
         #
         # Provide the selected files via a zip archive.
-        #
-        $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("log User $User downloaded zip archive for $host,"
+                      . " backup $num; files were: "
+                      . join(", ", @fileListTrim));
+
         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";
+       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+$/ );
-        exec("$BinDir/BackupPC_zipCreate",
-             "-h", $host,
-             "-n", $num,
-             "-c", $In{compressLevel},
-             "-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_zipCreate",
+                "-h", $host,
+                "-n", $num,
+                "-c", $In{compressLevel},
+                "-s", $share,
+                @pathOpts,
+                @fileList
+           ],
+           sub { print(@_); }
+       );
     } elsif ( $In{type} == 3 ) {
         #
         # Do restore directly onto host
@@ -982,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}}"));
@@ -1024,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}}"));
@@ -1040,14 +1042,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).
     #
@@ -1133,59 +1135,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 ${EscHTML($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 ) {
        #
@@ -1198,20 +1163,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";
@@ -1250,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);
@@ -1284,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 "" );
@@ -1312,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;
@@ -1332,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>
@@ -1354,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>
@@ -1412,12 +1370,14 @@ 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}}");
 
-    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 ) {
@@ -1465,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
@@ -1473,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 "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 ) {
@@ -1501,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 = "";
@@ -1511,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);
@@ -1544,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();
 }
 
@@ -1626,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;
     }
@@ -1651,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
 {
@@ -1679,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();
@@ -1743,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/);
@@ -1754,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;
 }
@@ -1788,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}}");
@@ -1819,7 +1790,6 @@ sub genPoolInfo
     my $poolTime   = timeStamp2($info->{"${name}Time"});
     $info->{"${name}FileCntRm"} = $info->{"${name}FileCntRm"} + 0;
     return eval("qq{$Lang->{Pool_Stat}}");
-
 }
 
 ###########################################################################
@@ -1841,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;
@@ -1850,7 +1822,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
@@ -1860,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();
     }
@@ -1890,7 +1862,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
@@ -1935,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