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