Apache::ASP Site Building

By: Joshua Chamas

published originally in PerlMonth.com in 1999

Last month, I gave a rough introduction of Apache::ASP, and why you might want to use it to build your web site. Now I get to show you Apache::ASP in action.

Requirements

First, we must decide what our site will do, or state its requirements. As a trivial site, we are going do build something my.*.com style, which holds a user's favorite links, a MyBookmarks site if you will.

This site will require a user to login with a chosen user name for security, and view, add, and delete their internet bookmarks. The deletion will leave the deleted bookmark data in the form to allow easy modification and recreation of that bookmark.

The user will also be able to logout, and the system will auto-logout their account automatically after 15 minutes, so that if it is a public terminal, another user using the same browser later will not be able modify the first user's bookmarks.

Specification

Often times, there is a specification round that we must do to pick our web application environment and hardware, as well as supported client software, but this is a no brainer here. We are choosing Apache::ASP because of its built in $Session which make user logins easy, and its built in event Session_OnEnd which will automatically destroy the contents of $Session every SessionTimeout, which defaults to 20 minutes.

Also, because our web application has more than one page, we will make use of the same headers and footers for each page, using the includes <!--#include file=src.inc--> functionality to modularize the html.

Design

Before we start coding, let's take a minute to diagram what pages and actions our MyBookmarks web application needs to have. We have 2 pages, the intro, and the actual bookmarks page, where we get to view, add, and delete the bookmark entries. We have the user login to the bookmarks, and logout, securing access for the user's eyes only.

You might also design the objects, methods, and functions that will be used for the site, but this site is so simple, that we are going to jump into implementation.

Implementation

We start by configuring .htaccess file of a directory in apache to allow Apache::ASP to run .asp files, and testing the configuration with a dummy.asp file.

# .htaccess
DirectoryIndex index.asp
<Files ~ \.asp$>
	SetHandler perl-script
	PerlHandler Apache::ASP
	PerlSetVar Global .
	PerlSetVar GlobalPackage My::Bookmarks
	PerlSetVar StateDir /tmp/asp_apps_bookmarks
	PerlSetVar Debug 2
	PerlSetVar SessionTimeout 15
	PerlSetVar StatScripts 1
	PerlSetVar AllowApplicationState 1
	PerlSetVar AllowSessionState 1
</Files>

# dummy.asp
INTRO <%=$Session%>

If the index.asp works on your server, and just prints INTRO Apache::ASP::Session=HASH(0x??????), then we know Apache::ASP is working and $Sessions are enabled.


Next, we set up the global.asa with globals and libraries that need to be initialized for the web application, and define the relevant event handlers. We also set up per request globals, like the document's title, which is something that we can do in Script_OnStart. Finally, we use the Script_OnStart and Script_OnEnd events to automatically include the header and footer for each script in our web application, and initialize relevant globals used by the scripts.

Notice that each script can process its own Logout request, which was a decision made after the design because it seemed good to make the first script, index.asp, $Session aware.

# global.asa
use File::Basename;
use DBI;
use DBD::CSV;

use vars qw( $DarkColor $Name %Titles $FontBase $Db $Title $Basename $Form $Query );

$DarkColor = '#0000aa';
$Name = "MyBookmarks";
%Titles = (
	   'index.asp' => 'Introduction',
	   'bookmarks.asp' => 'Viewer'
	  );
$FontBase = 'face=verdana,arial';

$Db = DBI->connect("DBI:CSV:f_dir=".Apache->dir_config('StateDir'), '', '', 
		   { RaiseError => 1 })
  or die "Cannot connect: " . $DBI::errstr;

# setup bookmark database if first time
unless(eval { $Db->do("select bookmark_id,username,title,url from bookmarks") }) {
    eval { $Db->do("drop table bookmarks"); };
    $Db->do(<<CREATE) || die("can't create table $DBI::errstr");
    create table bookmarks (
			    bookmark_id varchar(15),
			    username varchar(30),
			    title varchar(60),
			    url varchar(120)
			   )
CREATE
  ;
}

$Db->do("select * from bookmarks")
  || die("can't do select against bookmarks: $DBI::errstr");

