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


Writing Apache Modules with Perl and C
By:   Lincoln Stein and Doug MacEachern
Published:   O'Reilly & Associates, Inc.  - March 1999

Copyright © 1999 by O'Reilly & Associates, Inc.


 


   Show Contents   Previous Page   Next Page

Chapter 4 - Content Handlers / Content Handlers as File Processors
A Dynamic Navigation Bar

Many large web sites use a navigation bar to help users find their way around the main subdivisions of the site. Simple navigation bars are composed entirely of link text, while fancier ones use inline images to create the illusion of a series of buttons. Some sites use client-side Java, JavaScript, or frames to achieve special effects like button "rollover," in which the button image changes when the mouse passes over it. Regardless of the technology used to display the navigation bar, they can be troublesome to maintain. Every time you add a new page to the site, you have to remember to insert the correct HTML into the page to display the correct version of the navigation bar. If the structure of the site changes, you might have to manually update dozens or hundreds of HTML files.

Apache content handlers to the rescue. In this section, we develop a navigation bar module called Apache::NavBar. When activated, this module automatically adds a navigation bar to the tops and bottoms of all HTML pages on the site. Each major content area of the site is displayed as a hypertext link. When an area is "active" (the user is viewing one of the pages contained within it), its link is replaced with highlighted text (see Figure 4-3).

Figure 4-3. The navigation bar at the top of this page was generated dynamically by Apache::NavBar.

In this design, the navigation bar is built dynamically from a configuration file. Here's the one that Lincoln uses at his laboratory's site at http://stein.cshl.org:

# Configuration file for the navigation bar
/index.html             Home
/jade/                  Jade
/AcePerl/               AcePerl
/software/boulder/      BoulderIO
/software/WWW/          WWW
/linux/                 Linux

The right column of this configuration file defines six areas named "Home," "Jade," "AcePerl," "BoulderIO," "WWW," and "Linux" (the odd names correspond to various software packages). The left column defines the URI that each link corresponds to. For example, selecting the "Home" link takes the user to /index.html. These URIs are also used by the navigation bar generation code to decide when to display an area as active. In the example above, any page that starts with /linux/ is considered to be part of the "Linux" area and its label will be appropriately highlighted. In contrast, since /index.html refers to a file rather than a partial path, only the home page itself is considered to be contained within the "Home" area.

Example 4-6 gives the complete code for Apache::NavBar. At the end of the example is a sample entry for perl.conf (or httpd.conf if you prefer) which activates the navigation bar for the entire site.

package Apache::NavBar; 
# file Apache/NavBar.pm
use strict;
use Apache::Constants qw(:common);
use Apache::File ();
my %BARS = ();
my $TABLEATTS   = 'WIDTH="100%" BORDER=1';
my $TABLECOLOR  = '#C8FFFF';
my $ACTIVECOLOR = '#FF0000';

The preamble brings in the usual modules and defines some constants that will be used later in the code. Among the constants are ones that control the color and size of the navigation bar.

 sub handler {
   my $r = shift;
   my $bar = read_configuration($r) || return DECLINED;

The handler() function starts by calling an internal function named read_configuration(), which, as its name implies, parses the navigation bar configuration file. If successful, the function returns a custom-designed NavBar object that implements the methods we need to build the navigation bar on the fly. As in the server-side includes example, we cache NavBar objects in the package global %BARS and only re-create them when the configuration file changes. The cache logic is all handled internally by read_configuration().

If, for some reason, read_configuration() returns an undefined value, we decline the transaction by returning DECLINED. Apache will display the page, but the navigation bar will be missing.

    $r->content_type eq 'text/html'  || return DECLINED; 
my $fh = Apache::File->new($r->filename) || return DECLINED;

As in the server-side include example, we check the MIME type of the requested file. If it isn't of type text/html, then we can't add a navigation bar to it and we return DECLINED to let Apache take its default actions. Otherwise, we attempt to open the file by calling Apache::File's new() method. If this fails, we again return DECLINED to let Apache generate the appropriate error message.

    my $navbar = make_bar($r, $bar);

Having successfully processed the configuration file and opened the requested file, we call an internal subroutine named make_bar() to create the HTML text for the navigation bar. We'll look at this subroutine momentarily. This fragment of HTML is stored in a variable named $navbar.

    $r->send_http_header;
  return OK if $r->header_only;
    local $/ = "";
   while (<$fh>) {
      s:(</BODY>):$navbar$1:i;
      s:(<BODY.*?>):$1$navbar:si;
   } continue {
      $r->print($_);  
}
    return OK;
}

