419ccb9d563d73066be059759766bebcec7a2918
[BackupPC.git] / cgi-bin / BackupPC_Admin
1 #!/usr/bin/perl -T
2 #============================================================= -*-perl-*-w
3 #
4 # BackupPC_Admin: Apache/CGI interface for BackupPC.
5 #
6 # DESCRIPTION
7 #   BackupPC_Admin provides a flexible web interface for BackupPC.
8 #   It is a CGI script that runs under Apache.
9 #
10 #   It requires that Apache pass in $ENV{SCRIPT_NAME} and
11 #   $ENV{REMOTE_USER}. The latter requires .ht_access style
12 #   authentication. Replace the code below if you are using some other
13 #   type of authentication, and have a different way of getting the
14 #   user name.
15 #
16 #   Also, this script needs to run as the BackupPC user.  To accomplish
17 #   this the script is typically installed as setuid to the BackupPC user.
18 #
19 # AUTHOR
20 #   Craig Barratt  <cbarratt@users.sourceforge.net>
21 #
22 # COPYRIGHT
23 #   Copyright (C) 2001  Craig Barratt
24 #
25 #   This program is free software; you can redistribute it and/or modify
26 #   it under the terms of the GNU General Public License as published by
27 #   the Free Software Foundation; either version 2 of the License, or
28 #   (at your option) any later version.
29 #
30 #   This program is distributed in the hope that it will be useful,
31 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
32 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
33 #   GNU General Public License for more details.
34 #
35 #   You should have received a copy of the GNU General Public License
36 #   along with this program; if not, write to the Free Software
37 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
38 #
39 #========================================================================
40 #
41 # Version 1.5.0beta0, released 30 Jun 2002.
42 #
43 # See http://backuppc.sourceforge.net.
44 #
45 #========================================================================
46
47 use strict;
48 use CGI;
49 use lib "/usr/local/BackupPC/lib";
50 use BackupPC::Lib;
51 use BackupPC::FileZIO;
52 use BackupPC::Attrib qw(:all);
53 use Data::Dumper;
54
55 use vars qw($Cgi %In $MyURL $User %Conf $TopDir $BinDir $bpc);
56 use vars qw(%Status %Info %Jobs @BgQueue @UserQueue @CmdQueue
57             %QueueLen %StatusHost);
58 use vars qw($Hosts $HostsMTime $ConfigMTime $PrivAdmin);
59 use vars qw(%UserEmailInfo $UserEmailInfoMTime %RestoreReq);
60
61 use vars qw ($Lang);
62
63 $Cgi = new CGI;
64 %In = $Cgi->Vars;
65
66 #
67 # We require that Apache pass in $ENV{SCRIPT_NAME} and $ENV{REMOTE_USER}.
68 # The latter requires .ht_access style authentication.  Replace this
69 # code if you are using some other type of authentication, and have
70 # a different way of getting the user name.
71 #
72 $MyURL  = $ENV{SCRIPT_NAME};
73 $User   = $ENV{REMOTE_USER};
74
75 if ( !defined($bpc) ) {
76     ErrorExit($Lang->{BackupPC__Lib__new_failed__check_apache_error_log})
77         if ( !($bpc = BackupPC::Lib->new) );
78     $TopDir = $bpc->TopDir();
79     $BinDir = $bpc->BinDir();
80     %Conf   = $bpc->Conf();
81     $Lang   = $bpc->Lang();
82     $ConfigMTime = $bpc->ConfigMTime();
83 } elsif ( $bpc->ConfigMTime() != $ConfigMTime ) {
84     $bpc->ConfigRead();
85     %Conf   = $bpc->Conf();
86     $ConfigMTime = $bpc->ConfigMTime();
87     $Lang   = $bpc->Lang();
88 }
89
90 #
91 # Clean up %ENV for taint checking
92 #
93 delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
94 $ENV{PATH} = $Conf{MyPath};
95
96 #
97 # Verify we are running as the correct user
98 #
99 if ( $Conf{BackupPCUserVerify}
100         && $> != (my $uid = (getpwnam($Conf{BackupPCUser}))[2]) ) {
101     ErrorExit(eval("qq{$Lang->{Wrong_user__my_userid_is___}}"));
102 }
103
104 if ( !defined($Hosts) || $bpc->HostsMTime() != $HostsMTime ) {
105     $HostsMTime = $bpc->HostsMTime();
106     $Hosts = $bpc->HostInfoRead();
107
108     # turn operators list into a hash for quick lookups
109     foreach my $host (keys %$Hosts) {
110        $Hosts->{$host}{moreUsers} =
111            {map {$_, 1} split(",", $Hosts->{$host}{moreUsers}) }
112     }
113 }
114
115 my %ActionDispatch = (
116     "summary"                    => \&Action_Summary,
117     $Lang->{Start_Incr_Backup}   => \&Action_StartStopBackup,
118     $Lang->{Start_Full_Backup}   => \&Action_StartStopBackup,
119     $Lang->{Stop_Dequeue_Backup} => \&Action_StartStopBackup,
120     "queue"                      => \&Action_Queue,
121     "view"                       => \&Action_View,
122     "LOGlist"                    => \&Action_LOGlist,
123     "emailSummary"               => \&Action_EmailSummary,
124     "browse"                     => \&Action_Browse,
125     $Lang->{Restore}             => \&Action_Restore,
126     "RestoreFile"                => \&Action_RestoreFile,
127     $Lang->{hostInfo}            => \&Action_HostInfo,
128     "generalInfo"                => \&Action_GeneralInfo,
129     "restoreInfo"                => \&Action_RestoreInfo,
130 );
131
132 #
133 # Set default actions, then call sub handler
134 #
135 $In{action} ||= $Lang->{hostInfo}    if ( defined($In{host}) );
136 $In{action}   = "generalInfo" if ( !defined($ActionDispatch{$In{action}}) );
137 $ActionDispatch{$In{action}}();
138 exit(0);
139
140 ###########################################################################
141 # Action handling subroutines
142 ###########################################################################
143
144 sub Action_Summary
145 {
146     my($fullTot, $fullSizeTot, $incrTot, $incrSizeTot, $str,
147        $strNone, $strGood, $hostCntGood, $hostCntNone);
148
149     $hostCntGood = $hostCntNone = 0;
150     GetStatusInfo("hosts");
151     my $Privileged = CheckPermission();
152
153     if ( !$Privileged ) {
154         ErrorExit($Lang->{Only_privileged_users_can_view_PC_summaries} );
155     }
156     foreach my $host ( sort(keys(%Status)) ) {
157         my($fullDur, $incrCnt, $incrAge, $fullSize, $fullRate);
158         my @Backups = $bpc->BackupInfoRead($host);
159         my $fullCnt = $incrCnt = 0;
160         my $fullAge = $incrAge = -1;
161         for ( my $i = 0 ; $i < @Backups ; $i++ ) {
162             if ( $Backups[$i]{type} eq "full" ) {
163                 $fullCnt++;
164                 if ( $fullAge < 0 || $Backups[$i]{startTime} > $fullAge ) {
165                     $fullAge  = $Backups[$i]{startTime};
166                     $fullSize = $Backups[$i]{size} / (1024 * 1024);
167                     $fullDur  = $Backups[$i]{endTime} - $Backups[$i]{startTime};
168                 }
169                 $fullSizeTot += $Backups[$i]{size} / (1024 * 1024);
170             } else {
171                 $incrCnt++;
172                 if ( $incrAge < 0 || $Backups[$i]{startTime} > $incrAge ) {
173                     $incrAge = $Backups[$i]{startTime};
174                 }
175                 $incrSizeTot += $Backups[$i]{size} / (1024 * 1024);
176             }
177         }
178         if ( $fullAge < 0 ) {
179             $fullAge = "";
180             $fullRate = "";
181         } else {
182             $fullAge = sprintf("%.1f", (time - $fullAge) / (24 * 3600));
183             $fullRate = sprintf("%.2f",
184                                 $fullSize / ($fullDur <= 0 ? 1 : $fullDur));
185         }
186         if ( $incrAge < 0 ) {
187             $incrAge = "";
188         } else {
189             $incrAge = sprintf("%.1f", (time - $incrAge) / (24 * 3600));
190         }
191         $fullTot += $fullCnt;
192         $incrTot += $incrCnt;
193         $fullSize = sprintf("%.2f", $fullSize / 1000);
194         if (! $incrAge) { $incrAge = "&nbsp;"; }
195         $str = <<EOF;
196 <tr><td> ${HostLink($host)} </td>
197     <td align="center"> ${UserLink($Hosts->{$host}{user})} </td>
198     <td align="center"> $fullCnt </td>
199     <td align="center"> $fullAge </td>
200     <td align="center"> $fullSize </td>
201     <td align="center"> $fullRate </td>
202     <td align="center"> $incrCnt </td>
203     <td align="center"> $incrAge </td>
204     <td align="center"> $Status{$host}{state} </td>
205     <td> $Status{$host}{reason} </td></tr>
206 EOF
207         if ( @Backups == 0 ) {
208             $hostCntNone++;
209             $strNone .= $str;
210         } else {
211             $hostCntGood++;
212             $strGood .= $str;
213         }
214     }
215     $fullSizeTot = sprintf("%.2f", $fullSizeTot / 1000);
216     $incrSizeTot = sprintf("%.2f", $incrSizeTot / 1000);
217     my $now      = timeStamp2(time);
218
219     Header($Lang->{BackupPC__Server_Summary});
220     print eval ("qq{$Lang->{BackupPC_Summary}}");
221
222     Trailer();
223 }
224
225 sub Action_StartStopBackup
226 {
227     my($str, $reply);
228
229     my $start = 1 if ( $In{action} eq $Lang->{Start_Incr_Backup}
230                        || $In{action} eq $Lang->{Start_Full_Backup} );
231     my $doFull = $In{action} eq $Lang->{Start_Full_Backup} ? 1 : 0;
232     my $type = $doFull ? "full" : "incremental";
233     my $host = $In{host};
234     my $Privileged = CheckPermission($host);
235
236     if ( !$Privileged ) {
237         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_stop_or_start_backups}}"));
238     }
239     ServerConnect();
240
241     if ( $In{doit} ) {
242         if ( $start ) {
243             if ( $Hosts->{$host}{dhcp} ) {
244                 $reply = $bpc->ServerMesg(eval("qq{$Lang->{backup__In_hostIP___host}}"));
245                 $str = eval("qq{$Lang->{Backup_requested_on_DHCP__host}}");
246             } else {
247                 $reply = $bpc->ServerMesg(eval("qq{$Lang->{backup__host__host__User__doFull}}"));
248                 $str = eval("qq{$Lang->{Backup_requested_on__host_by__User}}");
249             }
250         } else {
251             $reply = $bpc->ServerMesg(eval("qq{$Lang->{stop__host__User__In_backoff}}"));
252             $str = eval("qq{$Lang->{Backup_stopped_dequeued_on__host_by__User}}");
253         }
254
255         Header(eval ("qq{$Lang->{BackupPC__Backup_Requested_on__host}}") );
256         print (eval ("qq{$Lang->{REPLY_FROM_SERVER}}"));
257
258         Trailer();
259     } else {
260         if ( $start ) {
261             my $ipAddr = ConfirmIPAddress($host);
262
263             Header($Lang->{BackupPC__Start_Backup_Confirm_on__host});
264             print (eval ("qq{$Lang->{Are_you_sure_start}}"));
265         } else {
266             my $backoff = "";
267             GetStatusInfo("host($host)");
268             if ( $StatusHost{backoffTime} > time ) {
269                 $backoff = sprintf("%.1f",
270                                   ($StatusHost{backoffTime} - time) / 3600);
271             }
272             Header($Lang->{BackupPC__Stop_Backup_Confirm_on__host});
273             print (eval ("qq{$Lang->{Are_you_sure_stop}}"));
274         }
275         Trailer();
276     }
277 }
278
279 sub Action_Queue
280 {
281     my($strBg, $strUser, $strCmd);
282
283     GetStatusInfo("queues");
284     my $Privileged = CheckPermission();
285
286     if ( !$Privileged ) {
287         ErrorExit($Lang->{Only_privileged_users_can_view_queues_});
288     }
289
290     while ( @BgQueue ) {
291         my $req = pop(@BgQueue);
292         my($reqTime) = timeStamp2($req->{reqTime});
293         $strBg .= <<EOF;
294 <tr><td> ${HostLink($req->{host})} </td>
295     <td align="center"> $reqTime </td>
296     <td align="center"> $req->{user} </td></tr>
297 EOF
298     }
299     while ( @UserQueue ) {
300         my $req = pop(@UserQueue);
301         my $reqTime = timeStamp2($req->{reqTime});
302         $strUser .= <<EOF;
303 <tr><td> ${HostLink($req->{host})} </td>
304     <td align="center"> $reqTime </td>
305     <td align="center"> $req->{user} </td></tr>
306 EOF
307     }
308     while ( @CmdQueue ) {
309         my $req = pop(@CmdQueue);
310         my $reqTime = timeStamp2($req->{reqTime});
311         (my $cmd = $req->{cmd}) =~ s/$BinDir\///;
312         $strCmd .= <<EOF;
313 <tr><td> ${HostLink($req->{host})} </td>
314     <td align="center"> $reqTime </td>
315     <td align="center"> $req->{user} </td>
316     <td> $cmd </td></tr>
317 EOF
318     }
319     Header($Lang->{BackupPC__Queue_Summary});
320
321     print ( eval ( "qq{$Lang->{Backup_Queue_Summary}}") );
322
323     Trailer();
324 }
325
326 sub Action_View
327 {
328     my $Privileged = CheckPermission($In{host});
329     my $compress = 0;
330     my $fh;
331     my $host = $In{host};
332     my $num  = $In{num};
333     my $type = $In{type};
334     my $linkHosts = 0;
335     my($file, $comment);
336     my $ext = $num ne "" ? ".$num" : "";
337
338     ErrorExit(eval("qq{$Lang->{Invalid_number__num}}")) if ( $num ne "" && $num !~ /^\d+$/ );
339     if ( $type eq "XferLOG" ) {
340         $file = "$TopDir/pc/$host/SmbLOG$ext";
341         $file = "$TopDir/pc/$host/XferLOG$ext" if ( !-f $file && !-f "$file.z");
342     } elsif ( $type eq "XferLOGbad" ) {
343         $file = "$TopDir/pc/$host/SmbLOG.bad";
344         $file = "$TopDir/pc/$host/XferLOG.bad" if ( !-f $file && !-f "$file.z");
345     } elsif ( $type eq "XferErrbad" ) {
346         $file = "$TopDir/pc/$host/SmbLOG.bad";
347         $file = "$TopDir/pc/$host/XferLOG.bad" if ( !-f $file && !-f "$file.z");
348         $comment = "(Extracting only Errors)";
349     } elsif ( $type eq "XferErr" ) {
350         $file = "$TopDir/pc/$host/SmbLOG$ext";
351         $file = "$TopDir/pc/$host/XferLOG$ext" if ( !-f $file && !-f "$file.z");
352         $comment = "(Extracting only Errors)";
353     } elsif ( $type eq "RestoreLOG" ) {
354         $file = "$TopDir/pc/$host/RestoreLOG$ext";
355     } elsif ( $type eq "RestoreErr" ) {
356         $file = "$TopDir/pc/$host/RestoreLOG$ext";
357         $comment = "(Extracting only Errors)";
358     } elsif ( $host ne "" && $type eq "config" ) {
359         $file = "$TopDir/pc/$host/config.pl";
360     } elsif ( $type eq "docs" ) {
361         $file = "$BinDir/../doc/BackupPC.html";
362         if ( open(LOG, $file) ) {
363             Header($Lang->{BackupPC__Documentation});
364             print while ( <LOG> );
365             close(LOG);
366             Trailer();
367         } else {
368             ErrorExit(eval("qq{$Lang->{Unable_to_open__file__configuration_problem}}"));
369         }
370         return;
371     } elsif ( $type eq "config" ) {
372         $file = "$TopDir/conf/config.pl";
373     } elsif ( $type eq "hosts" ) {
374         $file = "$TopDir/conf/hosts";
375     } elsif ( $host ne "" ) {
376         $file = "$TopDir/pc/$host/LOG$ext";
377     } else {
378         $file = "$TopDir/log/LOG$ext";
379         $linkHosts = 1;
380     }
381     if ( !$Privileged ) {
382         ErrorExit($Lang->{Only_privileged_users_can_view_log_or_config_files});
383     }
384     if ( !-f $file && -f "$file.z" ) {
385         $file .= ".z";
386         $compress = 1;
387     }
388     Header(eval("qq{$Lang->{Backup_PC__Log_File__file}}")  );
389     print( eval ("qq{$Lang->{Log_File__file__comment}}"));
390     if ( defined($fh = BackupPC::FileZIO->open($file, 0, $compress)) ) {
391         my $mtimeStr = $bpc->timeStamp((stat($file))[9], 1);
392
393         print ( eval ("qq{$Lang->{Contents_of_log_file}}"));
394
395         print "<pre>";
396         if ( $type eq "XferErr" || $type eq "XferErrbad"
397                                 || $type eq "RestoreErr" ) {
398             my $skipped;
399             while ( 1 ) {
400                 $_ = $fh->readLine();
401                 if ( $_ eq "" ) {
402                     print(eval ("qq{$Lang->{skipped__skipped_lines}}"));
403                     last;
404                 }
405                 if ( /smb: \\>/
406                         || /^\s*(\d+) \(\s*\d+\.\d kb\/s\) (.*)$/
407                         || /^tar: dumped \d+ files/
408                         || /^added interface/i
409                         || /^restore tar file /i
410                         || /^restore directory /i
411                         || /^tarmode is now/i
412                         || /^Total bytes written/i
413                         || /^Domain=/i
414                         || /^Getting files newer than/i
415                         || /^Output is \/dev\/null/
416                         || /^\([\d\.]* kb\/s\) \(average [\d\.]* kb\/s\)$/
417                         || /^\s+directory \\/
418                         || /^Timezone is/
419                         || /^\.\//
420                             ) {
421                     $skipped++;
422                     next;
423                 }
424                 print(eval("qq{$Lang->{skipped__skipped_lines}}")) if ( $skipped );
425                 $skipped = 0;
426                 print ${EscapeHTML($_)};
427             }
428         } elsif ( $linkHosts ) {
429             while ( 1 ) {
430                 $_ = $fh->readLine();
431                 last if ( $_ eq "" );
432                 my $s = ${EscapeHTML($_)};
433                 $s =~ s/\b([\w-]+)\b/defined($Hosts->{$1})
434                                         ? ${HostLink($1)} : $1/eg;
435                 print $s;
436             }
437         } elsif ( $type eq "config" ) {
438             while ( 1 ) {
439                 $_ = $fh->readLine();
440                 last if ( $_ eq "" );
441                 # remove any passwords and user names
442                 s/(SmbSharePasswd.*=.*['"]).*(['"])/$1$2/ig;
443                 s/(SmbShareUserName.*=.*['"]).*(['"])/$1$2/ig;
444                 s/(ServerMesgSecret.*=.*['"]).*(['"])/$1$2/ig;
445                 print ${EscapeHTML($_)};
446             }
447         } else {
448             while ( 1 ) {
449                 $_ = $fh->readLine();
450                 last if ( $_ eq "" );
451                 print ${EscapeHTML($_)};
452             }
453         }
454         $fh->close();
455     } else {
456         printf( eval("qq{$Lang->{_pre___Can_t_open_log_file__file}}"));
457     }
458     print <<EOF;
459 </pre>
460 EOF
461     Trailer();
462 }
463
464 sub Action_LOGlist
465 {
466     my $Privileged = CheckPermission($In{host});
467
468     if ( !$Privileged ) {
469         ErrorExit($Lang->{Only_privileged_users_can_view_log_files});
470     }
471     my $host = $In{host};
472     my($url0, $hdr, $root, $str);
473     if ( $host ne "" ) {
474         $root = "$TopDir/pc/$host/LOG";
475         $url0 = "&host=$host";
476         $hdr = "for host $host";
477     } else {
478         $root = "$TopDir/log/LOG";
479         $url0 = "";
480         $hdr = "";
481     }
482     for ( my $i = -1 ; ; $i++ ) {
483         my $url1 = "";
484         my $file = $root;
485         if ( $i >= 0 ) {
486             $file .= ".$i";
487             $url1 = "&num=$i";
488         }
489         $file .= ".z" if ( !-f $file && -f "$file.z" );
490         last if ( !-f $file );
491         my $mtimeStr = $bpc->timeStamp((stat($file))[9], 1);
492         my $size     = (stat($file))[7];
493         $str .= <<EOF;
494 <tr><td> <a href="$MyURL?action=view&type=LOG$url0$url1"><tt>$file</tt></a> </td>
495     <td align="right"> $size </td>
496     <td> $mtimeStr </td></tr>
497 EOF
498     }
499     Header($Lang->{BackupPC__Log_File_History});
500     print (eval("qq{$Lang->{Log_File_History__hdr}}"));
501     Trailer();
502 }
503
504 sub Action_EmailSummary
505 {
506     my $Privileged = CheckPermission();
507
508     if ( !$Privileged ) {
509         ErrorExit($Lang->{Only_privileged_users_can_view_email_summaries});
510     }
511     GetStatusInfo("hosts");
512     ReadUserEmailInfo();
513     my(%EmailStr, $str);
514     foreach my $u ( keys(%UserEmailInfo) ) {
515         next if ( !defined($UserEmailInfo{$u}{lastTime}) );
516         my $emailTimeStr = timeStamp2($UserEmailInfo{$u}{lastTime});
517         $EmailStr{$UserEmailInfo{$u}{lastTime}} .= <<EOF;
518 <tr><td>${UserLink($u)} </td>
519     <td>${HostLink($UserEmailInfo{$u}{lastHost})} </td>
520     <td>$emailTimeStr </td>
521     <td>$UserEmailInfo{$u}{lastSubj} </td></tr>
522 EOF
523     }
524     foreach my $t ( sort({$b <=> $a} keys(%EmailStr)) ) {
525         $str .= $EmailStr{$t};
526     }
527     Header($Lang->{Email_Summary});
528     print (eval("qq{$Lang->{Recent_Email_Summary}}"));
529     Trailer();
530 }
531
532 sub Action_Browse
533 {
534     my $Privileged = CheckPermission($In{host});
535     my($i, $dirStr, $fileStr, $mangle);
536     my($numF, $compressF, $mangleF, $fullDirF);
537     my $checkBoxCnt = 0;      # checkbox counter
538
539     if ( !$Privileged ) {
540         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_browse_backup_files}}"));
541     }
542     my $host = $In{host};
543     my $num  = $In{num};
544     my $dir  = $In{dir};
545     if ( $host eq "" ) {
546         ErrorExit($Lang->{Empty_host_name});
547     }
548     #
549     # Find the requested backup and the previous filled backup
550     #
551     my @Backups = $bpc->BackupInfoRead($host);
552     for ( $i = 0 ; $i < @Backups ; $i++ ) {
553         if ( !$Backups[$i]{noFill} ) {
554             $numF      = $Backups[$i]{num};
555             $mangleF   = $Backups[$i]{mangle};
556             $compressF = $Backups[$i]{compress};
557         }
558         last if ( $Backups[$i]{num} == $num );
559     }
560     if ( $i >= @Backups ) {
561         ErrorExit("Backup number $num for host ${EscapeHTML($host)} does"
562                 . " not exist.");
563     }
564     if ( !$Backups[$i]{noFill} ) {
565         # no need to back-fill a filled backup
566         $numF = $mangleF = $compressF = undef;
567     }
568     my $backupTime = timeStamp2($Backups[$i]{startTime});
569     my $backupAge = sprintf("%.1f", (time - $Backups[$i]{startTime})
570                                     / (24 * 3600));
571     $mangle = $Backups[$i]{mangle};
572     if ( $dir eq "" || $dir eq "." || $dir eq ".." ) {
573         if ( !opendir(DIR, "$TopDir/pc/$host/$num") ) {
574             ErrorExit(eval("qq{$Lang->{Can_t_browse_bad_directory_name}}"));
575         }
576         #
577         # Read this directory and find the first directory
578         #
579         foreach my $f ( readdir(DIR) ) {
580             next if ( $f eq "." || $f eq ".." );
581             if ( -d "$TopDir/pc/$host/$num/$f" ) {
582                 $dir = "/$f";
583                 last;
584             }
585         }
586         closedir(DIR);
587         if ( $dir eq "" || $dir eq "." || $dir eq ".." ) {
588             ErrorExit(eval("qq{$Lang->{Directory___EscapeHTML}}"));
589         }
590     }
591     my $relDir   = $dir;
592     my $fullDir  = "$TopDir/pc/$host/$num/$relDir";
593     if ( defined($numF) ) {
594         # get full path to filled backup
595         if ( $mangle && !$mangleF ) {
596             $fullDirF = "$TopDir/pc/$host/$numF/"
597                                 . $bpc->fileNameUnmangle($relDir);
598         } else {
599             $fullDirF = "$TopDir/pc/$host/$numF/$relDir";
600         }
601     }
602     my $currDir    = undef;
603     #
604     # Read attributes for the directory and optionally for the filled backup
605     #
606     my $attr  = BackupPC::Attrib->new({ compress => $Backups[$i]{compress}});
607     my $attrF = BackupPC::Attrib->new({ compress => $compressF})
608                                                     if ( defined($numF) );
609     $attr->read($fullDir) if ( -f $attr->fileName($fullDir) );
610     if ( defined($numF) && -f $attrF->fileName($fullDirF)
611                         && $attrF->read($fullDirF) ) {
612         $attr->merge($attrF);
613     }
614     #
615     # Loop up the directory tree until we hit the top.
616     #
617     my(@DirStrPrev);
618     while ( 1 ) {
619         my($fLast, $fum, $fLastum, @DirStr);
620
621         if ( $fullDir =~ m{(^|/)\.\.(/|$)} || !opendir(DIR, $fullDir) ) {
622             ErrorExit(eval("qq{$Lang->{Can_t_browse_bad_directory_name2}}"));
623         }
624         #
625         # Read this directory and optionally the corresponding filled directory
626         #
627         my @Dir = readdir(DIR);
628         closedir(DIR);
629         if ( defined($numF) && opendir(DIR, $fullDirF) ) {
630             if ( $mangle == $mangleF ) {
631                 @Dir = (@Dir, readdir(DIR));
632             } else {
633                 foreach my $f ( readdir(DIR) ) {
634                     next if ( $f eq "." || $f eq ".." );
635                     push(@Dir, $bpc->fileNameMangle($f));
636                 }
637             }
638             closedir(DIR);
639         }
640         my $fileCnt = 0;          # file counter
641         $fLast = $dirStr = "";
642         #
643         # Loop over each of the files in this directory
644         #
645         my(@DirUniq);
646         foreach my $f ( sort({uc($a) cmp uc($b)} @Dir) ) {
647             next if ( $f eq "." || $f eq ".."
648                    || $f eq $fLast || ($mangle && $f eq "attrib") );
649             $fLast = $f;
650             push(@DirUniq, $f);
651         }
652         while ( defined(my $f = shift(@DirUniq)) ) {
653             my $path = "$relDir/$f";
654             my($dirOpen, $gotDir, $imgStr, $img);
655             $fum = $mangle ? $bpc->fileNameUnmangle($f) : $f;  # unmangled $f
656             my $fumURI = $fum;                                 # URI escaped $f
657             $path =~ s{^/+}{/};
658             $path =~ s/([^\w.\/-])/uc sprintf("%%%02x", ord($1))/eg;
659             $fumURI =~ s/([^\w.\/-])/uc sprintf("%%%02x", ord($1))/eg;
660             $dirOpen = 1 if ( defined($currDir) && $f eq $currDir );
661             if ( -d "$fullDir/$f" ) {
662                 #
663                 # Display directory if it exists in current backup.
664                 # First find out if there are subdirs
665                 #
666                 my @s = (defined($numF) && -d "$fullDirF/$f")
667                             ? stat("$fullDirF/$f")
668                             : stat("$fullDir/$f");
669                 my($bold, $unbold, $BGcolor);
670                 $img |= 1 << 6;
671                 $img |= 1 << 5 if ( $s[3] > 2 );
672                 if ( $dirOpen ) {
673                     $bold = "<b>";
674                     $unbold = "</b>";
675                     $img |= 1 << 2;
676                     $img |= 1 << 3 if ( $s[3] > 2 );
677                 }
678                 my $imgFileName = sprintf("%07b.gif", $img);
679                 $imgStr = "<img src=\"$Conf{CgiImageDirURL}/$imgFileName\" align=\"absmiddle\" width=\"9\" height=\"19\" border=\"0\">";
680                 if ( "$relDir/$f" eq $dir ) {
681                     $BGcolor = " bgcolor=\"$Conf{CgiHeaderBgColor}\"";
682                 } else {
683                     $BGcolor = "";
684                 }
685                 my $dirName = $fum;
686                 $dirName =~ s/ /&nbsp;/g;
687                 push(@DirStr, {needTick => 1,
688                                tdArgs   => $BGcolor,
689                                link     => <<EOF});
690 <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>
691 EOF
692                 $fileCnt++;
693                 $gotDir = 1;
694                 if ( $dirOpen ) {
695                     my($lastTick, $doneLastTick);
696                     foreach my $d ( @DirStrPrev ) {
697                         $lastTick = $d if ( $d->{needTick} );
698                     }
699                     $doneLastTick = 1 if ( !defined($lastTick) );
700                     foreach my $d ( @DirStrPrev ) {
701                         $img = 0;
702                         if  ( $d->{needTick} ) {
703                             $img |= 1 << 0;
704                         }
705                         if ( $d == $lastTick ) {
706                             $img |= 1 << 4;
707                             $doneLastTick = 1;
708                         } elsif ( !$doneLastTick ) {
709                             $img |= 1 << 3 | 1 << 4;
710                         }
711                         my $imgFileName = sprintf("%07b.gif", $img);
712                         $imgStr = "<img src=\"$Conf{CgiImageDirURL}/$imgFileName\" align=\"absmiddle\" width=\"9\" height=\"19\" border=\"0\">";
713                         push(@DirStr, {needTick => 0,
714                                        tdArgs   => $d->{tdArgs},
715                                        link     => $imgStr . $d->{link}
716                         });
717                     }
718                 }
719             }
720             if ( $relDir eq $dir ) {
721                 #
722                 # This is the selected directory, so display all the files
723                 #
724                 my $attrStr;
725                 if ( defined($a = $attr->get($fum)) ) {
726                     my $mtimeStr = $bpc->timeStamp($a->{mtime});
727                     my $typeStr  = $attr->fileType2Text($a->{type});
728                     my $modeStr  = sprintf("0%o", $a->{mode} & 07777);
729                     $attrStr .= <<EOF;
730     <td align="center">$typeStr</td>
731     <td align="right">$modeStr</td>
732     <td align="right">$a->{size}</td>
733     <td align="right">$mtimeStr</td>
734 </tr>
735 EOF
736                 } else {
737                     $attrStr .= "<td colspan=\"4\" align=\"center\"> </td>\n";
738                 }
739                 if ( $gotDir ) {
740                     $fileStr .= <<EOF;
741 <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>
742 $attrStr
743 </tr>
744 EOF
745                 } else {
746                     $fileStr .= <<EOF;
747 <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>
748 $attrStr
749 </tr>
750 EOF
751                 }
752                 $checkBoxCnt++;
753             }
754         }
755         @DirStrPrev = @DirStr;
756         last if ( $relDir eq "" );
757         # 
758         # Prune the last directory off $relDir
759         #
760         $relDir =~ s/(.*)\/(.*)/$1/;
761         $currDir = $2;
762         $fullDir = "$TopDir/pc/$host/$num/$relDir";
763         $fullDirF = "$TopDir/pc/$host/$numF/$relDir" if ( defined($numF) );
764     }
765     my $dirDisplay = $mangle ? $bpc->fileNameUnmangle($dir) : $dir;
766     $dirDisplay =~ s{//}{/}g;
767     my $filledBackup;
768     if ( defined($numF) ) {
769         $filledBackup = eval("qq{$Lang->{This_display_is_merged_with_backup}}");
770     }
771     Header(eval("qq{$Lang->{Browse_backup__num_for__host}}"));
772
773     foreach my $d ( @DirStrPrev ) {
774         $dirStr .= "<tr><td$d->{tdArgs}>$d->{link}\n";
775     }
776
777     ### hide checkall button if there are no files
778     my ($topCheckAll, $checkAll, $fileHeader);
779     if ( $fileStr ) {
780         $fileHeader = eval("qq{$Lang->{fileHeader}}");
781
782         $checkAll = $Lang->{checkAll};
783
784         # and put a checkall box on top if there are at least 20 files
785         if ( $checkBoxCnt >= 20 ) {
786             $topCheckAll = $checkAll;
787             $topCheckAll =~ s{allFiles}{allFilestop}g;
788         }
789     } else {
790         $fileStr = eval("qq{$Lang->{The_directory_is_empty}}");
791     }
792  
793     print (eval("qq{$Lang->{Backup_browse_for__host}}"));
794     Trailer();
795 }
796
797 sub Action_Restore
798 {
799     my($str, $reply, $i);
800     my $Privileged = CheckPermission($In{host});
801     if ( !$Privileged ) {
802         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_restore_backup_files}}"));
803     }
804     my $host = $In{host};
805     my $num  = $In{num};
806     my(@fileList, $fileListStr, $hiddenStr, $share, $pathHdr, $badFileCnt);
807     my @Backups = $bpc->BackupInfoRead($host);
808     for ( $i = 0 ; $i < @Backups ; $i++ ) {
809         last if ( $Backups[$i]{num} == $num );
810     }
811     my $mangle = $Backups[$i]{mangle};
812     ServerConnect();
813
814     if ( !defined($Hosts->{$host}) ) {
815         ErrorExit(eval("qq{$Lang->{Bad_host_name}}"));
816     }
817     for ( my $i = 0 ; $i < $In{fcbMax} ; $i++ ) {
818         next if ( !defined($In{"fcb$i"}) );
819         (my $name = $In{"fcb$i"}) =~ s/%([0-9A-F]{2})/chr(hex($1))/eg;
820         $badFileCnt++ if ( $name =~ m{(^|/)\.\.(/|$)} );
821         if ( $name =~ m{^/+(.*?)(/.*)} ) {
822             $share = $1;
823             $name  = $mangle ? $bpc->fileNameUnmangle($2) : $2;
824             if ( @fileList == 0 ) {
825                 $pathHdr = $name;
826             } else {
827                 while ( substr($name, 0, length($pathHdr)) ne $pathHdr ) {
828                     $pathHdr = substr($pathHdr, 0, rindex($pathHdr, "/"));
829                 }
830             }
831         }
832         push(@fileList, $name);
833         $share = $mangle ? $bpc->fileNameUnmangle($share) : $share;
834         $hiddenStr .= <<EOF;
835 <input type="hidden" name="fcb$i" value="$In{'fcb' . $i}">
836 EOF
837         $fileListStr .= <<EOF;
838 <li> ${EscapeHTML($name)}
839 EOF
840     }
841     $hiddenStr .= "<input type=\"hidden\" name=\"fcbMax\" value=\"$In{fcbMax}\">\n";
842     $badFileCnt++ if ( $In{pathHdr} =~ m{(^|/)\.\.(/|$)} );
843     $badFileCnt++ if ( $In{num} =~ m{(^|/)\.\.(/|$)} );
844     if ( @fileList == 0 ) {
845         ErrorExit($Lang->{You_haven_t_selected_any_files__please_go_Back_to});
846     }
847     if ( $badFileCnt ) {
848         ErrorExit($Lang->{Nice_try__but_you_can_t_put});
849     }
850     if ( @fileList == 1 ) {
851         $pathHdr =~ s/(.*)\/.*/$1/;
852     }
853     $pathHdr = "/" if ( $pathHdr eq "" );
854     if ( $In{type} != 0 && @fileList == $In{fcbMax} ) {
855         #
856         # All the files in the list were selected, so just restore the
857         # entire parent directory
858         #
859         @fileList = ( $pathHdr );
860     }
861     if ( $In{type} == 0 ) {
862         #
863         # Tell the user what options they have
864         #
865         Header(eval("qq{$Lang->{Restore_Options_for__host}}"));
866         print(eval("qq{$Lang->{Restore_Options_for__host2}}"));
867
868         #
869         # Verify that Archive::Zip is available before showing the
870         # zip restore option
871         #
872         if ( eval { require Archive::Zip } ) {
873             print (eval("qq{$Lang->{Option_2__Download_Zip_archive}}"));
874         } else {
875             print (eval("qq{$Lang->{Option_2__Download_Zip_archive2}}"));
876         }
877         print (eval("qq{$Lang->{Option_3__Download_Zip_archive}}"));
878         Trailer();
879     } elsif ( $In{type} == 1 ) {
880         #
881         # Provide the selected files via a tar archive.
882         #
883         $SIG{CHLD} = 'IGNORE';
884         my $pid = fork();
885         if ( !defined($pid) ) {
886             $bpc->ServerMesg(eval("qq{$Lang->{log_Can_t_fork_for_tar_restore_request_by__User}}"));
887             ErrorExit($Lang->{Can_t_fork_for_tar_restore});
888         }
889         if ( $pid ) {
890             #
891             # This is the parent.
892             #
893             my @fileListTrim = @fileList;
894             if ( @fileListTrim > 10 ) {
895                 @fileListTrim = (@fileListTrim[0..9], '...');
896             }
897             $bpc->ServerMesg(eval("qq{$Lang->{log_User__User_downloaded_tar_archive_for__host}}"));
898             return;
899         }
900         #
901         # This is the child.  Print the headers and run BackupPC_tarCreate.
902         #
903         my @pathOpts;
904         if ( $In{relative} ) {
905             @pathOpts = ("-r", $pathHdr, "-p", "");
906         }
907         $bpc->ServerDisconnect();
908         print "Content-Type: application/x-gtar\n";
909         print "Content-Transfer-Encoding: binary\n";
910         print "Content-Disposition: attachment; filename=\"restore.tar\"\n\n";
911         exec("$BinDir/BackupPC_tarCreate",
912              "-h", $host,
913              "-n", $num,
914              "-s", $share,
915              @pathOpts,
916              @fileList
917         );
918     } elsif ( $In{type} == 2 ) {
919         #
920         # Provide the selected files via a zip archive.
921         #
922         $SIG{CHLD} = 'IGNORE';
923         my $pid = fork();
924         if ( !defined($pid) ) {
925             $bpc->ServerMesg(eval("qq{$Lang->{log_Can_t_fork_for_zip_restore_request_by__User}}"));
926             ErrorExit($Lang->{Can_t_fork_for_zip_restore});
927         }
928         if ( $pid ) {
929             #
930             # This is the parent.
931             #
932             my @fileListTrim = @fileList;
933             if ( @fileListTrim > 10 ) {
934                 @fileListTrim = (@fileListTrim[0..9], '...');
935             }
936             $bpc->ServerMesg(eval("qq{$Lang->{log_User__User_downloaded_zip_archive_for__host}}"));
937             return;
938         }
939         #
940         # This is the child.  Print the headers and run BackupPC_tarCreate.
941         #
942         my @pathOpts;
943         if ( $In{relative} ) {
944             @pathOpts = ("-r", $pathHdr, "-p", "");
945         }
946         $bpc->ServerDisconnect();
947         print "Content-Type: application/zip\n";
948         print "Content-Transfer-Encoding: binary\n";
949         print "Content-Disposition: attachment; filename=\"restore.zip\"\n\n";
950         $In{compressLevel} = 5 if ( $In{compressLevel} !~ /^\d+$/ );
951         exec("$BinDir/BackupPC_zipCreate",
952              "-h", $host,
953              "-n", $num,
954              "-c", $In{compressLevel},
955              "-s", $share,
956              @pathOpts,
957              @fileList
958         );
959     } elsif ( $In{type} == 3 ) {
960         #
961         # Do restore directly onto host
962         #
963         if ( !defined($Hosts->{$In{hostDest}}) ) {
964             ErrorExit(eval("qq{$Lang->{Host__doesn_t_exist}}"));
965         }
966         if ( !CheckPermission($In{hostDest}) ) {
967             ErrorExit(eval("qq{$Lang->{You_don_t_have_permission_to_restore_onto_host}}"));
968         }
969         $fileListStr = "";
970         foreach my $f ( @fileList ) {
971             my $targetFile = $f;
972             (my $strippedShare = $share) =~ s/^\///;
973             (my $strippedShareDest = $In{shareDest}) =~ s/^\///;
974             substr($targetFile, 0, length($pathHdr)) = $In{pathHdr};
975             $fileListStr .= <<EOF;
976 <tr><td>$host:/$strippedShare$f</td><td>$In{hostDest}:/$strippedShareDest$targetFile</td></tr>
977 EOF
978         }
979         Header(eval("qq{$Lang->{Restore_Confirm_on__host}}"));
980         print(eval("qq{$Lang->{Are_you_sure}}"));
981         Trailer();
982     } elsif ( $In{type} == 4 ) {
983         if ( !defined($Hosts->{$In{hostDest}}) ) {
984             ErrorExit(eval("qq{$Lang->{Host_doesn_t_exist}}"));
985         }
986         if ( !CheckPermission($In{hostDest}) ) {
987             ErrorExit(eval("qq{$Lang->{You_don_t_have_permission_to_restore_onto_host}}"));
988         }
989         my $hostDest = $1 if ( $In{hostDest} =~ /(.+)/ );
990         my $ipAddr = ConfirmIPAddress($hostDest);
991         #
992         # Prepare and send the restore request.  We write the request
993         # information using Data::Dumper to a unique file,
994         # $TopDir/pc/$hostDest/restoreReq.$$.n.  We use a file
995         # in case the list of files to restore is very long.
996         #
997         my $reqFileName;
998         for ( my $i = 0 ; ; $i++ ) {
999             $reqFileName = "restoreReq.$$.$i";
1000             last if ( !-f "$TopDir/pc/$hostDest/$reqFileName" );
1001         }
1002         my %restoreReq = (
1003             # source of restore is hostSrc, #num, path shareSrc/pathHdrSrc
1004             num         => $In{num},
1005             hostSrc     => $host,
1006             shareSrc    => $share,
1007             pathHdrSrc  => $pathHdr,
1008
1009             # destination of restore is hostDest:shareDest/pathHdrDest
1010             hostDest    => $hostDest,
1011             shareDest   => $In{shareDest},
1012             pathHdrDest => $In{pathHdr},
1013
1014             # list of files to restore
1015             fileList    => \@fileList,
1016
1017             # other info
1018             user        => $User,
1019             reqTime     => time,
1020         );
1021         my($dump) = Data::Dumper->new(
1022                          [  \%restoreReq],
1023                          [qw(*RestoreReq)]);
1024         $dump->Indent(1);
1025         if ( open(REQ, ">$TopDir/pc/$hostDest/$reqFileName") ) {
1026             print(REQ $dump->Dump);
1027             close(REQ);
1028         } else {
1029             ErrorExit(eval("qq{$Lang->{Can_t_open_create}}"));
1030         }
1031         $reply = $bpc->ServerMesg(eval("qq{$Lang->{restore__ipAddr}}"));
1032         $str = eval("qq{$Lang->{Restore_requested_to_host__hostDest__backup___num}}");
1033         Header(eval("qq{$Lang->{Restore_Requested_on__hostDest}}"));
1034         print (eval("qq{$Lang->{Reply_from_server_was___reply}}"));
1035         Trailer();
1036     }
1037 }
1038
1039 sub Action_RestoreFile
1040 {
1041     restoreFile($In{host}, $In{num}, $In{dir});
1042 }
1043
1044 sub restoreFile
1045 {
1046     my($host, $num, $dir, $skipHardLink, $origName) = @_;
1047     my($Privileged) = CheckPermission($host);
1048     my($i, $numF, $mangleF, $compressF, $mangle, $compress, $dirUM);
1049     #
1050     # Some common content (media) types from www.iana.org (via MIME::Types).
1051     #
1052     my $Ext2ContentType = {
1053         'asc'  => 'text/plain',
1054         'avi'  => 'video/x-msvideo',
1055         'bmp'  => 'image/bmp',
1056         'book' => 'application/x-maker',
1057         'cc'   => 'text/plain',
1058         'cpp'  => 'text/plain',
1059         'csh'  => 'application/x-csh',
1060         'csv'  => 'text/comma-separated-values',
1061         'c'    => 'text/plain',
1062         'deb'  => 'application/x-debian-package',
1063         'doc'  => 'application/msword',
1064         'dot'  => 'application/msword',
1065         'dtd'  => 'text/xml',
1066         'dvi'  => 'application/x-dvi',
1067         'eps'  => 'application/postscript',
1068         'fb'   => 'application/x-maker',
1069         'fbdoc'=> 'application/x-maker',
1070         'fm'   => 'application/x-maker',
1071         'frame'=> 'application/x-maker',
1072         'frm'  => 'application/x-maker',
1073         'gif'  => 'image/gif',
1074         'gtar' => 'application/x-gtar',
1075         'gz'   => 'application/x-gzip',
1076         'hh'   => 'text/plain',
1077         'hpp'  => 'text/plain',
1078         'h'    => 'text/plain',
1079         'html' => 'text/html',
1080         'htmlx'=> 'text/html',
1081         'htm'  => 'text/html',
1082         'iges' => 'model/iges',
1083         'igs'  => 'model/iges',
1084         'jpeg' => 'image/jpeg',
1085         'jpe'  => 'image/jpeg',
1086         'jpg'  => 'image/jpeg',
1087         'js'   => 'application/x-javascript',
1088         'latex'=> 'application/x-latex',
1089         'maker'=> 'application/x-maker',
1090         'mid'  => 'audio/midi',
1091         'midi' => 'audio/midi',
1092         'movie'=> 'video/x-sgi-movie',
1093         'mov'  => 'video/quicktime',
1094         'mp2'  => 'audio/mpeg',
1095         'mp3'  => 'audio/mpeg',
1096         'mpeg' => 'video/mpeg',
1097         'mpg'  => 'video/mpeg',
1098         'mpp'  => 'application/vnd.ms-project',
1099         'pdf'  => 'application/pdf',
1100         'pgp'  => 'application/pgp-signature',
1101         'php'  => 'application/x-httpd-php',
1102         'pht'  => 'application/x-httpd-php',
1103         'phtml'=> 'application/x-httpd-php',
1104         'png'  => 'image/png',
1105         'ppm'  => 'image/x-portable-pixmap',
1106         'ppt'  => 'application/powerpoint',
1107         'ppt'  => 'application/vnd.ms-powerpoint',
1108         'ps'   => 'application/postscript',
1109         'qt'   => 'video/quicktime',
1110         'rgb'  => 'image/x-rgb',
1111         'rtf'  => 'application/rtf',
1112         'rtf'  => 'text/rtf',
1113         'shar' => 'application/x-shar',
1114         'shtml'=> 'text/html',
1115         'swf'  => 'application/x-shockwave-flash',
1116         'tex'  => 'application/x-tex',
1117         'texi' => 'application/x-texinfo',
1118         'texinfo'=> 'application/x-texinfo',
1119         'tgz'  => 'application/x-gtar',
1120         'tiff' => 'image/tiff',
1121         'tif'  => 'image/tiff',
1122         'txt'  => 'text/plain',
1123         'vcf'  => 'text/x-vCard',
1124         'vrml' => 'model/vrml',
1125         'wav'  => 'audio/x-wav',
1126         'wmls' => 'text/vnd.wap.wmlscript',
1127         'wml'  => 'text/vnd.wap.wml',
1128         'wrl'  => 'model/vrml',
1129         'xls'  => 'application/vnd.ms-excel',
1130         'xml'  => 'text/xml',
1131         'xwd'  => 'image/x-xwindowdump',
1132         'z'    => 'application/x-compress',
1133         'zip'  => 'application/zip',
1134     };
1135
1136     if ( !$Privileged ) {
1137         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_restore_backup_files2}}"));
1138     }
1139     ServerConnect();
1140     my @Backups = $bpc->BackupInfoRead($host);
1141     if ( $host eq "" ) {
1142         ErrorExit($Lang->{Empty_host_name});
1143     }
1144     $dir = "/" if ( $dir eq "" );
1145     for ( $i = 0 ; $i < @Backups ; $i++ ) {
1146         if ( !$Backups[$i]{noFill} ) {
1147             $numF      = $Backups[$i]{num};
1148             $mangleF   = $Backups[$i]{mangle};
1149             $compressF = $Backups[$i]{compress};
1150         }
1151         last if ( $Backups[$i]{num} == $num );
1152     }
1153     $mangle = $Backups[$i]{mangle};
1154     $compress = $Backups[$i]{compress};
1155     if ( !$Backups[$i]{noFill} ) {
1156         # no need to back-fill a filled backup
1157         $numF = $mangleF = $compressF = undef;
1158     }
1159     my $fullPath = "$TopDir/pc/$host/$num/$dir";
1160     $fullPath =~ s{/+}{/}g;
1161     if ( !-f $fullPath && defined($numF) ) {
1162         my $dirF = $dir;
1163         my $fullPathF;
1164         if ( $mangle && !$mangleF ) {
1165             $fullPathF = "$TopDir/pc/$host/$numF/"
1166                                 . $bpc->fileNameUnmangle($dir);
1167         } else {
1168             $fullPathF = "$TopDir/pc/$host/$numF/$dir";
1169         }
1170         if ( -f $fullPathF ) {
1171             $fullPath = $fullPathF;
1172             $compress = $compressF;
1173         }
1174     }
1175     if ( $fullPath =~ m{(^|/)\.\.(/|$)} || !-f $fullPath ) {
1176         ErrorExit(eval("qq{$Lang->{Can_t_restore_bad_file}}"));
1177     }
1178     my $dirUM = $mangle ? $bpc->fileNameUnmangle($dir) : $dir;
1179     my $attr = BackupPC::Attrib->new({compress => $compress});
1180     my $fullDir = $fullPath;
1181     $fullDir =~ s{(.*)/.*}{$1};
1182     my $fileName = $1 if ( $dirUM =~ /.*\/(.*)/ );
1183     $attr->read($fullDir) if ( -f $attr->fileName($fullDir) );
1184     my $a = $attr->get($fileName);
1185
1186     my $f = BackupPC::FileZIO->open($fullPath, 0, $compress);
1187     my $data;
1188     if ( !$skipHardLink && $a->{type} == BPC_FTYPE_HARDLINK ) {
1189         #
1190         # hardlinks should look like the file they point to
1191         #
1192         my $linkName;
1193         while ( $f->read(\$data, 65536) > 0 ) {
1194             $linkName .= $data;
1195         }
1196         $f->close;
1197         $linkName =~ s/^\.\///;
1198         my $share = $1 if ( $dir =~ /^\/?(.*?)\// );
1199         restoreFile($host, $num,
1200                 "$share/" . ($mangle ? $bpc->fileNameMangle($linkName)
1201                                      : $linkName), 1, $dir);
1202         return;
1203     }
1204     $dirUM =~ s{//}{/}g;
1205     $fullPath =~ s{//}{/}g;
1206     $bpc->ServerMesg("log User $User recovered file $dirUM ($fullPath)");
1207     $dir = $origName if ( defined($origName) );
1208     $dirUM = $mangle ? $bpc->fileNameUnmangle($dir) : $dir;
1209     my $ext = $1 if ( $dirUM =~ /\.([^\/\.]+)$/ );
1210     my $contentType = $Ext2ContentType->{lc($ext)}
1211                                     || "application/octet-stream";
1212     $fileName = $1 if ( $dirUM =~ /.*\/(.*)/ );
1213     $fileName =~ s/"/\\"/g;
1214     print "Content-Type: $contentType\n";
1215     print "Content-Transfer-Encoding: binary\n";
1216     print "Content-Disposition: attachment; filename=\"$fileName\"\n\n";
1217     while ( $f->read(\$data, 1024 * 1024) > 0 ) {
1218         print STDOUT $data;
1219     }
1220     $f->close;
1221 }
1222
1223 sub Action_HostInfo
1224 {
1225     my $host = $1 if ( $In{host} =~ /(.*)/ );
1226     my($statusStr, $startIncrStr);
1227
1228     $host =~ s/^\s+//;
1229     $host =~ s/\s+$//;
1230     return Action_GeneralInfo() if ( $host eq "" );
1231     $host = lc($host)
1232                 if ( !-d "$TopDir/pc/$host" && -d "$TopDir/pc/" . lc($host) );
1233     if ( $host =~ /\.\./ || !-d "$TopDir/pc/$host" ) {
1234         #
1235         # try to lookup by user name
1236         #
1237         if ( !defined($Hosts->{$host}) ) {
1238             foreach my $h ( keys(%$Hosts) ) {
1239                 if ( $Hosts->{$h}{user} eq $host
1240                         || lc($Hosts->{$h}{user}) eq lc($host) ) {
1241                     $host = $h;
1242                     last;
1243                 }
1244             }
1245             CheckPermission();
1246             ErrorExit(eval("qq{$Lang->{Unknown_host_or_user}}"))
1247                                 if ( !defined($Hosts->{$host}) );
1248         }
1249         $In{host} = $host;
1250     }
1251     GetStatusInfo("host($host)");
1252     $bpc->ConfigRead($host);
1253     %Conf = $bpc->Conf();
1254     my $Privileged = CheckPermission($host);
1255     if ( !$Privileged ) {
1256         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_view_information_about}}"));
1257     }
1258     ReadUserEmailInfo();
1259
1260     my @Backups = $bpc->BackupInfoRead($host);
1261     my($str, $sizeStr, $compStr, $errStr, $warnStr);
1262     for ( my $i = 0 ; $i < @Backups ; $i++ ) {
1263         my $startTime = timeStamp2($Backups[$i]{startTime});
1264         my $dur       = $Backups[$i]{endTime} - $Backups[$i]{startTime};
1265         $dur          = 1 if ( $dur <= 0 );
1266         my $duration  = sprintf("%.1f", $dur / 60);
1267         my $MB        = sprintf("%.1f", $Backups[$i]{size} / (1024*1024));
1268         my $MBperSec  = sprintf("%.2f", $Backups[$i]{size} / (1024*1024*$dur));
1269         my $MBExist   = sprintf("%.1f", $Backups[$i]{sizeExist} / (1024*1024));
1270         my $MBNew     = sprintf("%.1f", $Backups[$i]{sizeNew} / (1024*1024));
1271         my($MBExistComp, $ExistComp, $MBNewComp, $NewComp);
1272         if ( $Backups[$i]{sizeExist} && $Backups[$i]{sizeExistComp} ) {
1273             $MBExistComp = sprintf("%.1f", $Backups[$i]{sizeExistComp}
1274                                                 / (1024 * 1024));
1275             $ExistComp = sprintf("%.1f%%", 100 *
1276                   (1 - $Backups[$i]{sizeExistComp} / $Backups[$i]{sizeExist}));
1277         }
1278         if ( $Backups[$i]{sizeNew} && $Backups[$i]{sizeNewComp} ) {
1279             $MBNewComp = sprintf("%.1f", $Backups[$i]{sizeNewComp}
1280                                                 / (1024 * 1024));
1281             $NewComp = sprintf("%.1f%%", 100 *
1282                   (1 - $Backups[$i]{sizeNewComp} / $Backups[$i]{sizeNew}));
1283         }
1284         my $age = sprintf("%.1f", (time - $Backups[$i]{startTime}) / (24*3600));
1285         my $browseURL = "$MyURL?action=browse&host=$host&num=$Backups[$i]{num}";
1286         my $filled = $Backups[$i]{noFill} ? $Lang->{No} : $Lang->{Yes};
1287         $filled .= " ($Backups[$i]{fillFromNum}) "
1288                             if ( $Backups[$i]{fillFromNum} ne "" );
1289         my $ltype;
1290         if ($Backups[$i]{type} eq "full") { $ltype = $Lang->{full}; }
1291         else { $ltype = $Lang->{incremental}; }
1292         $str .= <<EOF;
1293 <tr><td align="center"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
1294     <td align="center"> $ltype </td>
1295     <td align="center"> $filled </td>
1296     <td align="right">  $startTime </td>
1297     <td align="right">  $duration </td>
1298     <td align="right">  $age </td>
1299     <td align="left">   <tt>$TopDir/pc/$host/$Backups[$i]{num}</tt> </td></tr>
1300 EOF
1301         $sizeStr .= <<EOF;
1302 <tr><td align="center"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
1303     <td align="center"> $ltype </td>
1304     <td align="right">  $Backups[$i]{nFiles} </td>
1305     <td align="right">  $MB </td>
1306     <td align="right">  $MBperSec </td>
1307     <td align="right">  $Backups[$i]{nFilesExist} </td>
1308     <td align="right">  $MBExist </td>
1309     <td align="right">  $Backups[$i]{nFilesNew} </td>
1310     <td align="right">  $MBNew </td>
1311 </tr>
1312 EOF
1313         $Backups[$i]{compress} ||= "off";
1314         my $is_compress = $Lang->{off};
1315         if ($Backups[$i]{compress} ne "off") {$is_compress = $Lang->{on}; }
1316         if (! $ExistComp) { $ExistComp = "&nbsp;"; }
1317         if (! $MBExistComp) { $MBExistComp = "&nbsp;"; }
1318         $compStr .= <<EOF;
1319 <tr><td align="center"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
1320     <td align="center"> $ltype </td>
1321     <td align="center"> $is_compress </td> 
1322     <td align="right">  $MBExist </td>
1323     <td align="right">  $MBExistComp </td> 
1324     <td align="right">  $ExistComp </td>   
1325     <td align="right">  $MBNew </td>
1326     <td align="right">  $MBNewComp </td>
1327     <td align="right">  $NewComp </td>
1328 </tr>
1329 EOF
1330         $errStr .= <<EOF;
1331 <tr><td align="center"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
1332     <td align="center"> $ltype </td>
1333     <td align="center"> <a href="$MyURL?action=view&type=XferLOG&num=$Backups[$i]{num}&host=$host">XferLOG</a>,
1334                       <a href="$MyURL?action=view&type=XferErr&num=$Backups[$i]{num}&host=$host">Errors</a> </td>
1335     <td align="right">  $Backups[$i]{xferErrs} </td>
1336     <td align="right">  $Backups[$i]{xferBadFile} </td>
1337     <td align="right">  $Backups[$i]{xferBadShare} </td>
1338     <td align="right">  $Backups[$i]{tarErrs} </td></tr>
1339 EOF
1340     }
1341
1342     my @Restores = $bpc->RestoreInfoRead($host);
1343     my $restoreStr;
1344
1345     for ( my $i = 0 ; $i < @Restores ; $i++ ) {
1346         my $startTime = timeStamp2($Restores[$i]{startTime});
1347         my $dur       = $Restores[$i]{endTime} - $Restores[$i]{startTime};
1348         $dur          = 1 if ( $dur <= 0 );
1349         my $duration  = sprintf("%.1f", $dur / 60);
1350         my $MB        = sprintf("%.1f", $Restores[$i]{size} / (1024*1024));
1351         my $MBperSec  = sprintf("%.2f", $Restores[$i]{size} / (1024*1024*$dur));
1352         my $Restores_Result = $Lang->{failed};
1353         if ($Restores[$i]{result} ne "failed") { $Restores_Result = $Lang->{success}; }
1354         $restoreStr  .= <<EOF;
1355 <tr><td align="center"><a href="$MyURL?action=restoreInfo&num=$Restores[$i]{num}&host=$host">$Restores[$i]{num}</a> </td>
1356     <td align="center"> $Restores_Result </td>
1357     <td align="right"> $startTime </td>
1358     <td align="right"> $duration </td>
1359     <td align="right"> $Restores[$i]{nFiles} </td>
1360     <td align="right"> $MB </td>
1361     <td align="right"> $Restores[$i]{tarCreateErrs} </td>
1362     <td align="right"> $Restores[$i]{xferErrs} </td>
1363 </tr>
1364 EOF
1365     }
1366     if ( $restoreStr ne "" ) {
1367         $restoreStr = eval("qq{$Lang->{Restore_Summary}}");
1368     }
1369     if ( @Backups == 0 ) {
1370         $warnStr = $Lang->{This_PC_has_never_been_backed_up};
1371     }
1372     if ( defined($Hosts->{$host}) ) {
1373         my $user = $Hosts->{$host}{user};
1374         my @moreUsers = sort(keys(%{$Hosts->{$host}{moreUsers}}));
1375         my $moreUserStr;
1376         foreach my $u ( sort(keys(%{$Hosts->{$host}{moreUsers}})) ) {
1377             $moreUserStr .= ", " if ( $moreUserStr ne "" );
1378             $moreUserStr .= "${UserLink($u)}";
1379         }
1380         if ( $moreUserStr ne "" ) {
1381             $moreUserStr = " ($Lang->{and} $moreUserStr).\n";
1382         } else {
1383             $moreUserStr = ".\n";
1384         }
1385         if ( $user ne "" ) {
1386             $statusStr .= eval("qq{$Lang->{This_PC_is_used_by}$moreUserStr}");
1387         }
1388         if ( defined($UserEmailInfo{$user})
1389                 && $UserEmailInfo{$user}{lastHost} eq $host ) {
1390             my $mailTime = timeStamp2($UserEmailInfo{$user}{lastTime});
1391             my $subj     = $UserEmailInfo{$user}{lastSubj};
1392             $statusStr  .= eval("qq{$Lang->{Last_email_sent_to__was_at___subject}}");
1393         }
1394     }
1395     if ( defined($Jobs{$host}) ) {
1396         my $startTime = timeStamp2($Jobs{$host}{startTime});
1397         (my $cmd = $Jobs{$host}{cmd}) =~ s/$BinDir\///g;
1398         $statusStr .= eval("qq{$Lang->{The_command_cmd_is_currently_running_for_started}}");
1399     }
1400     if ( $StatusHost{BgQueueOn} ) {
1401         $statusStr .= eval("qq{$Lang->{Host_host_is_queued_on_the_background_queue_will_be_backed_up_soon}}");
1402     }
1403     if ( $StatusHost{UserQueueOn} ) {
1404         $statusStr .= eval("qq{$Lang->{Host_host_is_queued_on_the_user_queue__will_be_backed_up_soon}}");
1405     }
1406     if ( $StatusHost{CmdQueueOn} ) {
1407         $statusStr .= eval("qq{$Lang->{A_command_for_host_is_on_the_command_queue_will_run_soon}}");
1408     }
1409     my $startTime = timeStamp2($StatusHost{endTime} == 0 ?
1410                 $StatusHost{startTime} : $StatusHost{endTime});
1411     my $reason = "";
1412     if ( $StatusHost{reason} ne "" ) {
1413         $reason = " ($StatusHost{reason})";
1414     }
1415     $statusStr .= eval("qq{$Lang->{Last_status_is_state_StatusHost_state_reason_as_of_startTime}}");
1416
1417     if ( $StatusHost{error} ne "" ) {
1418         $statusStr .= eval("qq{$Lang->{Last_error_is____EscapeHTML_StatusHost_error}}");
1419     }
1420     my $priorStr = "Pings";
1421     if ( $StatusHost{deadCnt} > 0 ) {
1422         $statusStr .= eval("qq{$Lang->{Pings_to_host_have_failed_StatusHost_deadCnt__consecutive_times}}");
1423         $priorStr = $Lang->{Prior_to_that__pings};
1424     }
1425     if ( $StatusHost{aliveCnt} > 0 ) {
1426         $statusStr .= eval("qq{$Lang->{priorStr_to_host_have_succeeded_StatusHostaliveCnt_consecutive_times}}");
1427
1428         if ( $StatusHost{aliveCnt} >= $Conf{BlackoutGoodCnt}
1429                 && $Conf{BlackoutGoodCnt} >= 0 && $Conf{BlackoutHourBegin} >= 0
1430                 && $Conf{BlackoutHourEnd} >= 0 ) {
1431             my(@days) = qw(Sun Mon Tue Wed Thu Fri Sat);
1432             my($days) = join(", ", @days[@{$Conf{BlackoutWeekDays}}]);
1433             my($t0) = sprintf("%d:%02d", $Conf{BlackoutHourBegin},
1434                             60 * ($Conf{BlackoutHourBegin}
1435                                      - int($Conf{BlackoutHourBegin})));
1436             my($t1) = sprintf("%d:%02d", $Conf{BlackoutHourEnd},
1437                             60 * ($Conf{BlackoutHourEnd}
1438                                      - int($Conf{BlackoutHourEnd})));
1439             $statusStr .= eval("qq{$Lang->{Because__host_has_been_on_the_network_at_least__Conf_BlackoutGoodCnt_consecutive_times___}}");
1440         }
1441     }
1442     if ( $StatusHost{backoffTime} > time ) {
1443         my $hours = sprintf("%.1f", ($StatusHost{backoffTime} - time) / 3600);
1444         $statusStr .= eval("qq{$Lang->{Backups_are_deferred_for_hours_hours_change_this_number}}");
1445
1446     }
1447     if ( @Backups ) {
1448         # only allow incremental if there are already some backups
1449         $startIncrStr = <<EOF;
1450 <input type="submit" value="\$Lang->{Start_Incr_Backup}" name="action">
1451 EOF
1452     }
1453
1454     $startIncrStr = eval ("qq{$startIncrStr}");
1455
1456     Header(eval("qq{$Lang->{Host__host_Backup_Summary}}"));
1457     print(eval("qq{$Lang->{Host__host_Backup_Summary2}}"));
1458     Trailer();
1459 }
1460
1461 sub Action_GeneralInfo
1462 {
1463     GetStatusInfo("info jobs hosts queueLen");
1464     my $Privileged = CheckPermission();
1465
1466     my($jobStr, $statusStr, $tarPidHdr);
1467     foreach my $host ( sort(keys(%Jobs)) ) {
1468         my $startTime = timeStamp2($Jobs{$host}{startTime});
1469         next if ( $host eq $bpc->trashJob
1470                     && $Jobs{$host}{processState} ne "running" );
1471         $Jobs{$host}{type} = $Status{$host}{type}
1472                     if ( $Jobs{$host}{type} eq "" && defined($Status{$host}));
1473         (my $cmd = $Jobs{$host}{cmd}) =~ s/$BinDir\///g;
1474         $jobStr .= <<EOF;
1475 <tr><td> ${HostLink($host)} </td>
1476     <td align="center"> $Jobs{$host}{type} </td>
1477     <td align="center"> ${UserLink($Hosts->{$host}{user})} </td>
1478     <td> $startTime </td>
1479     <td> $cmd </td>
1480     <td align="center"> $Jobs{$host}{pid} </td>
1481     <td align="center"> $Jobs{$host}{xferPid} </td>
1482 EOF
1483         if ( $Jobs{$host}{tarPid} > 0 ) {
1484             $jobStr .= "    <td align=\"center\"> $Jobs{$host}{tarPid} </td>\n";
1485             $tarPidHdr ||= "<td align=\"center\"> tar PID </td>\n";
1486         }
1487         $jobStr .= "</tr>\n";
1488     }
1489     foreach my $host ( sort(keys(%Status)) ) {
1490         next if ( $Status{$host}{reason} ne "backup failed" );
1491         my $startTime = timeStamp2($Status{$host}{startTime});
1492         my($errorTime, $XferViewStr);
1493         if ( $Status{$host}{errorTime} > 0 ) {
1494             $errorTime = timeStamp2($Status{$host}{errorTime});
1495         }
1496         if ( -f "$TopDir/pc/$host/SmbLOG.bad"
1497                 || -f "$TopDir/pc/$host/SmbLOG.bad.z"
1498                 || -f "$TopDir/pc/$host/XferLOG.bad"
1499                 || -f "$TopDir/pc/$host/XferLOG.bad.z"
1500                 ) {
1501             $XferViewStr = <<EOF;
1502 <a href="$MyURL?action=view&type=XferLOGbad&host=$host">XferLOG</a>,
1503 <a href="$MyURL?action=view&type=XferErrbad&host=$host">XferErr</a>
1504 EOF
1505         } else {
1506             $XferViewStr = "";
1507         }
1508         (my $shortErr = $Status{$host}{error}) =~ s/(.{48}).*/$1.../;   
1509         $statusStr .= <<EOF;
1510 <tr><td> ${HostLink($host)} </td>
1511     <td align="center"> $Status{$host}{type} </td>
1512     <td align="center"> ${UserLink($Hosts->{$host}{user})} </td>
1513     <td align="right"> $startTime </td>
1514     <td> $XferViewStr </td>
1515     <td align="right"> $errorTime </td>
1516     <td> ${EscapeHTML($shortErr)} </td></tr>
1517 EOF
1518     }
1519     my $now          = timeStamp2(time);
1520     my $nextWakeupTime = timeStamp2($Info{nextWakeup});
1521     my $DUlastTime   = timeStamp2($Info{DUlastValueTime});
1522     my $DUmaxTime    = timeStamp2($Info{DUDailyMaxTime});
1523     my $numBgQueue   = $QueueLen{BgQueue};
1524     my $numUserQueue = $QueueLen{UserQueue};
1525     my $numCmdQueue  = $QueueLen{CmdQueue};
1526     my $serverStartTime = timeStamp2($Info{startTime});
1527     my $poolInfo     = genPoolInfo("pool", \%Info);
1528     my $cpoolInfo    = genPoolInfo("cpool", \%Info);
1529     if ( $Info{poolFileCnt} > 0 && $Info{cpoolFileCnt} > 0 ) {
1530         $poolInfo = <<EOF;
1531 <li>Uncompressed pool:
1532 <ul>
1533 $poolInfo
1534 </ul>
1535 <li>Compressed pool:
1536 <ul>
1537 $cpoolInfo
1538 </ul>
1539 EOF
1540     } elsif ( $Info{cpoolFileCnt} > 0 ) {
1541         $poolInfo = $cpoolInfo;
1542     }
1543
1544     Header($Lang->{H_BackupPC_Server_Status});
1545         #Header("H_BackupPC_Server_Status");
1546     print (eval ("qq{$Lang->{BackupPC_Server_Status}}"));
1547
1548     #Header($Lang->{BackupPC_Server_Status});
1549
1550     #my $trans_text = $Lang->{BackupPC_Server_Status};
1551     #print eval ("qq{$trans_text}");
1552     Trailer();
1553 }
1554
1555 sub Action_RestoreInfo
1556 {
1557     my $Privileged = CheckPermission($In{host});
1558     my $host = $1 if ( $In{host} =~ /(.*)/ );
1559     my $num  = $In{num};
1560     my $i;
1561
1562     if ( !$Privileged ) {
1563         ErrorExit($Lang->{Only_privileged_users_can_view_restore_information});
1564     }
1565     #
1566     # Find the requested restore
1567     #
1568     my @Restores = $bpc->RestoreInfoRead($host);
1569     for ( $i = 0 ; $i < @Restores ; $i++ ) {
1570         last if ( $Restores[$i]{num} == $num );
1571     }
1572     if ( $i >= @Restores ) {
1573         ErrorExit(eval("qq{$Lang->{Restore_number__num_for_host__does_not_exist}}"));
1574     }
1575
1576     %RestoreReq = ();
1577     do "$TopDir/pc/$host/RestoreInfo.$Restores[$i]{num}"
1578             if ( -f "$TopDir/pc/$host/RestoreInfo.$Restores[$i]{num}" );
1579
1580     my $startTime = timeStamp2($Restores[$i]{startTime});
1581     my $reqTime   = timeStamp2($RestoreReq{reqTime});
1582     my $dur       = $Restores[$i]{endTime} - $Restores[$i]{startTime};
1583     $dur          = 1 if ( $dur <= 0 );
1584     my $duration  = sprintf("%.1f", $dur / 60);
1585     my $MB        = sprintf("%.1f", $Restores[$i]{size} / (1024*1024));
1586     my $MBperSec  = sprintf("%.2f", $Restores[$i]{size} / (1024*1024*$dur));
1587
1588     my $fileListStr = "";
1589     foreach my $f ( @{$RestoreReq{fileList}} ) {
1590         my $targetFile = $f;
1591         (my $strippedShareSrc  = $RestoreReq{shareSrc}) =~ s/^\///;
1592         (my $strippedShareDest = $RestoreReq{shareDest}) =~ s/^\///;
1593         substr($targetFile, 0, length($RestoreReq{pathHdrSrc}))
1594                                         = $RestoreReq{pathHdrDest};
1595         $fileListStr .= <<EOF;
1596 <tr><td>$RestoreReq{hostSrc}:/$strippedShareSrc$f</td><td>$RestoreReq{hostDest}:/$strippedShareDest$targetFile</td></tr>
1597 EOF
1598     }
1599
1600     Header(eval("qq{$Lang->{Restore___num_details_for__host}}"));
1601     print(eval("qq{$Lang->{Restore___num_details_for__host2 }}"));
1602     Trailer();
1603 }
1604     
1605 ###########################################################################
1606 # Miscellaneous subroutines
1607 ###########################################################################
1608
1609 sub timeStamp2
1610 {
1611     my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
1612               = localtime($_[0] == 0 ? time : $_[0] );
1613     $year += 1900;
1614     $mon++;
1615     if ( $Conf{CgiDateFormatMMDD} ) {
1616         return sprintf("$mon/$mday %02d:%02d", $hour, $min);
1617     } else {
1618         return sprintf("$mday/$mon %02d:%02d", $hour, $min);
1619     }
1620 }
1621
1622 sub HostLink
1623 {
1624     my($host) = @_;
1625     my($s);
1626     if ( defined($Hosts->{$host}) || defined($Status{$host}) ) {
1627         $s = "<a href=\"$MyURL?host=$host\">$host</a>";
1628     } else {
1629         $s = $host;
1630     }
1631     return \$s;
1632 }
1633
1634 sub UserLink
1635 {
1636     my($user) = @_;
1637     my($s);
1638
1639     return \$user if ( $user eq ""
1640                     || $Conf{CgiUserUrlCreate} eq "" );
1641     if ( $Conf{CgiUserHomePageCheck} eq ""
1642             || -f sprintf($Conf{CgiUserHomePageCheck}, $user, $user, $user) ) {
1643         $s = "<a href=\""
1644              . sprintf($Conf{CgiUserUrlCreate}, $user, $user, $user)
1645              . "\">$user</a>";
1646     } else {
1647         $s = $user;
1648     }
1649     return \$s;
1650 }
1651
1652 sub EscapeHTML
1653 {
1654     my($s) = @_;
1655     $s =~ s/&/&amp;/g;
1656     $s =~ s/\"/&quot;/g;
1657     $s =~ s/>/&gt;/g;
1658     $s =~ s/</&lt;/g;
1659     $s =~ s{([^[:print:]])}{sprintf("&\#x%02X", ord($1));}eg;
1660     return \$s;
1661 }
1662
1663 ##sub URIEncode
1664 ##{
1665 ##    my($s) = @_;
1666 ##    $s =~ s{(['"&%[:^print:]])}{sprintf("%%%02X", ord($1));}eg;
1667 ##    return \$s;
1668 ##}
1669
1670 sub ErrorExit
1671 {
1672     my(@mesg) = @_;
1673     my($head) = shift(@mesg);
1674     my($mesg) = join("</p>\n<p>", @mesg);
1675     $Conf{CgiHeaderFontType} ||= "arial"; 
1676     $Conf{CgiHeaderFontSize} ||= "3";  
1677     $Conf{CgiNavBarBgColor}  ||= "#ddeeee";
1678     $Conf{CgiHeaderBgColor}  ||= "#99cc33";
1679
1680     $bpc->ServerMesg("log User $User (host=$In{host}) got CGI error: $head")
1681                             if ( defined($bpc) );
1682     if ( !defined($Lang->{Error}) ) {
1683         Header("BackupPC: Error");
1684         print <<EOF;
1685 ${h1("Error: Language strings not defined!!")}
1686 <p>$mesg</p>
1687 EOF
1688         Trailer();
1689     } else {
1690         Header(eval("qq{$Lang->{Error}}"));
1691         print (eval("qq{$Lang->{Error____head}}"));
1692         Trailer();
1693     }
1694     exit(1);
1695 }
1696
1697 sub ServerConnect
1698 {
1699     #
1700     # Verify that the server connection is ok
1701     #
1702     return if ( $bpc->ServerOK() );
1703     $bpc->ServerDisconnect();
1704     if ( my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort}) ) {
1705         ErrorExit(eval("qq{$Lang->{Unable_to_connect_to_BackupPC_server}}"));
1706     }
1707 }
1708
1709 sub GetStatusInfo
1710 {
1711     my($status) = @_;
1712     ServerConnect();
1713     my $reply = $bpc->ServerMesg("status $status");
1714     $reply = $1 if ( $reply =~ /(.*)/s );
1715     eval($reply);
1716     # ignore status related to admin and trashClean jobs
1717     if ( $status =~ /\bhosts\b/ ) {
1718         delete($Status{$bpc->adminJob});
1719         delete($Status{$bpc->trashJob});
1720     }
1721 }
1722
1723 sub ReadUserEmailInfo
1724 {
1725     if ( (stat("$TopDir/log/UserEmailInfo.pl"))[9] != $UserEmailInfoMTime ) {
1726         do "$TopDir/log/UserEmailInfo.pl";
1727         $UserEmailInfoMTime = (stat("$TopDir/log/UserEmailInfo.pl"))[9];
1728     }
1729 }
1730
1731 #
1732 # Check if the user is privileged.  A privileged user can access
1733 # any information (backup files, logs, status pages etc).
1734 #
1735 # A user is privileged if they belong to the group
1736 # $Conf{CgiAdminUserGroup}, or they are in $Conf{CgiAdminUsers}
1737 # or they are the user assigned to a host in the host file.
1738 #
1739 sub CheckPermission
1740 {
1741     my($host) = @_;
1742     my $Privileged = 0;
1743
1744     return 0 if ( $User eq "" || ($host ne "" && !defined($Hosts->{$host})) );
1745     if ( $Conf{CgiAdminUserGroup} ne "" ) {
1746         my($n,$p,$gid,$mem) = getgrnam($Conf{CgiAdminUserGroup});
1747         $Privileged ||= ($mem =~ /\b$User\b/);
1748     }
1749     if ( $Conf{CgiAdminUsers} ne "" ) {
1750         $Privileged ||= ($Conf{CgiAdminUsers} =~ /\b$User\b/);
1751         $Privileged ||= $Conf{CgiAdminUsers} eq "*";
1752     }
1753     $PrivAdmin = $Privileged;
1754     $Privileged ||= $User eq $Hosts->{$host}{user};
1755     $Privileged ||= defined($Hosts->{$host}{operators}{$User});
1756
1757     return $Privileged;
1758 }
1759
1760 #
1761 # Returns the list of hosts that should appear in the navigation bar
1762 # for this user.  If $Conf{CgiNavBarAdminAllHosts} is set, the admin
1763 # gets all the hosts.  Otherwise, regular users get hosts for which
1764 # they are the user or are listed in the moreUsers column in the
1765 # hosts file.
1766 #
1767 sub GetUserHosts
1768 {
1769     if ( $Conf{CgiNavBarAdminAllHosts} && CheckPermission() ) {
1770        return sort keys %$Hosts;
1771     }
1772
1773     return sort grep { $Hosts->{$_}{user} eq $User ||
1774                        defined($Hosts->{$_}{moreUsers}{$User}) } keys(%$Hosts);
1775 }
1776
1777 #
1778 # Given a host name tries to find the IP address.  For non-dhcp hosts
1779 # we just return the host name.  For dhcp hosts we check the address
1780 # the user is using ($ENV{REMOTE_ADDR}) and also the last-known IP
1781 # address for $host.  (Later we should replace this with a broadcast
1782 # nmblookup.)
1783 #
1784 sub ConfirmIPAddress
1785 {
1786     my($host) = @_;
1787     my $ipAddr = $host;
1788
1789     if ( $Hosts->{$host}{dhcp}
1790                && $ENV{REMOTE_ADDR} =~ /^(\d+[\.\d]*)$/ ) {
1791         $ipAddr = $1;
1792         my($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($ipAddr);
1793         if ( $netBiosHost ne $host ) {
1794             my($tryIP);
1795             GetStatusInfo("host($host)");
1796             if ( defined($StatusHost{dhcpHostIP})
1797                         && $StatusHost{dhcpHostIP} ne $ipAddr ) {
1798                 $tryIP = eval("qq{$Lang->{tryIP}}");
1799                 ($netBiosHost, $netBiosUser)
1800                         = $bpc->NetBiosInfoGet($StatusHost{dhcpHostIP});
1801             }
1802             if ( $netBiosHost ne $host ) {
1803                 ErrorExit(eval("qq{$Lang->{Can_t_find_IP_address_for}}"),
1804                           eval("qq{$Lang->{host_is_a_DHCP_host}}"));
1805             }
1806             $ipAddr = $StatusHost{dhcpHostIP};
1807         }
1808     }
1809     return $ipAddr;
1810 }
1811
1812 sub genPoolInfo
1813 {
1814     my($name, $info) = @_;
1815     my $poolSize   = sprintf("%.2f", $info->{"${name}Kb"} / (1000 * 1024));
1816     my $poolRmSize = sprintf("%.2f", $info->{"${name}KbRm"} / (1000 * 1024));
1817     my $poolTime   = timeStamp2($info->{"${name}Time"});
1818     $info->{"${name}FileCntRm"} = $info->{"${name}FileCntRm"} + 0;
1819     return eval("qq{$Lang->{Pool_Stat}}");
1820
1821 }
1822
1823 ###########################################################################
1824 # HTML layout subroutines
1825 ###########################################################################
1826
1827 sub Header
1828 {
1829     my($title) = @_;
1830     my @adminLinks = (
1831         { link => "",                          name => $Lang->{Status},
1832                                                priv => 1},
1833         { link => "?action=summary",           name => $Lang->{PC_Summary} },
1834         { link => "?action=view&type=LOG",     name => $Lang->{LOG_file} },
1835         { link => "?action=LOGlist",           name => $Lang->{Old_LOGs} },
1836         { link => "?action=emailSummary",      name => $Lang->{Email_summary} },
1837         { link => "?action=view&type=config",  name => $Lang->{Config_file} },
1838         { link => "?action=view&type=hosts",   name => $Lang->{Hosts_file} },
1839         { link => "?action=queue",             name => $Lang->{Current_queues} },
1840         { link => "?action=view&type=docs",    name => $Lang->{Documentation},
1841                                                priv => 1},
1842         { link => "http://backuppc.sourceforge.net", name => "SourceForge",
1843                                                      priv => 1},
1844     );
1845     print $Cgi->header();
1846     print <<EOF;
1847 <!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN">
1848 <html><head>
1849 <title>$title</title>
1850 $Conf{CgiHeaders}
1851 </head><body>
1852 <table cellpadding="0" cellspacing="0" border="0">
1853 <tr valign="top"><td valign="top" bgcolor="$Conf{CgiNavBarBgColor}" width="10%">
1854 EOF
1855     NavSectionTitle("BackupPC");
1856     print "&nbsp;\n";
1857     if ( defined($In{host}) && defined($Hosts->{$In{host}}) ) {
1858         my $host = $In{host};
1859         NavSectionTitle( eval("qq{$Lang->{Host_Inhost}}") );
1860         NavSectionStart();
1861         NavLink("?host=$host", $Lang->{Home});
1862         NavLink("?action=view&type=LOG&host=$host", $Lang->{LOG_file});
1863         NavLink("?action=LOGlist&host=$host", $Lang->{Old_LOGs});
1864         if ( -f "$TopDir/pc/$host/SmbLOG.bad"
1865                     || -f "$TopDir/pc/$host/SmbLOG.bad.z"
1866                     || -f "$TopDir/pc/$host/XferLOG.bad"
1867                     || -f "$TopDir/pc/$host/XferLOG.bad.z" ) {
1868             NavLink("?action=view&type=XferLOGbad&host=$host",
1869                                 $Lang->{Last_bad_XferLOG});
1870             NavLink("?action=view&type=XferErrbad&host=$host",
1871                                 $Lang->{Last_bad_XferLOG_errors_only});
1872         }
1873         if ( -f "$TopDir/pc/$host/config.pl" ) {
1874             NavLink("?action=view&type=config&host=$host", $Lang->{Config_file});
1875         }
1876         NavSectionEnd();
1877     }
1878     NavSectionTitle($Lang->{Hosts});
1879     if ( defined($Hosts) && %$Hosts > 0 ) {
1880         NavSectionStart(0);
1881         foreach my $host ( GetUserHosts() ) {
1882             NavLink("?host=$host", $host);
1883         }
1884         NavSectionEnd();
1885     }
1886     print <<EOF;
1887 <table cellpadding="2" cellspacing="0" border="0" width="100%">
1888     <tr><td>$Lang->{Host_or_User_name}</td>
1889     <tr><td><form action="$MyURL" method="get"><small>
1890     <input type="text" name="host" size="10" maxlength="64">
1891     <input type="hidden" name="action" value="$Lang->{hostInfo}"><input type="submit" value="$Lang->{Go}" name="ignore">
1892     </small></form></td></tr>
1893 </table>
1894 EOF
1895     NavSectionTitle($Lang->{NavSectionTitle_});
1896     NavSectionStart();
1897     foreach my $l ( @adminLinks ) {
1898         if ( $PrivAdmin || $l->{priv} ) {
1899             NavLink($l->{link}, $l->{name});
1900         } else {
1901             NavLink(undef, $l->{name});
1902         }
1903     }
1904     NavSectionEnd();
1905     print <<EOF;
1906 </td><td valign="top" width="5">&nbsp;&nbsp;</td>
1907 <td valign="top" width="90%">
1908 EOF
1909 }
1910
1911 sub Trailer
1912 {
1913     print <<EOF;
1914 </td></table>
1915 </body></html>
1916 EOF
1917 }
1918
1919
1920 sub NavSectionTitle
1921 {
1922     my($head) = @_;
1923     print <<EOF;
1924 <table cellpadding="2" cellspacing="0" border="0" width="100%">
1925 <tr><td bgcolor="$Conf{CgiHeaderBgColor}"><font face="$Conf{CgiHeaderFontType}"
1926 size="$Conf{CgiHeaderFontSize}"><b>$head</b>
1927 </font></td></tr>
1928 </table>
1929 EOF
1930 }
1931
1932 sub NavSectionStart
1933 {
1934     my($padding) = @_;
1935
1936     $padding = 2 if ( !defined($padding) );
1937     print <<EOF;
1938 <table cellpadding="$padding" cellspacing="0" border="0" width="100%">
1939 EOF
1940 }
1941
1942 sub NavSectionEnd
1943 {
1944     print "</table>\n";
1945 }
1946
1947 sub NavLink
1948 {
1949     my($link, $text) = @_;
1950     print "<tr><td width=\"2%\" valign=\"top\"><b>&middot;</b></td>";
1951     if ( defined($link) ) {
1952         $link = "$MyURL$link" if ( $link eq "" || $link =~ /^\?/ );
1953         print <<EOF;
1954 <td width="98%"><a href="$link"><small>$text</small></a></td></tr>
1955 EOF
1956     } else {
1957         print <<EOF;
1958 <td width="98%"><small>$text</small></td></tr>
1959 EOF
1960     }
1961 }
1962
1963 sub h1
1964 {
1965     my($str) = @_;
1966     return \<<EOF;
1967 <table cellpadding="2" cellspacing="0" border="0" width="100%">
1968 <tr>
1969 <td bgcolor="$Conf{CgiHeaderBgColor}">&nbsp;<font face="$Conf{CgiHeaderFontType}"
1970     size="$Conf{CgiHeaderFontSize}"><b>$str</b></font>
1971 </td></tr>
1972 </table>
1973 EOF
1974 }
1975
1976 sub h2
1977 {
1978     my($str) = @_;
1979     return \<<EOF;
1980 <table cellpadding="2" cellspacing="0" border="0" width="100%">
1981 <tr>
1982 <td bgcolor="$Conf{CgiHeaderBgColor}">&nbsp;<font face="$Conf{CgiHeaderFontType}"
1983     size="$Conf{CgiHeaderFontSize}"><b>$str</b></font>
1984 </td></tr>
1985 </table>
1986 EOF
1987 }