* Major changes from Ryan Kucera to add style sheets to the CGI
[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.0_CVS, released 3 Jul 2003.
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     #
97     # Default REMOTE_USER so in a miminal installation the user
98     # has a sensible default.
99     #
100     $ENV{REMOTE_USER} = $Conf{BackupPCUser} if ( !defined($ENV{REMOTE_USER}) );
101
102     #
103     # We require that Apache pass in $ENV{SCRIPT_NAME} and $ENV{REMOTE_USER}.
104     # The latter requires .ht_access style authentication.  Replace this
105     # code if you are using some other type of authentication, and have
106     # a different way of getting the user name.
107     #
108     $MyURL  = $ENV{SCRIPT_NAME};
109     $User   = $ENV{REMOTE_USER};
110
111     if ( !defined($bpc) ) {
112         ErrorExit($Lang->{BackupPC__Lib__new_failed__check_apache_error_log})
113             if ( !($bpc = BackupPC::Lib->new(undef, undef, 1)) );
114         $TopDir = $bpc->TopDir();
115         $BinDir = $bpc->BinDir();
116         %Conf   = $bpc->Conf();
117         $Lang   = $bpc->Lang();
118         $ConfigMTime = $bpc->ConfigMTime();
119     } elsif ( $bpc->ConfigMTime() != $ConfigMTime ) {
120         $bpc->ServerMesg("log Re-read config file because mtime changed");
121         $bpc->ServerMesg("server reload");
122     }
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             Header(eval("qq{$Lang->{Unable_to_connect_to_BackupPC_server}}"));
268             print (eval("qq{$Lang->{Admin_Start_Server}}"));
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     $Privileged ||= $User eq $Hosts->{$host}{user};
324     $Privileged ||= defined($Hosts->{$host}{moreUsers}{$User});
325
326     return $Privileged;
327 }
328
329 #
330 # Returns the list of hosts that should appear in the navigation bar
331 # for this user.  If $Conf{CgiNavBarAdminAllHosts} is set, the admin
332 # gets all the hosts.  Otherwise, regular users get hosts for which
333 # they are the user or are listed in the moreUsers column in the
334 # hosts file.
335 #
336 sub GetUserHosts
337 {
338     my($host) = @_;
339     my @hosts;
340
341     if ( $Conf{CgiNavBarAdminAllHosts} && 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     #
348     # return the selected host first (if present)
349     #
350     return @hosts if ( !defined($host) || !grep(/^$host$/, @hosts) );
351     return ($host, grep(!/^$host$/, @hosts));
352 }
353
354 #
355 # Given a host name tries to find the IP address.  For non-dhcp hosts
356 # we just return the host name.  For dhcp hosts we check the address
357 # the user is using ($ENV{REMOTE_ADDR}) and also the last-known IP
358 # address for $host.  (Later we should replace this with a broadcast
359 # nmblookup.)
360 #
361 sub ConfirmIPAddress
362 {
363     my($host) = @_;
364     my $ipAddr = $host;
365
366     if ( defined($Hosts->{$host}) && $Hosts->{$host}{dhcp}
367                && $ENV{REMOTE_ADDR} =~ /^(\d+[\.\d]*)$/ ) {
368         $ipAddr = $1;
369         my($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($ipAddr);
370         if ( $netBiosHost ne $host ) {
371             my($tryIP);
372             GetStatusInfo("host(${EscURI($host)})");
373             if ( defined($StatusHost{dhcpHostIP})
374                         && $StatusHost{dhcpHostIP} ne $ipAddr ) {
375                 $tryIP = eval("qq{$Lang->{tryIP}}");
376                 ($netBiosHost, $netBiosUser)
377                         = $bpc->NetBiosInfoGet($StatusHost{dhcpHostIP});
378             }
379             if ( $netBiosHost ne $host ) {
380                 ErrorExit(eval("qq{$Lang->{Can_t_find_IP_address_for}}"),
381                           eval("qq{$Lang->{host_is_a_DHCP_host}}"));
382             }
383             $ipAddr = $StatusHost{dhcpHostIP};
384         }
385     }
386     return $ipAddr;
387 }
388
389 ###########################################################################
390 # HTML layout subroutines
391 ###########################################################################
392
393 sub Header
394 {
395     my($title, $content) = @_;
396     my @adminLinks = (
397         { link => "",                         name => $Lang->{Status},
398                                               priv => 1},
399         { link => "?action=adminOpts",        name => $Lang->{Admin_Options} },
400         { link => "?action=summary",          name => $Lang->{PC_Summary} },
401         { link => "?action=view&type=LOG",    name => $Lang->{LOG_file} },
402         { link => "?action=LOGlist",          name => $Lang->{Old_LOGs} },
403         { link => "?action=emailSummary",     name => $Lang->{Email_summary} },
404         { link => "?action=view&type=config", name => $Lang->{Config_file} },
405         { link => "?action=view&type=hosts",  name => $Lang->{Hosts_file} },
406         { link => "?action=queue",            name => $Lang->{Current_queues} },
407         { link => "?action=view&type=docs",   name => $Lang->{Documentation},
408                                               priv => 1},
409         { link => "http://backuppc.sourceforge.net/faq", name => "FAQ",
410                                               priv => 1},
411         { link => "http://backuppc.sourceforge.net", name => "SourceForge",
412                                               priv => 1},
413     );
414     print $Cgi->header();
415     print <<EOF;
416 <!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN">
417 <html><head>
418 <title>$title</title>
419 $Conf{CSSstylesheet}
420 $Conf{CgiHeaders}
421 </head><body onLoad="document.getElementById('NavMenu').style.height=document.body.scrollHeight">
422 <img src="$Conf{CgiImageDirURL}/logo.gif" hspace="5" vspace="7"><br>
423 EOF
424     if (!defined($In{host})) {
425         print <<EOF;
426 <div id="Content">
427 $content
428 <br><br><br>
429 </div>
430 <div class="NavMenu" id="NavMenu" style="height:100%">
431 EOF
432     } else {
433         print "<div class=\"NavMenu\">";
434     }
435     NavSectionTitle($Lang->{Hosts});
436     my $hostSelectbox = "<option value=\"#\">Select a host...</option>";
437     my @hosts;
438     if ( defined($Hosts) && %$Hosts > 0 ) {
439         @hosts = GetUserHosts($In{host});
440         foreach my $host ( @hosts ) {
441             if ($In{host} eq $host) {
442                 print <<EOF;
443 </div>
444 <div class="HostOn"> <a href="?host=${EscURI($host)}">$host</a> <br>
445 <div class="HostOnContent">
446 EOF
447                 NavLink("?host=${EscURI($host)}",
448                         "$host $Lang->{Home}", " class=\"navbar\"");
449                 NavLink("?action=browse&host=${EscURI($host)}",
450                         "Browse backups", " class=\"navbar\"");
451                 NavLink("?action=view&type=LOG&host=${EscURI($host)}",
452                         $Lang->{LOG_file}, " class=\"navbar\"");
453                 NavLink("?action=LOGlist&host=${EscURI($host)}",
454                         $Lang->{LOG_files}, " class=\"navbar\"");
455                 if ( -f "$TopDir/pc/$host/SmbLOG.bad"
456                             || -f "$TopDir/pc/$host/SmbLOG.bad.z"
457                             || -f "$TopDir/pc/$host/XferLOG.bad"
458                             || -f "$TopDir/pc/$host/XferLOG.bad.z" ) {
459                    NavLink("?action=view&type=XferLOGbad&host=${EscURI($host)}",
460                             $Lang->{Last_bad_XferLOG}, " class=\"navbar\"");
461                    NavLink("?action=view&type=XferErrbad&host=${EscURI($host)}",
462                             $Lang->{Last_bad_XferLOG_errors_only},
463                             " class=\"navbar\"");
464                 }
465                 if ( -f "$TopDir/pc/$host/config.pl" ) {
466                     NavLink("?action=view&type=config&host=${EscURI($host)}",
467                             $Lang->{Config_file}, " class=\"navbar\"");
468                 }
469                 print <<EOF;
470 </div></div>
471 <div id="Content">
472 $content
473 <br><br><br>
474 </div>
475 <div class="NavMenu" style="height:100%" id="NavMenu">
476 EOF
477             } else {
478                 NavLink("?host=${EscURI($host)}", $host) if ( @hosts < 6 );
479                 $hostSelectbox .= "<option value=\"?host=${EscURI($host)}\">"
480                                 . "$host</option>";
481             }
482         }
483     }
484     if ( @hosts >= 6 ) {
485         print <<EOF;
486 <br>
487 <select onChange="document.location=this.value">
488 $hostSelectbox
489 </select>
490 <br><br>
491 EOF
492     }
493     NavSectionTitle($Lang->{Host_or_User_name});
494     print <<EOF;
495 <form action="$MyURL" method="get">
496     <input type="text" name="host" size="10" maxlength="64">
497     <input type="hidden" name="action" value="hostInfo"><input type="submit" value="$Lang->{Go}" name="ignore">
498     </form>
499 EOF
500     NavSectionTitle($Lang->{NavSectionTitle_});
501     foreach my $l ( @adminLinks ) {
502         if ( $PrivAdmin || $l->{priv} ) {
503             NavLink($l->{link}, $l->{name});
504     }
505 }
506
507 print <<EOF;
508 <br><br><br>
509 </div>
510 EOF
511 }
512
513 sub Trailer
514 {
515     print <<EOF;
516 </body></html>
517 EOF
518 }
519
520
521 sub NavSectionTitle
522 {
523     my($head) = @_;
524     print <<EOF;
525 <div class="NavTitle">
526 $head
527 </div>
528 EOF
529 }
530
531 sub NavSectionStart
532 {
533 }
534
535 sub NavSectionEnd
536 {
537 }
538
539 sub NavLink
540 {
541     my($link, $text) = @_;
542     if ( defined($link) ) {
543         my($class);
544         $class = " class=\"NavCurrent\""
545                 if ( length($link) && $ENV{REQUEST_URI} =~ /\Q$link\E$/
546                     || $link eq "" && $ENV{REQUEST_URI} !~ /\?/ );
547         $link = "$MyURL$link" if ( $link eq "" || $link =~ /^\?/ );
548         print <<EOF;
549 <a href="$link"$class>$text</a>
550 EOF
551     } else {
552         print <<EOF;
553 $text<br>
554 EOF
555     }
556 }
557
558 sub h1
559 {
560     my($str) = @_;
561     return \<<EOF;
562 <div class="h1">$str</div>
563 EOF
564 }
565
566 sub h2
567 {
568     my($str) = @_;
569     return \<<EOF;
570 <div class="h2">$str</div>
571 EOF
572 }