sub Script_OnStart {
    $Basename = basename($0);
    $Title = $Name.' / '.$Titles{$Basename};
    $Response->Include('header.inc');
    $Form = $Request->Form();
    $Query = $Request->QueryString();
    $Response->Expires(0);

    # a user may logout from any script, destroy session, and go
    # to login / intro page
    if($Form->{logout}) {
	$Session->Abandon();
	$Response->Redirect("index.asp?abandon=".
			    ++$Application->{abandon});
    }
}

sub Script_OnEnd {
    $Response->Include('footer.inc');
}

sub Application_OnStart {
    # use max_bookmark_id as a pseudo sequence
    $Application->Lock();
    my $sth = $Db->prepare_cached
      ("select bookmark_id from bookmarks order by bookmark_id desc");
    $sth->execute();
    $Application->{max_bookmark_id} = $sth->fetchrow_array();
    $Application->UnLock();
}


Next we set up the headers and footers for each page. One problem with HTML is that it requires you to specify the unique titles of the document before the standard body style for your site, so we cheated this and created the per page titles already in the Script_OnStart of the global.asa.

# header.inc
<html>
<head><title><%=$Title%></title></head>
<body bgcolor=white link=purple alink=yellow vlink=gray>

<form src=<%=$Basename%> method=POST>
<table border=0 width=100% cellpadding=5 cellspacing=0>
<tr bgcolor=<%= $DarkColor %>>
	<td>
	<b><font <%=$FontBase%> size=+1 color=yellow>
		<%=$Title%>
		<% if($Session->{user}) { %>
		  for <%= $Session->{user} %>
		<% } %>
	</font></b>
	</td>
	<td align=right>
	<font <%=$FontBase%>>
	<% if($Session->{'user'}) { %>
		<input type=submit name=logout value=Logout>
	<% } else { %>
		&nbsp;
	<% } %>
	</font>
	</td>
</tr>
</form>
</table>

<table border=0 cellpadding=5 width=100% ><tr><td valign=top>
<font <%=$FontBase%> size=+0>

# footer.inc
</font>
</table>

<table border=0 width=100% cellpadding=5>
<tr>
	<td bgcolor=yellow align=center>
	<font <%=$FontBase%> size=-1 color=<%= $DarkColor %>>
	<b>
		My-NotExists-Bookmarks 
		Cool Technologies Etc., ???, &copy; <%= (localtime())[5] + 1900 %>
	</b>
	</font>
	</td>
</tr>
</table>
</body>
</html>


Doing the intro page should now be fairly easy. We will handle the login at the intro page, and redirect to the viewer upon success. We keep the login processing perl code at the top so we don't print out any HTML before the redirect is handled.

# index.asp
<%
# process user login
my $error;
my $user = $Form->{'user'};
if(defined $user) {
	$user =~ /^\w+$/ or $error = 
		"Your username must made of only letter and numbers";
	length($user) > 3 or $error = 
		"Your username much be at least 4 character long";
	
	unless($error) {
		$Session->{user} = $user;
		$Response->Redirect('bookmarks.asp');
	}
}
$user ||= $Session->{user};
%>
Hello, and welcome to the MyBookmarks Apache::ASP demo application.
To begin your bookmark experience, please login now:

<center>
<% if($error) { %>
	<p><b><font color=red size=-1>* <%=$error%></font></b>
<% } %>
<form src=<%=$Basename%> method=POST>
<input type=text name=user value="<%=$Server->HTMLEncode($user)%>">
<input type=submit value=Login>
</form>
</center>

This demo makes use of the Apache::ASP objects, especially
<tt>$Session</tt> and <tt>$Response</tt>, modularizes html 
via SSI file includes, and uses the <tt>Script_OnStart</tt>
and  <tt>Script_OnEnd</tt> event hooks to 
simplify common tasks done for each script in this web
application.


The final script for the site is the bookmarks.asp script, which is the most complex of the bunch. This script is in charge of viewing, adding, and deleting the user bookmarks. In order to do the bookmark modifications, the script processes its own form input.

# bookmarks.asp
<%
# only a logged in user may view the bookmarks
$Session->{'user'} || $Response->Redirect('index.asp');

