## 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.