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