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:

    username VARCHAR(30) NOT NULL,
    email VARCHAR(50) NOT NULL,
    password VARCHAR(20) NOT NULL,
    password_hint VARCHAR(60) NOT NULL,
    PRIMARY KEY(user_id),

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.

Listing 2

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.

Listing 3

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.

Listing 4

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.

Listing 5

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:

my $user_info = $m->comp("get-user-info.comp",
                          user_id => $session{user_id});

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>
% }