home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


20.9. Creating HTML Templates

Problem

You want to store a parameterized template in an external file, read it in from your CGI script, and substitute your own variables for escapes embedded in the text. This way you can separate your program from the static parts of the document.

Solution

To expand only variable references, use this template function:

sub template {
    my ($filename, $fillings) = @_;
    my $text;
    local $/;                   # slurp mode (undef)
    local *F;                   # create local filehandle
    open(F, "< $filename\0")    || return;
    $text = <F>;                # read whole file
    close(F);                   # ignore retval
    # replace quoted words with value in %$fillings hash
    $text =~ s{ %% ( .*? ) %% }
              { exists( $fillings->{$1} )
                      ? $fillings->{$1}
                      : ""
              }gsex;
    return $text;
}

On a data file like this:

<!-- simple.template for internal template() function -->
<HTML><HEAD><TITLE>Report for %%username%%</TITLE></HEAD>
<BODY><H1>Report for %%username%%</H1>
%%username%% logged in %%count%% times, for a total of %%total%% minutes.

Or use the CPAN module Text::Template to expand full expressions if you can guarantee the data file is secure from tampering. A data file for Text::Template looks like this:

<!-- fancy.template for Text::Template -->
<HTML><HEAD><TITLE>Report for {$user}</TITLE></HEAD>
<BODY><H1>Report for {$user}</H1>
{ lcfirst($user) } logged in {$count} times, for a total of 
{ int($total / 60) } minutes.

Discussion

Parameterized output for your CGI scripts is a good idea for many reasons. Separating your program from its data lets you give other people (art directors, for instance) the ability to change the HTML but not the program. Even better, two programs can share the same template, so style changes in the template will be immediately reflected in both.

For example, suppose you have stored the first template from the Solution in a file. Then your CGI program contains the definition of the template subroutine above and makes appropriate settings for variables $username , $count , and $total . You can fill in the template by simply using:

%fields = (
            username => $whats_his_name,
            count    => $login_count,
            total    => $minute_used,
);

print template("/home/httpd/templates/simple.template", \%fields);

The template file contains keywords surrounded with double percent symbols ( %%KEYWORD%% ). These keywords are looked up in the %$fillings hash whose reference was passed as the second argument to template . Example 20.7 is a more elaborate example using an SQL database.

Example 20.7: userrep1

#!/usr/bin/perl -w
# userrep1 - report duration of user logins using SQL database

use DBI;
use CGI qw(:standard);

# template() defined as in the Solution section above

$user = param("username")                   or die "No username";

$dbh = DBI->connect("dbi:mysql:connections:mysql.domain.com",
    "connections", "seekritpassword")       or die "Couldn't connect\n";
$sth = $dbh->prepare(<<"END_OF_SELECT")     or die "Couldn't prepare SQL";
    SELECT COUNT(duration),SUM(duration) 
    FROM logins WHERE username='$user'
END_OF_SELECT

# this time the duration is assumed to be in seconds
if (@row = $sth->fetchrow_array()) {
    ($count, $seconds) = @row;
} else {
    ($count, $seconds) = (0,0);
} 

$sth->finish();
$dbh->disconnect;

print header();
print template("report.tpl", {   
    'username' => $user,
    'count'    => $count,
    'total'    => $seconds 
});

If you want a fancier, more flexible solution, look at the second template in the Solution section, which relies upon the CPAN module Text::Template. Contents of braces found within the template file are evaluated as Perl code. Ordinarily, these substitutions will just be simple variables:

You owe: {$total}

but they can also include full expressions:

The average was {$count ?  ($total/$count) : 0}.

Example 20.8 is an example of how you could use that template.

Example 20.8: userrep2

#!/usr/bin/perl -w
# userrep2 - report duration of user logins using SQL database

use Text::Template;
use DBI;
use CGI qw(:standard);

$tmpl = "/home/httpd/templates/fancy.template";
$template = Text::Template->new(-type => "file", -source => $tmpl);
$user = param("username")                   or die "No username";

$dbh = 

DBI->connect("dbi:mysql:connections:mysql.domain.com",
    "connections", "secret passwd")         or die "Couldn't db connect\n";
$sth = $dbh->prepare(<<"END_OF_SELECT")     or die "Couldn't prepare SQL";
    SELECT COUNT(duration),SUM(duration) 
    FROM logins WHERE username='$user'
END_OF_SELECT

$sth->execute()                             or die "Couldn't execute SQL";

if (@row = $sth->fetchrow_array()) {
    ($count, $total) = @row;
} else {
    $count = $total = 0;
}

$sth->finish();
$dbh->disconnect;

print header();
print $template->fill_in();

The more powerful approach raises security concerns. Anyone who can write to the template file can insert code that your program will run. See Recipe 8.17 for ways to lessen this danger.

See Also

The documentation for the CPAN module Text::Template; Recipe 8.16 ; Recipe 14.10