Session Management with Mason

This Perl-based web helper and MySQL work together to let you quickly build a user registration system for your web site.

What Do We Store?

Just because we can store anything in
%session does not mean we
necessarily should. For instance, a site that wants to keep track
of users' names and e-mail addresses could potentially store this
information in %session. While doing so makes the information
readily available from within Mason components, it creates other
problems. For instance, it would be difficult to retrieve the rows
of “sessions” and use them to create a mass mailing to
subscribers' e-mail addresses.

For this reason, I generally use Apache::Session to store
only one value, the primary key associated with the user's row in a
Users table. (There are other ways
to accomplish the same task, such as including the user's unique
16-character ID field in the Users table and adding a “UNIQUE”
constraint on it.) If
$session{user_id} exists, then we
can assume the user has previously registered, and use that value
to retrieve other information from Users. If $session{user_id} does
not exist, then we assume the user is new to our system.

Here is one possible definition for a Users table which we
can use in this way:

We define all of the columns in this database as NOT NULL,
meaning that they are mandatory fields. Aside from the user's
unique ID (which is automatically generated by MySQL), user name
and e-mail address, we require a password and a password hint. As
we will see, these will allow us to create a full login system, and
to handle some of the problems associated with HTTP cookies.

Registration Components

Now that we have defined a Users table, it is time to define
some Mason components. Some of these components will be similar to
subroutine, and others will be similar to HTML fragments. As we saw
last month, both are acceptable (and welcome) types of Mason
components. I typically use an .html suffix on top-level components
that are visible to the user, and a .comp suffix on others—but you
may wish to set up your own conventions.

Before we do anything else, we will need a component that
allows us to connect to the database, and to retrieve a database
handle (traditionally known as $dbh). Because
Mason typically runs under mod_perl, we will take advantage of the
Apache::DBI module, which keeps a
database connection open even after an HTTP request has been
served. Reusing database connections in this way dramatically
increases the speed of our application, since logging in to a
database can be relatively slow.

Listing 2 contains a simple Mason component that connects to
the database and returns a valid $dbh. By
putting this functionality inside one component, we avoid having to
include that code inside every other component on the site.
Moreover, it means that if we have to modify the data source name
(“DSN” in Perl lingo), we can do so by changing one file.

Notice how database-connect consists solely of
<%perl> and
<%once> sections, without any HTML. This
is an example of a component that acts purely as a Perl subroutine,
returning a value to its caller. By contrast, Listing 3 contains
register-form.html, a top-level component that contains only a few
lines of Perl. The majority of register-form.html is straight HTML,
and can be written by a graphic designer, rather than a
programmer.

Registering is a relatively straightforward process.
Information typed into register-form.html is sent to register.html
(see Listing 4). The latter retrieves the name-value pairs from the
form, placing them into scalar variables using the Mason
<%args> section. If one or more elements
are missing, register.html gives the user an error message
indicating that the information needs to be updated.

If the user's registration information appears to be
complete, register.html performs a quick SELECT to ensure that the
user name will indeed be unique. True, we have defined the table
such that a user name must be unique, but we would rather produce a
nice-looking error message for our users than display an error
message from the database.

Note that this code creates a race condition; it is possible
that two users could try to register with the same user name
simultaneously. Both would be told that the user name is available,
and yet only one would be allowed to insert the requested user
name. Databases that support transactions, such as PostgreSQL, can
avoid this problem by wrapping the SELECT and
the following INSERT into a single transaction,
which can then be rolled back if there is an error.

register-form.html attempts
to be somewhat helpful, reminding users if they are already logged
in. (After all, there usually isn't any reason to register if
you're already logged in.) It uses the component get-user-info.comp
(see Listing 5), which takes one argument (a user ID) and returns a
hash reference describing the user with that ID. Since user IDs are
stored in %session with the
user_id key, we can retrieve a
hash reference with user information as follows:

If $session{user_id} is undefined—that
is, if the user has no session—then get-user-info.comp returns
undef. Otherwise, a program can retrieve information for the user
with the hash reference's keys. Indeed, the top of
register-form.html demonstrates this:

% if ($user_info) {
<P>You are currently logged in as <b><% $user_info->{username} %></b>. Do
you really want to register?</P>
% } else {
<P>You are not logged in. Go ahead and register!</P>
% }