my $error;
if($Form->{submit} =~ /create/i) {
	unless($Form->{new_url}) {
		$error = "The Url must be ".
			"filled in to create a new bookmark";
		goto ERROR;
	}

	my $sth = $Db->prepare_cached(
		"select url from bookmarks where username=? and url=?"
		);
	$sth->execute($Session->{'user'}, $Form->{new_url});
	if($sth->fetchrow_array) {
		$error = "You already have $Form->{new_url} ".
			"for a bookmark";
		goto ERROR;
	} else {
		$sth = $Db->prepare_cached(<<SQL);
insert into bookmarks (bookmark_id, username, url, title)
values (?,?,?,?)
SQL
	;
		$Application->Lock();
		$sth->execute(
			++$Application->{max_bookmark_id}, 
			$Session->{'user'}, 
			$Form->{new_url}, 
			$Form->{new_title}
			);
		$Application->UnLock();
	}
}

if($Query->{delete}) {
	my $sth = $Db->prepare_cached(<<SQL);

select * from bookmarks 
where bookmark_id = ?
and username = ?

SQL
	;
	$sth->execute($Query->{delete}, $Session->{user});
	if(my $data = $sth->fetchrow_hashref) {
		my $sth = $Db->prepare_cached(<<SQL);

delete from bookmarks 
where bookmark_id = ? 
and username = ?

SQL
	;
		$sth->execute($Query->{delete}, $Session->{user});
		$Form->{new_url} = $data->{'url'};
		$Form->{new_title} = $data->{'title'};
	}
}

# get all the bookmarks
ERROR:
my $sth = $Db->prepare_cached(
			"select * from bookmarks where username=? ".
			"order by bookmark_id"
			);
$sth->execute($Session->{'user'});
my @bookmarks;
while(my $bookmark = $sth->fetchrow_hashref()) {
	push(@bookmarks, $bookmark);
}
%>

<% if(@bookmarks) { %>
	Welcome to your bookmarks!
<% } else { %>
	You don't have any bookmarks.  Please feel free to 
	add some using the below form.
<% } %>

<center>
<% if($error) { %>
	<p><b><font color=red size=-1>* <%=$error%></font></b>
<% } %>
<form src=<%=$Basename%> method=POST>
<table border=0>
	<% for ('new_url', 'new_title') { 
		my $name = $_;
		my $title = join(' ', 
			map { ucfirst $_ } split(/_/, $name));
		%>
		<tr>
		<td><b><%=$title%>:</b></td>
		<td><input type=text name=<%=$name%> 
			value="<%=$Form->{$name}%>" 
			size=40 maxlength=120>
		</td>
		</tr>
	<% } %>
	<tr>
	<td>&nbsp;</td>
	<td>
		<font <%=$FontBase%>>
		<input type=submit name=submit 
			value="Create Bookmark"></td></tr>
		</font>
	</td>
</form>
</table>

<% if(@bookmarks) { 
	my $half_index = int((@bookmarks+1)/2);
	%>
	<p>
	<table border=0 width=80% bgcolor=<%= $DarkColor %> cellspacing=0>
	<tr><td align=center>

	<table border=0 width=100% cellspacing=1 cellpadding=3>
	<tr bgcolor=<%= $DarkColor %>><td align=center colspan=4>
		<font color=yellow><b>Bookmarks</b></font>
	</td></tr>
	<% for(my $i=0; $i<$half_index; $i++) { %>
		<tr>
		<% for($i, $i+$half_index) { 
			my $data = ($_ < @bookmarks) ? 
				$bookmarks[$_] : undef;
			$data->{title} ||= $data->{'url'};
			my $text = $data->{bookmark_id} ? 
				"<a href=$data->{'url'}
					>$data->{'title'}</a>" 
					: "&nbsp;";
			%> 
			<td bgcolor=#c0c0c0 width=30 align=center>
			<% if($data->{bookmark_id}) { %>
				<font size=-1><tt>
				<a href=<%=
				"$Basename?delete=$data->{bookmark_id}"
				%>>[DEL]</a>
				</tt></font>
			<% } else { %>
			  &nbsp;
			<% } %>
			</td>
			<td bgcolor=white><%= $text || '&nbsp;'%></td> 
		<% } %>
		</tr>
	<% } %>
	</table>	
	
	</td></tr></table>
	<br>
<% } %>

</center>


That's it :) If you would like you may view the MyBookmarks web application online. Next month, we will tune the MyBookmarks web application for maximum throughput, and minimum RAM usage.