The remaining code should look familiar. We send the HTTP header and loop through the text in paragraph-style chunks looking for all instances of the <BODY> and </BODY> tags. When we find either tag we insert the navigation bar just below or above it. We use paragraph mode (by setting $/ to the empty string) in order to catch documents that have spread the initial <BODY> tag among multiple lines.

sub make_bar {
   my($r, $bar) = @_;
   # create the navigation bar
   my $current_url = $r->uri;
   my @cells;

The make_bar() function is responsible for generating the navigation bar HTML code. First, it recovers the current document's URI by calling the Apache request object's uri() method. Next, it calls $bar->urls() to fetch the list of partial URIs for the site's major areas and iterates over the areas in a for() loop:

   for my $url ($bar->urls) {
     my $label = $bar->label($url);
     my $is_current = $current_url =~ /^$url/;
     my $cell = $is_current ?
         qq(<FONT COLOR="$ACTIVECOLOR">$label</FONT>)
             : qq(<A HREF="$url">$label</A>);
     push @cells,
     qq(<TD CLASS="navbar" ALIGN=CENTER BGCOLOR="$TABLECOLOR">$cell</TD>\n); 
}

For each URI, the code fetches its human-readable label by calling $bar->label() and determines whether the current document is part of the area using a pattern match. What happens next depends on whether the current document is part of the area or not. In the former case, the code generates a label enclosed within a <FONT> tag with the COLOR attribute set to red. In the latter case, the code generates a hypertext link. The label or link is then pushed onto a growing array of HTML table cells.

    return qq(<TABLE $TABLEATTS><TR>@cells</TR></TABLE>\n);
}

At the end of the loop, the code incorporates the table cells into a one-row table and returns the HTML to the caller.

We next look at the read_configuration() function:

sub read_configuration {
   my $r = shift;
   my $conf_file;
   return unless $conf_file = $r->dir_config('NavConf');
   return unless -e ($conf_file = $r->server_root_relative($conf_file));

Potentially there can be several configuration files, each one for a different part of the site. The path to the configuration file is specified by a per-directory Perl configuration variable named NavConf. We retrieve the path to the configuration file with dir_config(), convert it into an absolute path name with server_root_relative(), and test that the file exists with the -e operator.

    my $mod_time = (stat _)[9];
   return $BARS{$conf_file} if $BARS{$conf_file} 
&& $BARS{$conf_file}->modified >= $mod_time; return $BARS{$conf_file} = NavBar->new($conf_file); }

Because we don't want to reparse the configuration each time we need it, we cache the NavBar object in much the same way we did with the server-side include example. Each NavBar object has a modified() method that returns the time that its configuration file was modified. The NavBar objects are held in a global cache named %BARS and indexed by the name of the configuration files. The next bit of code calls stat() to return the configuration file's modification time--notice that we can stat() the _ filehandle because the foregoing -e operation will have cached its results. We then check whether there is already a ready-made NavBar object in the cache, and if so, whether its modification date is not older than the configuration file. If both tests are true, we return the cached object; otherwise, we create a new one by calling the NavBar new() method.

You'll notice that we use a different technique for finding the modification date here than we did in Apache::ESSI (Example 4-3). In the previous example, we used the -M file test flag, which returns the relative age of the file in days since the Perl interpreter was launched. In this example, we use stat() to determine the absolute age of the file from the filesystem timestamp. The reason for this will become clear later, when we modify the module to handle If-Modified-Since caching.

Toward the bottom of the example is the definition for the NavBar class. It defines three methods named new(), urls(), and label():

package NavBar;
# create a new NavBar object
sub new {
   my ($class,$conf_file) = @_;
   my (@c,%c);
   my $fh = Apache::File->new($conf_file) || return;
  while (<$fh>) {
      chomp;
      s/^\s+//; s/\s+$//;   # fold leading and trailing whitespace
      next if /^#/ || /^$/; # skip comments and empty lines
      next unless my($url, $label) = /^(\S+)\s+(.+)/;
      push @c, $url;     # keep the url in an ordered array
      $c{$url} = $label; # keep its label in a hash
   }
   return bless {'urls'  => \@c,
                'labels' => \%c,
                'modified' => (stat $conf_file)[9]}, $class;
}

The new() method is called to parse a configuration file and return a new NavBar object. It opens up the indicated configuration file, splits each row into the URI and label parts, and stores the two parts into a hash. Since the order in which the various areas appear in the navigation bar is significant, this method also saves the URIs to an ordered array.

