"Fossies" - the Fresh Open Source Software Archive 
Member "BackupPC-4.4.0/lib/BackupPC/CGI/Lib.pm" (20 Jun 2020, 19339 Bytes) of package /linux/privat/BackupPC-4.4.0.tar.gz:
The requested HTML page contains a <FORM> tag that is unusable on "Fossies" in "automatic" (rendered) mode so that page is shown as HTML source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file. For more information about "Lib.pm" see the
Fossies "Dox" file reference documentation and the latest
Fossies "Diffs" side-by-side code changes report:
4.3.2_vs_4.4.0.
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-2020 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 3 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, see <http://www.gnu.org/licenses/>.
28 #
29 #========================================================================
30 #
31 # Version 4.4.0, released 20 Jun 2020.
32 #
33 # See http://backuppc.sourceforge.net.
34 #
35 #========================================================================
36
37 package BackupPC::CGI::Lib;
38
39 use strict;
40 use BackupPC::Lib;
41
42 require Exporter;
43
44 use vars qw( @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS );
45
46 use vars qw($Cgi %In $MyURL $User %Conf $TopDir $LogDir $BinDir $bpc);
47 use vars qw(%Status %Info %Jobs @BgQueue @UserQueue @CmdQueue
48 %QueueLen %StatusHost);
49 use vars qw($Hosts $HostsMTime $ConfigMTime $PrivAdmin);
50 use vars qw(%UserEmailInfo $UserEmailInfoMTime %RestoreReq %ArchiveReq);
51 use vars qw($Lang);
52
53 @ISA = qw(Exporter);
54
55 @EXPORT = qw( );
56
57 @EXPORT_OK = qw(
58 timeStamp2
59 HostLink
60 UserLink
61 EscHTML
62 EscURI
63 ErrorExit
64 ServerConnect
65 GetStatusInfo
66 ReadUserEmailInfo
67 CheckPermission
68 GetUserHosts
69 ConfirmIPAddress
70 Header
71 Trailer
72 NavSectionTitle
73 NavSectionStart
74 NavSectionEnd
75 NavLink
76 h1
77 h2
78 $Cgi %In $MyURL $User %Conf $TopDir $LogDir $BinDir $bpc
79 %Status %Info %Jobs @BgQueue @UserQueue @CmdQueue
80 %QueueLen %StatusHost
81 $Hosts $HostsMTime $ConfigMTime $PrivAdmin
82 %UserEmailInfo $UserEmailInfoMTime %RestoreReq %ArchiveReq
83 $Lang
84 );
85
86 %EXPORT_TAGS = ('all' => [@EXPORT_OK]);
87
88 sub NewRequest
89 {
90 $Cgi = new CGI;
91 %In = $Cgi->Vars;
92
93 if ( !defined($bpc) ) {
94 ErrorExit($Lang->{BackupPC__Lib__new_failed__check_apache_error_log})
95 if ( !($bpc = BackupPC::Lib->new(undef, undef, undef, 1)) );
96 $TopDir = $bpc->TopDir();
97 $LogDir = $bpc->LogDir();
98 $BinDir = $bpc->BinDir();
99 %Conf = $bpc->Conf();
100 $Lang = $bpc->Lang();
101 $ConfigMTime = $bpc->ConfigMTime();
102 umask($Conf{UmaskMode});
103 } elsif ( $bpc->ConfigMTime() != $ConfigMTime ) {
104 $bpc->ConfigRead();
105 $TopDir = $bpc->TopDir();
106 $LogDir = $bpc->LogDir();
107 $BinDir = $bpc->BinDir();
108 %Conf = $bpc->Conf();
109 $Lang = $bpc->Lang();
110 $ConfigMTime = $bpc->ConfigMTime();
111 umask($Conf{UmaskMode});
112 }
113
114 #
115 # Default REMOTE_USER so in a miminal installation the user
116 # has a sensible default.
117 #
118 $ENV{REMOTE_USER} = $Conf{BackupPCUser} if ( $ENV{REMOTE_USER} eq "" );
119
120 #
121 # We require that Apache pass in $ENV{SCRIPT_NAME} and $ENV{REMOTE_USER}.
122 # The latter requires .ht_access style authentication. Replace this
123 # code if you are using some other type of authentication, and have
124 # a different way of getting the user name.
125 #
126 $MyURL = $ENV{SCRIPT_NAME};
127 $User = $ENV{REMOTE_USER};
128
129 #
130 # Handle LDAP uid=user when using mod_authz_ldap and otherwise untaint
131 #
132 $User = $1 if ( $User =~ /uid=([^,]+)/i || $User =~ /(.*)/ );
133
134 #
135 # Clean up %ENV for taint checking
136 #
137 delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
138 $ENV{PATH} = $Conf{MyPath};
139
140 #
141 # Verify we are running as the correct user
142 #
143 if ( $Conf{BackupPCUserVerify} && $> != (my $uid = getpwnam($Conf{BackupPCUser})) ) {
144 ErrorExit(eval("qq{$Lang->{Wrong_user__my_userid_is___}}"), <<EOF);
145 This script needs to run as the user specified in \$Conf{BackupPCUser},
146 which is set to $Conf{BackupPCUser}.
147 <p>
148 This is an installation problem. If you are using mod_perl then
149 it appears that Apache is not running as user $Conf{BackupPCUser}.
150 If you are not using mod_perl, then most like setuid is not working
151 properly on BackupPC_Admin. Check the permissions on
152 $Conf{CgiDir}/BackupPC_Admin and look at the documentation.
153 EOF
154 }
155
156 if ( !defined($Hosts) || $bpc->HostsMTime() != $HostsMTime ) {
157 $HostsMTime = $bpc->HostsMTime();
158 $Hosts = $bpc->HostInfoRead();
159
160 # turn moreUsers list into a hash for quick lookups
161 foreach my $host ( keys %$Hosts ) {
162 $Hosts->{$host}{moreUsers} =
163 {map { $_, 1 } split(",", $Hosts->{$host}{moreUsers})};
164 }
165 }
166
167 #
168 # Untaint the host name
169 #
170 if ( $In{host} =~ /^([\w.\s-]+)$/ ) {
171 $In{host} = $1;
172 } else {
173 delete($In{host});
174 }
175 }
176
177 sub timeStamp2
178 {
179 my $now = $_[0] == 0 ? time : $_[0];
180 my($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($now);
181 $mon++;
182 if ( $Conf{CgiDateFormatMMDD} == 2 ) {
183 $year += 1900;
184 return sprintf("%04d-%02d-%02d %02d:%02d", $year, $mon, $mday, $hour, $min);
185 } elsif ( $Conf{CgiDateFormatMMDD} ) {
186 #
187 # Add the year if the time is more than 330 days ago
188 #
189 if ( time - $now > 330 * 24 * 3600 ) {
190 $year -= 100;
191 return sprintf("$mon/$mday/%02d %02d:%02d", $year, $hour, $min);
192 } else {
193 return sprintf("$mon/$mday %02d:%02d", $hour, $min);
194 }
195 } else {
196 #
197 # Add the year if the time is more than 330 days ago
198 #
199 if ( time - $now > 330 * 24 * 3600 ) {
200 $year -= 100;
201 return sprintf("$mday/$mon/%02d %02d:%02d", $year, $hour, $min);
202 } else {
203 return sprintf("$mday/$mon %02d:%02d", $hour, $min);
204 }
205 }
206 }
207
208 sub HostLink
209 {
210 my($host) = @_;
211 my($s);
212 if ( defined($Hosts->{$host}) ) {
213 $s = "<a href=\"$MyURL?host=${EscURI($host)}\">$host</a>";
214 } else {
215 $s = $host;
216 }
217 return \$s;
218 }
219
220 sub UserLink
221 {
222 my($user) = @_;
223 my($s);
224
225 return \$user if ( $user eq "" || $Conf{CgiUserUrlCreate} eq "" );
226 if ( $Conf{CgiUserHomePageCheck} eq "" || -f sprintf($Conf{CgiUserHomePageCheck}, $user, $user, $user) ) {
227 $s = "<a href=\"" . sprintf($Conf{CgiUserUrlCreate}, $user, $user, $user) . "\">$user</a>";
228 } else {
229 $s = $user;
230 }
231 return \$s;
232 }
233
234 sub EscHTML
235 {
236 my($s) = @_;
237 $s =~ s/&/&/g;
238 $s =~ s/\"/"/g;
239 $s =~ s/>/>/g;
240 $s =~ s/</</g;
241 ### $s =~ s{([^[:print:]])}{sprintf("&\#x%02X;", ord($1));}eg;
242 return \$s;
243 }
244
245 sub EscURI
246 {
247 my($s) = @_;
248 $s =~ s{([^\w.\/-])}{sprintf("%%%02X", ord($1));}eg;
249 return \$s;
250 }
251
252 sub ErrorExit
253 {
254 my(@mesg) = @_;
255 my($head) = shift(@mesg);
256 my($mesg) = join("</p>\n<p>", @mesg);
257
258 if ( !defined($ENV{REMOTE_USER}) ) {
259 $mesg .= <<EOF;
260 <p>
261 Note: \$ENV{REMOTE_USER} is not set, which could mean there is an
262 installation problem. BackupPC_Admin expects Apache to authenticate
263 the user and pass their user name into this script as the REMOTE_USER
264 environment variable. See the documentation.
265 EOF
266 }
267
268 $bpc->ServerMesg("log User $User (host=$In{host}) got CGI error: $head")
269 if ( defined($bpc) );
270 if ( !defined($Lang->{Error}) ) {
271 $mesg = <<EOF if ( !defined($mesg) );
272 There is some problem with the BackupPC installation.
273 Please check the permissions on BackupPC_Admin.
274 EOF
275 my $content = <<EOF;
276 ${h1("Error: Unable to read config.pl or language strings!!")}
277 <p>$mesg</p>
278 EOF
279 Header("BackupPC: Error", $content);
280 Trailer();
281 } else {
282 my $content = eval("qq{$Lang->{Error____head}}");
283 Header(eval("qq{$Lang->{Error}}"), $content);
284 Trailer();
285 }
286 exit(1);
287 }
288
289 sub ServerConnect
290 {
291 #
292 # Verify that the server connection is ok
293 #
294 return if ( $bpc->ServerOK() );
295 $bpc->ServerDisconnect();
296 if ( my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort}) ) {
297 if ( CheckPermission()
298 && -f $Conf{ServerInitdPath}
299 && $Conf{ServerInitdStartCmd} ne "" ) {
300 my $content = eval("qq{$Lang->{Admin_Start_Server}}");
301 Header(eval("qq{$Lang->{Unable_to_connect_to_BackupPC_server}}"), $content);
302 Trailer();
303 exit(1);
304 } else {
305 ErrorExit(
306 eval("qq{$Lang->{Unable_to_connect_to_BackupPC_server}}"),
307 eval("qq{$Lang->{Unable_to_connect_to_BackupPC_server_error_message}}")
308 );
309 }
310 }
311 }
312
313 sub GetStatusInfo
314 {
315 my($status) = @_;
316 ServerConnect();
317 %Status = () if ( $status =~ /\bhosts\b/ );
318 %StatusHost = () if ( $status =~ /\bhost\(/ );
319 my $reply = $bpc->ServerMesg("status $status");
320 $reply = $1 if ( $reply =~ /(.*)/s );
321 eval($reply);
322
323 # ignore status related to admin jobs
324 if ( $status =~ /\bhosts\b/ ) {
325 foreach my $host ( grep(/admin/, keys(%Status)) ) {
326 delete($Status{$host}) if ( $bpc->isAdminJob($host) );
327 }
328 delete($Status{$bpc->scgiJob});
329 }
330 }
331
332 sub ReadUserEmailInfo
333 {
334 if ( (stat("$LogDir/UserEmailInfo.pl"))[9] != $UserEmailInfoMTime ) {
335 do "$LogDir/UserEmailInfo.pl";
336 $UserEmailInfoMTime = (stat("$LogDir/UserEmailInfo.pl"))[9];
337 }
338 }
339
340 #
341 # Check if the user is privileged. A privileged user can access
342 # any information (backup files, logs, status pages etc).
343 #
344 # A user is privileged if they belong to the group
345 # $Conf{CgiAdminUserGroup}, or they are in $Conf{CgiAdminUsers}
346 # or they are the user assigned to a host in the host file.
347 #
348 sub CheckPermission
349 {
350 my($host) = @_;
351 my $Privileged = 0;
352
353 return 0 if ( $User eq "" && $Conf{CgiAdminUsers} ne "*" || $host ne "" && !defined($Hosts->{$host}) );
354 if ( $Conf{CgiAdminUserGroup} ne "" ) {
355 for ( split(/\s+/, $Conf{CgiAdminUserGroup}) ) {
356 my($n, $p, $gid, $mem) = getgrnam($_);
357 $Privileged ||= ($mem =~ /\b\Q$User\E\b/);
358 last if ( $Privileged );
359 }
360 }
361 if ( $Conf{CgiAdminUsers} ne "" ) {
362 $Privileged ||= ($Conf{CgiAdminUsers} =~ /\b\Q$User\E\b/);
363 $Privileged ||= $Conf{CgiAdminUsers} eq "*";
364 }
365 $PrivAdmin = $Privileged;
366 return $Privileged if ( !defined($host) );
367
368 $Privileged ||= $User eq $Hosts->{$host}{user};
369 $Privileged ||= defined($Hosts->{$host}{moreUsers}{$User});
370 return $Privileged;
371 }
372
373 #
374 # Returns the list of hosts that should appear in the navigation bar
375 # for this user. If $getAll is set, the admin gets all the hosts.
376 # Otherwise, regular users get hosts for which they are the user or
377 # are listed in the moreUsers column in the hosts file.
378 #
379 sub GetUserHosts
380 {
381 my($getAll) = @_;
382 my @hosts;
383
384 if ( $getAll && CheckPermission() ) {
385 @hosts = sort keys %$Hosts;
386 } else {
387 @hosts = sort grep { $Hosts->{$_}{user} eq $User || defined($Hosts->{$_}{moreUsers}{$User}) } keys(%$Hosts);
388 }
389 return @hosts;
390 }
391
392 #
393 # Given a host name tries to find the IP address. For non-dhcp hosts
394 # we just return the host name. For dhcp hosts we check the address
395 # the user is using ($ENV{REMOTE_ADDR}) and also the last-known IP
396 # address for $host. (Later we should replace this with a broadcast
397 # nmblookup.)
398 #
399 sub ConfirmIPAddress
400 {
401 my($host) = @_;
402 my $ipAddr = $host;
403
404 if ( defined($Hosts->{$host}) && $Hosts->{$host}{dhcp} && $ENV{REMOTE_ADDR} =~ /^(\d+[\.\d]*)$/ ) {
405 $ipAddr = $1;
406 my($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($ipAddr);
407 if ( $netBiosHost ne $host ) {
408 my($tryIP);
409 GetStatusInfo("host(${EscURI($host)})");
410 if ( defined($StatusHost{dhcpHostIP}) && $StatusHost{dhcpHostIP} ne $ipAddr ) {
411 $tryIP = eval("qq{$Lang->{tryIP}}");
412 ($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($StatusHost{dhcpHostIP});
413 }
414 if ( $netBiosHost ne $host ) {
415 ErrorExit(eval("qq{$Lang->{Can_t_find_IP_address_for}}"), eval("qq{$Lang->{host_is_a_DHCP_host}}"));
416 }
417 $ipAddr = $StatusHost{dhcpHostIP};
418 }
419 }
420 return $ipAddr;
421 }
422
423 ###########################################################################
424 # HTML layout subroutines
425 ###########################################################################
426
427 sub Header
428 {
429 my($title, $content, $noBrowse, $contentSub, $contentPost) = @_;
430 my @adminLinks = (
431 {link => "?action=status", name => $Lang->{Status}},
432 {link => "?action=summary", name => $Lang->{PC_Summary}},
433 {
434 link => "?action=editConfig",
435 name => $Lang->{CfgEdit_Edit_Config},
436 priv => 1
437 },
438 {
439 link => "?action=editConfig&newMenu=hosts",
440 name => $Lang->{CfgEdit_Edit_Hosts},
441 priv => 1
442 },
443 {
444 link => "?action=adminOpts",
445 name => $Lang->{Admin_Options},
446 priv => 1
447 },
448 {
449 link => "?action=view&type=LOG",
450 name => $Lang->{LOG_file},
451 priv => 1
452 },
453 {
454 link => "?action=LOGlist",
455 name => $Lang->{Old_LOGs},
456 priv => 1
457 },
458 {
459 link => "?action=emailSummary",
460 name => $Lang->{Email_summary},
461 priv => 1
462 },
463 {
464 link => "?action=queue",
465 name => $Lang->{Current_queues},
466 priv => 1
467 },
468 @{$Conf{CgiNavBarLinks} || []},
469 );
470 my $host = $In{host};
471
472 binmode(select, ":utf8");
473 print $Cgi->header(-charset => "utf-8");
474 print <<EOF;
475 <!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN">
476 <html><head>
477 <title>$title</title>
478 <link rel=stylesheet type="text/css" href="$Conf{CgiImageDirURL}/$Conf{CgiCSSFile}?version=2" title="CSSFile">
479
480 <link rel="apple-touch-icon" sizes="180x180" href="$Conf{CgiImageDirURL}/apple-touch-icon.png?v=2">
481 <link rel="icon" type="image/png" sizes="32x32" href="$Conf{CgiImageDirURL}/favicon-32x32.png?v=2">
482 <link rel="icon" type="image/png" sizes="16x16" href="$Conf{CgiImageDirURL}/favicon-16x16.png?v=2">
483 <link rel="mask-icon" href="$Conf{CgiImageDirURL}/safari-pinned-tab.svg?v=2" color="#5bbad5">
484 <link rel="shortcut icon" href="$Conf{CgiImageDirURL}/favicon.ico?v=2">
485
486 $Conf{CgiHeaders}
487 <script src="$Conf{CgiImageDirURL}/sorttable.js"></script>
488 </head><body onLoad="document.getElementById('NavMenu').style.height=document.body.scrollHeight">
489
490 <div id="navigation-container">
491 <div id="logo-container">
492 <a href="https://backuppc.github.io/backuppc/"><img src="$Conf{CgiImageDirURL}/logo320.png"></a>
493 </div>
494 EOF
495
496 if ( defined($Hosts) && defined($host) && defined($Hosts->{$host}) ) {
497 print "<div class=\"NavMenu section-title\">";
498 NavSectionTitle("${EscHTML($host)}");
499 print <<EOF;
500 </div>
501 <div class="NavMenu host">
502 EOF
503 NavLink("?host=${EscURI($host)}", "$host $Lang->{Home}", " class=\"navbar\"");
504 NavLink("?action=browse&host=${EscURI($host)}", $Lang->{Browse}, " class=\"navbar\"") if ( !$noBrowse );
505 NavLink("?action=view&type=LOG&host=${EscURI($host)}", $Lang->{LOG_file}, " class=\"navbar\"");
506 NavLink("?action=LOGlist&host=${EscURI($host)}", $Lang->{LOG_files}, " class=\"navbar\"");
507 if ( -f "$TopDir/pc/$host/SmbLOG.bad"
508 || -f "$TopDir/pc/$host/SmbLOG.bad.z"
509 || -f "$TopDir/pc/$host/XferLOG.bad"
510 || -f "$TopDir/pc/$host/XferLOG.bad.z" ) {
511 NavLink(
512 "?action=view&type=XferLOGbad&host=${EscURI($host)}",
513 $Lang->{Last_bad_XferLOG},
514 " class=\"navbar\""
515 );
516 NavLink(
517 "?action=view&type=XferErrbad&host=${EscURI($host)}",
518 $Lang->{Last_bad_XferLOG_errors_only},
519 " class=\"navbar\""
520 );
521 }
522 if ( $Conf{CgiUserConfigEditEnable} || $PrivAdmin ) {
523 NavLink("?action=editConfig&host=${EscURI($host)}", $Lang->{CfgEdit_Edit_Config}, " class=\"navbar\"");
524 } elsif ( -f "$TopDir/pc/$host/config.pl" || ($host ne "config" && -f "$TopDir/conf/$host.pl") ) {
525 NavLink("?action=view&type=config&host=${EscURI($host)}", $Lang->{Config_file}, " class=\"navbar\"");
526 }
527 print "</div>\n";
528 }
529 print <<EOF;
530 <div class="NavMenu" id="NavMenu">
531 EOF
532 my $hostSelectbox = "<option value=\"#\">$Lang->{Select_a_host}</option>";
533 my @hosts = GetUserHosts($Conf{CgiNavBarAdminAllHosts});
534 NavSectionTitle($Lang->{Hosts});
535 if ( defined($Hosts) && %$Hosts > 0 && @hosts ) {
536 foreach my $host ( @hosts ) {
537 NavLink("?host=${EscURI($host)}", $host)
538 if ( @hosts < $Conf{CgiNavBarAdminAllHosts} );
539 my $sel = " selected" if ( $host eq $In{host} );
540 $hostSelectbox .= "<option value=\"?host=${EscURI($host)}\"$sel>$host</option>";
541 }
542 }
543 if ( @hosts >= $Conf{CgiNavBarAdminAllHosts} ) {
544 print <<EOF;
545 <select onChange="document.location=this.value">
546 $hostSelectbox
547 </select>
548 EOF
549 }
550 if ( $Conf{CgiSearchBoxEnable} ) {
551 print <<EOF;
552 <form action="$MyURL" method="get">
553 <input type="text" name="host" size="14" maxlength="64">
554 <input type="hidden" name="action" value="hostInfo"><input type="submit" value="$Lang->{Go}" name="ignore">
555 </form>
556 EOF
557 }
558 NavSectionTitle($Lang->{NavSectionTitle_});
559 foreach my $l ( @adminLinks ) {
560 if ( $PrivAdmin || !$l->{priv} ) {
561 my $txt = $l->{lname} ne "" ? $Lang->{$l->{lname}} : $l->{name};
562 NavLink($l->{link}, $txt);
563 }
564 }
565
566 print <<EOF;
567 </div>
568 </div> <!-- end #navigation-container -->
569 EOF
570
571 print("<div id=\"Content\">\n$content\n");
572 if ( defined($contentSub) && ref($contentSub) eq "CODE" ) {
573 while ( (my $s = &$contentSub()) ne "" ) {
574 print($s);
575 }
576 }
577 print($contentPost) if ( defined($contentPost) );
578 }
579
580 sub Trailer
581 {
582 print <<EOF;
583 </body></html>
584 EOF
585 }
586
587 sub NavSectionTitle
588 {
589 my($head) = @_;
590 print <<EOF;
591 <h2 class="NavTitle">$head</h2>
592 EOF
593 }
594
595 sub NavSectionStart
596 {
597 }
598
599 sub NavSectionEnd
600 {
601 }
602
603 sub NavLink
604 {
605 my($link, $text) = @_;
606 if ( defined($link) ) {
607 my($class);
608 $class = " class=\"NavCurrent\""
609 if ( length($link) && $ENV{REQUEST_URI} =~ /\Q$link\E$/
610 || length($link) && $link =~ /\&host=/ && $ENV{REQUEST_URI} =~ /\Q$link\E/
611 || $link eq "" && $ENV{REQUEST_URI} !~ /\?/ );
612 $link = "$MyURL$link" if ( $link eq "" || $link =~ /^\?/ );
613 print <<EOF;
614 <a href="$link"$class>$text</a>
615 EOF
616 } else {
617 print <<EOF;
618 $text<br>
619 EOF
620 }
621 }
622
623 sub h1
624 {
625 my($str) = @_;
626 return \<<EOF;
627 <h1>$str</h1>
628 EOF
629 }
630
631 sub h2
632 {
633 my($str) = @_;
634 return \<<EOF;
635 <h2>$str</h2>
636 EOF
637 }