* The CSS definition has been removed from the config.pl file and
[BackupPC.git] / lib / BackupPC / CGI / Lib.pm
1 #============================================================= -*-perl-*-
2 #
3 # BackupPC::CGI::Lib package
4 #
5 # DESCRIPTION
6 #
7 #   This library defines a BackupPC::Lib class and a variety of utility
8 #   functions used by BackupPC.
9 #
10 # AUTHOR
11 #   Craig Barratt  <cbarratt@users.sourceforge.net>
12 #
13 # COPYRIGHT
14 #   Copyright (C) 2003  Craig Barratt
15 #
16 #   This program is free software; you can redistribute it and/or modify
17 #   it under the terms of the GNU General Public License as published by
18 #   the Free Software Foundation; either version 2 of the License, or
19 #   (at your option) any later version.
20 #
21 #   This program is distributed in the hope that it will be useful,
22 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
23 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24 #   GNU General Public License for more details.
25 #
26 #   You should have received a copy of the GNU General Public License
27 #   along with this program; if not, write to the Free Software
28 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
29 #
30 #========================================================================
31 #
32 # Version 2.1.0beta0, released 20 Mar 2004.
33 #
34 # See http://backuppc.sourceforge.net.
35 #
36 #========================================================================
37
38 package BackupPC::CGI::Lib;
39
40 use strict;
41 use BackupPC::Lib;
42
43 require Exporter;
44
45 use vars qw( @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS );
46
47 use vars qw($Cgi %In $MyURL $User %Conf $TopDir $BinDir $bpc);
48 use vars qw(%Status %Info %Jobs @BgQueue @UserQueue @CmdQueue
49             %QueueLen %StatusHost);
50 use vars qw($Hosts $HostsMTime $ConfigMTime $PrivAdmin);
51 use vars qw(%UserEmailInfo $UserEmailInfoMTime %RestoreReq %ArchiveReq);
52 use vars qw($Lang);
53
54 @ISA = qw(Exporter);
55
56 @EXPORT    = qw( );
57
58 @EXPORT_OK = qw(
59                     timeStamp2
60                     HostLink
61                     UserLink
62                     EscHTML
63                     EscURI
64                     ErrorExit
65                     ServerConnect
66                     GetStatusInfo
67                     ReadUserEmailInfo
68                     CheckPermission
69                     GetUserHosts
70                     ConfirmIPAddress
71                     Header
72                     Trailer
73                     NavSectionTitle
74                     NavSectionStart
75                     NavSectionEnd
76                     NavLink
77                     h1
78                     h2
79                     $Cgi %In $MyURL $User %Conf $TopDir $BinDir $bpc
80                     %Status %Info %Jobs @BgQueue @UserQueue @CmdQueue
81                     %QueueLen %StatusHost
82                     $Hosts $HostsMTime $ConfigMTime $PrivAdmin
83                     %UserEmailInfo $UserEmailInfoMTime %RestoreReq %ArchiveReq
84                     $Lang
85              );
86
87 %EXPORT_TAGS = (
88     'all'    => [ @EXPORT_OK ],
89 );
90
91 sub NewRequest
92 {
93     $Cgi = new CGI;
94     %In = $Cgi->Vars;
95
96     if ( !defined($bpc) ) {
97         ErrorExit($Lang->{BackupPC__Lib__new_failed__check_apache_error_log})
98             if ( !($bpc = BackupPC::Lib->new(undef, undef, 1)) );
99         $TopDir = $bpc->TopDir();
100         $BinDir = $bpc->BinDir();
101         %Conf   = $bpc->Conf();
102         $Lang   = $bpc->Lang();
103         $ConfigMTime = $bpc->ConfigMTime();
104     } elsif ( $bpc->ConfigMTime() != $ConfigMTime ) {
105         $bpc->ServerMesg("log Re-read config file because mtime changed");
106         $bpc->ServerMesg("server reload");
107     }
108
109     #
110     # Default REMOTE_USER so in a miminal installation the user
111     # has a sensible default.
112     #
113     $ENV{REMOTE_USER} = $Conf{BackupPCUser} if ( $ENV{REMOTE_USER} eq "" );
114
115     #
116     # We require that Apache pass in $ENV{SCRIPT_NAME} and $ENV{REMOTE_USER}.
117     # The latter requires .ht_access style authentication.  Replace this
118     # code if you are using some other type of authentication, and have
119     # a different way of getting the user name.
120     #
121     $MyURL  = $ENV{SCRIPT_NAME};
122     $User   = $ENV{REMOTE_USER};
123
124     #
125     # Clean up %ENV for taint checking
126     #
127     delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
128     $ENV{PATH} = $Conf{MyPath};
129
130     #
131     # Verify we are running as the correct user
132     #
133     if ( $Conf{BackupPCUserVerify}
134             && $> != (my $uid = (getpwnam($Conf{BackupPCUser}))[2]) ) {
135         ErrorExit(eval("qq{$Lang->{Wrong_user__my_userid_is___}}"), <<EOF);
136 This script needs to run as the user specified in \$Conf{BackupPCUser},
137 which is set to $Conf{BackupPCUser}.
138 <p>
139 This is an installation problem.  If you are using mod_perl then
140 it appears that Apache is not running as user $Conf{BackupPCUser}.
141 If you are not using mod_perl, then most like setuid is not working
142 properly on BackupPC_Admin.  Check the permissions on
143 $Conf{CgiDir}/BackupPC_Admin and look at the documentation.
144 EOF
145     }
146
147     if ( !defined($Hosts) || $bpc->HostsMTime() != $HostsMTime ) {
148         $HostsMTime = $bpc->HostsMTime();
149         $Hosts = $bpc->HostInfoRead();
150
151         # turn moreUsers list into a hash for quick lookups
152         foreach my $host (keys %$Hosts) {
153            $Hosts->{$host}{moreUsers} =
154                {map {$_, 1} split(",", $Hosts->{$host}{moreUsers}) }
155         }
156     }
157 }
158
159 sub timeStamp2
160 {
161     my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
162               = localtime($_[0] == 0 ? time : $_[0] );
163     $mon++;
164     if ( $Conf{CgiDateFormatMMDD} ) {
165         return sprintf("$mon/$mday %02d:%02d", $hour, $min);
166     } else {
167         return sprintf("$mday/$mon %02d:%02d", $hour, $min);
168     }
169 }
170
171 sub HostLink
172 {
173     my($host) = @_;
174     my($s);
175     if ( defined($Hosts->{$host}) || defined($Status{$host}) ) {
176         $s = "<a href=\"$MyURL?host=${EscURI($host)}\">$host</a>";
177     } else {
178         $s = $host;
179     }
180     return \$s;
181 }
182
183 sub UserLink
184 {
185     my($user) = @_;
186     my($s);
187
188     return \$user if ( $user eq ""
189                     || $Conf{CgiUserUrlCreate} eq "" );
190     if ( $Conf{CgiUserHomePageCheck} eq ""
191             || -f sprintf($Conf{CgiUserHomePageCheck}, $user, $user, $user) ) {
192         $s = "<a href=\""
193              . sprintf($Conf{CgiUserUrlCreate}, $user, $user, $user)
194              . "\">$user</a>";
195     } else {
196         $s = $user;
197     }
198     return \$s;
199 }
200
201 sub EscHTML
202 {
203     my($s) = @_;
204     $s =~ s/&/&amp;/g;
205     $s =~ s/\"/&quot;/g;
206     $s =~ s/>/&gt;/g;
207     $s =~ s/</&lt;/g;
208     $s =~ s{([^[:print:]])}{sprintf("&\#x%02X;", ord($1));}eg;
209     return \$s;
210 }
211
212 sub EscURI
213 {
214     my($s) = @_;
215     $s =~ s{([^\w.\/-])}{sprintf("%%%02X", ord($1));}eg;
216     return \$s;
217 }
218
219 sub ErrorExit
220 {
221     my(@mesg) = @_;
222     my($head) = shift(@mesg);
223     my($mesg) = join("</p>\n<p>", @mesg);
224
225     if ( !defined($ENV{REMOTE_USER}) ) {
226         $mesg .= <<EOF;
227 <p>
228 Note: \$ENV{REMOTE_USER} is not set, which could mean there is an
229 installation problem.  BackupPC_Admin expects Apache to authenticate
230 the user and pass their user name into this script as the REMOTE_USER
231 environment variable.  See the documentation.
232 EOF
233     }
234
235     $bpc->ServerMesg("log User $User (host=$In{host}) got CGI error: $head")
236                             if ( defined($bpc) );
237     if ( !defined($Lang->{Error}) ) {
238         $mesg = <<EOF if ( !defined($mesg) );
239 There is some problem with the BackupPC installation.
240 Please check the permissions on BackupPC_Admin.
241 EOF
242         my $content = <<EOF;
243 ${h1("Error: Unable to read config.pl or language strings!!")}
244 <p>$mesg</p>
245 EOF
246         Header("BackupPC: Error", $content);
247         Trailer();
248     } else {
249         my $content = eval("qq{$Lang->{Error____head}}");
250         Header(eval("qq{$Lang->{Error}}"), $content);
251         Trailer();
252     }
253     exit(1);
254 }
255
256 sub ServerConnect
257 {
258     #
259     # Verify that the server connection is ok
260     #
261     return if ( $bpc->ServerOK() );
262     $bpc->ServerDisconnect();
263     if ( my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort}) ) {
264         if ( CheckPermission() 
265           && -f $Conf{ServerInitdPath}
266           && $Conf{ServerInitdStartCmd} ne "" ) {
267             my $content = eval("qq{$Lang->{Admin_Start_Server}}");
268             Header(eval("qq{$Lang->{Unable_to_connect_to_BackupPC_server}}"), $content);
269             Trailer();
270             exit(1);
271         } else {
272             ErrorExit(eval("qq{$Lang->{Unable_to_connect_to_BackupPC_server}}"));
273         }
274     }
275 }
276
277 sub GetStatusInfo
278 {
279     my($status) = @_;
280     ServerConnect();
281     my $reply = $bpc->ServerMesg("status $status");
282     $reply = $1 if ( $reply =~ /(.*)/s );
283     eval($reply);
284     # ignore status related to admin and trashClean jobs
285     if ( $status =~ /\bhosts\b/ ) {
286         delete($Status{$bpc->adminJob});
287         delete($Status{$bpc->trashJob});
288     }
289 }
290
291 sub ReadUserEmailInfo
292 {
293     if ( (stat("$TopDir/log/UserEmailInfo.pl"))[9] != $UserEmailInfoMTime ) {
294         do "$TopDir/log/UserEmailInfo.pl";
295         $UserEmailInfoMTime = (stat("$TopDir/log/UserEmailInfo.pl"))[9];
296     }
297 }
298
299 #
300 # Check if the user is privileged.  A privileged user can access
301 # any information (backup files, logs, status pages etc).
302 #
303 # A user is privileged if they belong to the group
304 # $Conf{CgiAdminUserGroup}, or they are in $Conf{CgiAdminUsers}
305 # or they are the user assigned to a host in the host file.
306 #
307 sub CheckPermission
308 {
309     my($host) = @_;
310     my $Privileged = 0;
311
312     return 0 if ( $User eq "" && $Conf{CgiAdminUsers} ne "*"
313                || $host ne "" && !defined($Hosts->{$host}) );
314     if ( $Conf{CgiAdminUserGroup} ne "" ) {
315         my($n,$p,$gid,$mem) = getgrnam($Conf{CgiAdminUserGroup});
316         $Privileged ||= ($mem =~ /\b$User\b/);
317     }
318     if ( $Conf{CgiAdminUsers} ne "" ) {
319         $Privileged ||= ($Conf{CgiAdminUsers} =~ /\b$User\b/);
320         $Privileged ||= $Conf{CgiAdminUsers} eq "*";
321     }
322     $PrivAdmin = $Privileged;
323     return $Privileged if ( !defined($host) );
324
325     $Privileged ||= $User eq $Hosts->{$host}{user};
326     $Privileged ||= defined($Hosts->{$host}{moreUsers}{$User});
327     return $Privileged;
328 }
329
330 #
331 # Returns the list of hosts that should appear in the navigation bar
332 # for this user.  If $getAll is set, the admin gets all the hosts.
333 # Otherwise, regular users get hosts for which they are the user or
334 # are listed in the moreUsers column in the hosts file.
335 #
336 sub GetUserHosts
337 {
338     my($getAll) = @_;
339     my @hosts;
340
341     if ( $getAll && CheckPermission() ) {
342         @hosts = sort keys %$Hosts;
343     } else {
344         @hosts = sort grep { $Hosts->{$_}{user} eq $User ||
345                        defined($Hosts->{$_}{moreUsers}{$User}) } keys(%$Hosts);
346     }
347     return @hosts;
348 }
349
350 #
351 # Given a host name tries to find the IP address.  For non-dhcp hosts
352 # we just return the host name.  For dhcp hosts we check the address
353 # the user is using ($ENV{REMOTE_ADDR}) and also the last-known IP
354 # address for $host.  (Later we should replace this with a broadcast
355 # nmblookup.)
356 #
357 sub ConfirmIPAddress
358 {
359     my($host) = @_;
360     my $ipAddr = $host;
361
362     if ( defined($Hosts->{$host}) && $Hosts->{$host}{dhcp}
363                && $ENV{REMOTE_ADDR} =~ /^(\d+[\.\d]*)$/ ) {
364         $ipAddr = $1;
365         my($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($ipAddr);
366         if ( $netBiosHost ne $host ) {
367             my($tryIP);
368             GetStatusInfo("host(${EscURI($host)})");
369             if ( defined($StatusHost{dhcpHostIP})
370                         && $StatusHost{dhcpHostIP} ne $ipAddr ) {
371                 $tryIP = eval("qq{$Lang->{tryIP}}");
372                 ($netBiosHost, $netBiosUser)
373                         = $bpc->NetBiosInfoGet($StatusHost{dhcpHostIP});
374             }
375             if ( $netBiosHost ne $host ) {
376                 ErrorExit(eval("qq{$Lang->{Can_t_find_IP_address_for}}"),
377                           eval("qq{$Lang->{host_is_a_DHCP_host}}"));
378             }
379             $ipAddr = $StatusHost{dhcpHostIP};
380         }
381     }
382     return $ipAddr;
383 }
384
385 ###########################################################################
386 # HTML layout subroutines
387 ###########################################################################
388
389 sub Header
390 {
391     my($title, $content, $noBrowse, $contentSub, $contentPost) = @_;
392     my @adminLinks = (
393         { link => "",                         name => $Lang->{Status},
394                                               priv => 1},
395         { link => "?action=adminOpts",        name => $Lang->{Admin_Options} },
396         { link => "?action=summary",          name => $Lang->{PC_Summary},
397                                               priv => 1},
398         { link => "?action=view&type=LOG",    name => $Lang->{LOG_file} },
399         { link => "?action=LOGlist",          name => $Lang->{Old_LOGs} },
400         { link => "?action=emailSummary",     name => $Lang->{Email_summary} },
401         { link => "?action=view&type=config", name => $Lang->{Config_file} },
402         { link => "?action=view&type=hosts",  name => $Lang->{Hosts_file} },
403         { link => "?action=queue",            name => $Lang->{Current_queues} },
404         { link => "?action=view&type=docs",   name => $Lang->{Documentation},
405                                               priv => 1},
406         { link => "http://backuppc.sourceforge.net/faq", name => "FAQ",
407                                               priv => 1},
408         { link => "http://backuppc.sourceforge.net", name => "SourceForge",
409                                               priv => 1},
410     );
411     my $host = $In{host};
412
413     print $Cgi->header();
414     print <<EOF;
415 <!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN">
416 <html><head>
417 <title>$title</title>
418 <link rel=stylesheet type="text/css" href="$Conf{CgiImageDirURL}/$Conf{CgiCSSFile}" title="CSSFile">
419 $Conf{CgiHeaders}
420 </head><body onLoad="document.getElementById('NavMenu').style.height=document.body.scrollHeight">
421 <img src="$Conf{CgiImageDirURL}/logo.gif" hspace="5" vspace="7"><br>
422 EOF
423
424     if ( defined($Hosts) && defined($host) && defined($Hosts->{$host}) ) {
425         print "<div class=\"NavMenu\">";
426         NavSectionTitle("${EscURI($host)}");
427         print <<EOF;
428 </div>
429 <div class="NavMenu">
430 EOF
431         NavLink("?host=${EscURI($host)}",
432                 "$host $Lang->{Home}", " class=\"navbar\"");
433         NavLink("?action=browse&host=${EscURI($host)}",
434                 $Lang->{Browse}, " class=\"navbar\"") if ( !$noBrowse );
435         NavLink("?action=view&type=LOG&host=${EscURI($host)}",
436                 $Lang->{LOG_file}, " class=\"navbar\"");
437         NavLink("?action=LOGlist&host=${EscURI($host)}",
438                 $Lang->{LOG_files}, " class=\"navbar\"");
439         if ( -f "$TopDir/pc/$host/SmbLOG.bad"
440                     || -f "$TopDir/pc/$host/SmbLOG.bad.z"
441                     || -f "$TopDir/pc/$host/XferLOG.bad"
442                     || -f "$TopDir/pc/$host/XferLOG.bad.z" ) {
443            NavLink("?action=view&type=XferLOGbad&host=${EscURI($host)}",
444                     $Lang->{Last_bad_XferLOG}, " class=\"navbar\"");
445            NavLink("?action=view&type=XferErrbad&host=${EscURI($host)}",
446                     $Lang->{Last_bad_XferLOG_errors_only},
447                     " class=\"navbar\"");
448         }
449         if ( -f "$TopDir/pc/$host/config.pl" ) {
450             NavLink("?action=view&type=config&host=${EscURI($host)}",
451                     $Lang->{Config_file}, " class=\"navbar\"");
452         }
453         print "</div>\n";
454     }
455     print("<div id=\"Content\">\n$content\n");
456     if ( defined($contentSub) && ref($contentSub) eq "CODE" ) {
457         while ( (my $s = &$contentSub()) ne "" ) {
458             print($s);
459         }
460     }
461     print($contentPost) if ( defined($contentPost) );
462     print <<EOF;
463 <br><br><br>
464 </div>
465 <div class="NavMenu" id="NavMenu" style="height:100%">
466 EOF
467     my $hostSelectbox = "<option value=\"#\">$Lang->{Select_a_host}</option>";
468     my @hosts = GetUserHosts($Conf{CgiNavBarAdminAllHosts});
469     if ( defined($Hosts) && %$Hosts > 0 && @hosts ) {
470         NavSectionTitle($Lang->{Hosts});
471         foreach my $host ( @hosts ) {
472             NavLink("?host=${EscURI($host)}", $host)
473                     if ( @hosts < $Conf{CgiNavBarAdminAllHosts} );
474             my $sel = " selected" if ( $host eq $In{host} );
475             $hostSelectbox .= "<option value=\"?host=${EscURI($host)}\"$sel>"
476                             . "$host</option>";
477         }
478     }
479     if ( @hosts >= $Conf{CgiNavBarAdminAllHosts} ) {
480         print <<EOF;
481 <br>
482 <select onChange="document.location=this.value">
483 $hostSelectbox
484 </select>
485 <br><br>
486 EOF
487     }
488     print <<EOF;
489 <form action="$MyURL" method="get">
490     <input type="text" name="host" size="14" maxlength="64">
491     <input type="hidden" name="action" value="hostInfo"><input type="submit" value="$Lang->{Go}" name="ignore">
492     </form>
493 EOF
494     NavSectionTitle($Lang->{NavSectionTitle_});
495     foreach my $l ( @adminLinks ) {
496         if ( $PrivAdmin || $l->{priv} ) {
497             NavLink($l->{link}, $l->{name});
498     }
499 }
500
501 print <<EOF;
502 <br><br><br>
503 </div>
504 EOF
505 }
506
507 sub Trailer
508 {
509     print <<EOF;
510 </body></html>
511 EOF
512 }
513
514
515 sub NavSectionTitle
516 {
517     my($head) = @_;
518     print <<EOF;
519 <div class="NavTitle">$head</div>
520 EOF
521 }
522
523 sub NavSectionStart
524 {
525 }
526
527 sub NavSectionEnd
528 {
529 }
530
531 sub NavLink
532 {
533     my($link, $text) = @_;
534     if ( defined($link) ) {
535         my($class);
536         $class = " class=\"NavCurrent\""
537                 if ( length($link) && $ENV{REQUEST_URI} =~ /\Q$link\E$/
538                     || $link eq "" && $ENV{REQUEST_URI} !~ /\?/ );
539         $link = "$MyURL$link" if ( $link eq "" || $link =~ /^\?/ );
540         print <<EOF;
541 <a href="$link"$class>$text</a>
542 EOF
543     } else {
544         print <<EOF;
545 $text<br>
546 EOF
547     }
548 }
549
550 sub h1
551 {
552     my($str) = @_;
553     return \<<EOF;
554 <div class="h1">$str</div>
555 EOF
556 }
557
558 sub h2
559 {
560     my($str) = @_;
561     return \<<EOF;
562 <div class="h2">$str</div>
563 EOF
564 }