# return ordered list of all the URIs in the navigation bar
sub urls  { return @{shift->{'urls'}}; }
# return the label for a particular URI in the navigation bar
sub label { return $_[0]->{'labels'}->{$_[1]} || $_[1]; }
# return the modification date of the configuration file
sub modified { return $_[0]->{'modified'}; }
1;

The urls() method returns the ordered list of areas, and the label() method uses the NavBar object's hash to return the human-readable label for the given URI. If none is defined, it just returns the URL. modified() returns the modification time of the configuration file.

Example 4-6. A Dynamic Navigation Bar

package Apache::NavBar;
# file Apache/NavBar.pm
use strict;
use Apache::Constants qw(:common);
use Apache::File ();
my %BARS = ();
my $TABLEATTS   = 'WIDTH="100%" BORDER=1';
my $TABLECOLOR  = '#C8FFFF';
my $ACTIVECOLOR = '#FF0000';
sub handler {
   my $r = shift;
   my $bar = read_configuration($r) || return DECLINED;
   $r->content_type eq 'text/html'  || return DECLINED;
   my $fh = Apache::File->new($r->filename) || return DECLINED;
   my $navbar = make_bar($r, $bar);
    $r->send_http_header;
   return OK if $r->header_only;
    local $/ = "";
   while (<$fh>) {
      s:(</BODY>):$navbar$1:oi;
      s:(<BODY.*?>):$1$navbar:osi;
   } continue {
      $r->print($_);
   }
    return OK;
}
sub make_bar {
   my($r, $bar) = @_;
   # create the navigation bar
   my $current_url = $r->uri;
   my @cells;
   for my $url ($bar->urls) {
      my $label = $bar->label($url);
      my $is_current = $current_url =~ /^$url/;
      my $cell = $is_current ?
          qq(<FONT COLOR="$ACTIVECOLOR">$label</FONT>)
              : qq(<A HREF="$url">$label</A>);
      push @cells,
      qq(<TD CLASS="navbar" ALIGN=CENTER BGCOLOR="$TABLECOLOR">$cell</TD>\n);
} return qq(<TABLE $TABLEATTS><TR>@cells</TR></TABLE>\n); }
# read the navigation bar configuration file and return it as a hash.
sub read_configuration {
   my $r = shift;
   my $conf_file;
   return unless $conf_file = $r->dir_config('NavConf');
   return unless -e ($conf_file = $r->server_root_relative($conf_file));
   my $mod_time = (stat _)[9];
   return $BARS{$conf_file} if $BARS{$conf_file}
     && $BARS{$conf_file}->modified >= $mod_time;
   return $BARS{$conf_file} = NavBar->new($conf_file);
}
package NavBar;
# create a new NavBar object
sub new {
   my ($class,$conf_file) = @_;
   my (@c,%c);
   my $fh = Apache::File->new($conf_file) || return;
   while (<$fh>) {
      chomp;
      s/^\s+//; s/\s+$//;   # fold leading and trailing whitespace
      next if /^#/ || /^$/; # skip comments and empty lines
      next unless my($url, $label) = /^(\S+)\s+(.+)/;
      push @c, $url;     # keep the url in an ordered array
      $c{$url} = $label; # keep its label in a hash
   }
   return bless {'urls' => \@c,
                'labels' => \%c,
                'modified' => (stat $conf_file)[9]}, $class;
}
# return ordered list of all the URIs in the navigation bar
sub urls  { return @{shift->{'urls'}}; }
# return the label for a particular URI in the navigation bar
sub label { return $_[0]->{'labels'}->{$_[1]} || $_[1]; }
# return the modification date of the configuration file
sub modified { return $_[0]->{'modified'}; }
1;
__END__

A configuration file section to go with Apache::NavBar might read:

<Location />
 SetHandler  perl-script
 PerlHandler Apache::NavBar
 PerlSetVar  NavConf conf/navigation.conf
</Location>

Because so much of what Apache::NavBar and Apache:ESSI do is similar, you might want to merge the navigation bar and server-side include examples. This is just a matter of cutting and pasting the navigation bar code into the server-side function definitions file and then writing a small stub function named NAVBAR(). This stub function will call the subroutines that read the configuration file and generate the navigation bar table. You can then incorporate the appropriate navigation bar into your pages anywhere you like with an include like this one:

<!--#NAVBAR-->
   Show Contents   Previous Page   Next Page
Copyright © 1999 by O'Reilly & Associates, Inc.