ЭЛЕКТРОННАЯ БИБЛИОТЕКА КОАПП
Сборники Художественной, Технической, Справочной, Английской, Нормативной, Исторической, и др. литературы.



19.8 Creating a Guestbook Program

If you have followed the examples above, you can now get some simple CGI programs going. But what about harder ones? A common request is to create a CGI program to manage a guestbook, so that visitors to your web site can record their own messages.[9]

[9] As we will note later on, this application might also be called a webchat program.

Actually, the form for this kind of thing is quite easy, easier in fact than some of our ice cream forms. Other matters get trickier. But don't worry, we'll explain it all as we go.

You probably want guestbook messages to survive a user's visit to your site, so you need a file to store them in. The CGI program (probably) runs under a different user, not as you; therefore, it won't normally have permission to update a file of yours. So, first, create a file with wide-open permissions. If you're on a UNIX system, then you can do this (from your shell) to initialize a file for the guestbook program to use:

touch /usr/tmp/chatfile
chmod 0666 /usr/tmp/chatfile

Okay, but how will you accommodate several folks using the guestbook program simultaneously? The operating system doesn't block simultaneous access to files, so if you're not careful, you could get a jumbled file as everyone writes to it at the same time. To avoid this, we'll use Perl's flock function to request exclusive access to the file we're going to update. It will look something like this:

use Fcntl qw(:flock); # imports LOCK_EX, LOCK_SH, LOCK_NB
....
flock(CHANDLE, LOCK_EX) || bail("cannot flock $CHATNAME: $!");

The LOCK_EX argument to flock is what buys us exclusive file access.[10]

[10] With Perl versions prior to the 5.004 release, you must comment out the use Fcntl and just use 2 as the argument to flock.

flock presents a simple but uniform locking mechanism even though its underlying implementation varies wildly between systems. It reliably "blocks," not returning until it gets the lock. Note that file locks are purely advisory: they only work when all processes accessing a file honor the locks in the same way. If three processes honor them, but another doesn't, all bets are off.

19.8.1 Object-Oriented Programming in Perl

Finally, and most important, it's time to teach you how to use objects and classes. Although building your own object module is beyond the scope of this book, you don't have to know about that in order to use existing, object-oriented library modules. For in-depth information about using and creating object modules, see Chapter 5 of Programming Perl and the perltoot (1) manpage.

We won't go into the theory behind objects here, but you can just treat them as packages (which they are!) of wonderful and marvelous things that you invoke indirectly. Objects provide subroutines that do anything you need to do with the object.

For instance, suppose the CGI.pm module returns an object called $query that represents the user's input. If you want to get a parameter from the query, invoke the param() subroutine like this:

$query->param("answer");

This says, "Run the param() subroutine on the $query object, with "answer" as an argument." It's just like invoking any other subroutine, except that you employ the name of the object followed by the -> syntax. Subroutines associated with objects, by the way, are called methods.

If you want to retrieve the return value of the param() subroutine, just use the usual assignment statement and store the value in a regular old variable named $he_said:

$he_said = $query->param("answer");

Objects look like scalars; you store them in scalar variables (like $query in our example), and you can make arrays or hashes of objects. But you don't treat them as you would strings or numbers. They're actually a particular kind of reference,[11] but you don't even treat them as you would ordinary references. Instead, you treat them like a special, user-defined type of data.

[11] A blessed reference, to be precise.

The type of a particular object is known as its class. The class name is normally just the module name - without the .pm suffix - and often the words "class" and "module" are used interchangeably. So we can speak of the CGI module and also the CGI class. Objects of a particular class are created and managed by the module implementing that class.

You access classes by loading in a module, which looks just like any other module except that object-oriented ones don't usually export anything. You can think of the class as a factory that cranks out brand-new objects. To get the class to produce one of these new objects, you invoke special methods called constructors. Here's an example:

$query = CGI->new(); # call method new() in class "CGI"

What you have there is the invocation of a class method. A class method looks just like an object method (which is what we were talking about a moment ago), except instead of using an object to call the method, you use the name of the class as though it were itself an object. An object method is saying "call the function by this name that is related to this object"; a class method is saying "call the function by this name that is related to this class."

Sometimes you'll see that same thing written this way:

$query = new CGI; # same thing

The second form is identical in behavior to the first. It's got less punctuation, so is sometimes preferred. But it's less convenient to use as part of a larger expression, so we'll use the first form exclusively in this book.

From the standpoint of the designer of object modules, an object is a reference to a user-defined data structure, often an anonymous hash. Inside this structure is stored all manner of interesting information. But the well-behaved user of an object is expected to get at this information (to inspect or change it), not by treating the object as a reference and going straight for the data it points to, but by employing only the available object and class methods. Changing the object's data by other means amounts to hanky-panky that is bound to get you talked about. To learn what those methods are and how they work, just read the object module's documentation, usually included as embedded pods.

19.8.2 Objects in CGI.pm

The CGI module is unusual in that it can be treated either as a traditional module with exported functions or as an object module. Some kinds of programs are more easily written using the object interface to CGI.pm rather than the procedural one. A guestbook program is one of these. We access the input that the user supplied to the form via a CGI object, and we can, if we want, use this same object to generate new HTML code for sending back to the user.

First, however, we need to create the object explicitly. For CGI.pm, as for so many other classes, the method that generates objects is the class method named new().[12]

[12] Unlike C++, Perl doesn't consider new a keyword; you're perfectly free to have constructor methods called gimme_another() or fred(). But most classes end up naming their constructors new() anyway.

This method constructs and returns a new CGI object corresponding to a filled-out form. The object contains all the user's form input. Without arguments, new() builds the object by reading the data passed by the remote browser. With a filehandle as an argument, it reads the handle instead, expecting to find form input saved from previous communication with a browser.

We'll show you the program and explain its details in a moment. Let's assume that the program is named guestbook and is in the cgi-bin directory. While this program does not look like one of the two-part scripts shown earlier (where one part outputs an HTML form, and the other part reads and responds to form input from a user), you will see that it nevertheless does handle both functions. So there is no need for a separate HTML document containing a guestbook form. The user might first trigger our program simply by clicking on a link like this:

Please sign our
<A HREF="http://www.SOMEWHERE.org/cgi-bin/guestbook">guestbook</A>.

The program then downloads an HTML form to the browser and, for good measure, also downloads any previous guest messages (up to a stated limit) for the user to review. The user then fills out the form, submits it, and the program reads what is submitted. This is added to the list of previous messages (saved in a file), which is then output to the browser again, along with a fresh form. The user can continue reading the current set of messages and submitting new messages via the supplied forms as long as he wishes.

Here's the program. You might want to scan it quickly before we step you through it.

#!/usr/bin/perl -w

use 5.004;
use strict;            # enforce declarations and quoting
use CGI qw(:standard); # import shortcuts
use Fcntl qw(:flock);  # imports LOCK_EX, LOCK_SH, LOCK_NB

sub bail {             # function to handle errors gracefully
    my $error = "@_";
    print h1("Unexpected Error"), p($error), end_html;
    die $error;
}

my(
    $CHATNAME, # name of guestbook file
    $MAXSAVE,  # how many to keep
    $TITLE,    # page title and header
    $cur,      # new entry in the guestbook
    @entries,  # all cur entries
    $entry,    # one particular entry
);

$TITLE = "Simple Guestbook";
$CHATNAME = "/usr/tmp/chatfile"; # wherever makes sense on your system
$MAXSAVE = 10;

print header, start_html($TITLE), h1($TITLE);

$cur = CGI->new();                         # current request
if ($cur->param("message")) {              # good, we got a message
    $cur->param("date", scalar localtime); # set to the current time
 @entries = ($cur);                        # save message to array
}

# open the file for read-write (preserving old contents)
open(CHANDLE, "+< $CHATNAME") || bail("cannot open $CHATNAME: $!");

# get exclusive lock on the guestbook (LOCK_EX == exclusive lock)
flock(CHANDLE, LOCK_EX) || bail("cannot flock $CHATNAME: $!");

# grab up to $MAXSAVE old entries, newest first
while (!eof(CHANDLE) && @entries < $MAXSAVE) {
    $entry = CGI->new(\*CHANDLE); # pass the filehandle by reference
    push @entries, $entry;
}
seek(CHANDLE, 0, 0) || bail("cannot rewind $CHATNAME: $!");
foreach $entry (@entries) {
    $entry->save(\*CHANDLE); # pass the filehandle by reference
}
truncate(CHANDLE, tell(CHANDLE)) || 
                                 bail("cannot truncate $CHATNAME: $!");
close(CHANDLE) || bail("cannot close $CHATNAME: $!");

print hr, start_form;         # hr() emits html horizontal rule: <HR>
print p("Name:", $cur->textfield(
    -NAME => "name"));
print p("Message:", $cur->textfield(
    -NAME => "message",
    -OVERRIDE => 1,           # clears previous message
    -SIZE => 50));
print p(submit("send"), reset("clear"));
print end_form, hr;

print h2("Prior Messages");
foreach $entry (@entries) {
    printf("%s [%s]: %s",
    $entry->param("date"),
    $entry->param("name"),
    $entry->param("message"));
    print br();
}
print end_html;

Figure 19.5 shows an example screen dump after running the guestbook program.

Figure 19.5: A simple guestbook form

Figure 19.5

Note that the program begins with:

use 5.004;

If you want to run it with an earlier version of Perl 5, you'll need to comment out the line reading:

use Fcntl qw (:flock);

and change LOCK_EX in the first flock invocation to be 2.

Since every execution of the program results in the return of an HTML form to the particular browser that sought us out, the program begins by getting a start on the HTML code:

print header, start_html($TITLE), h1($TITLE);

It then creates a new CGI object:

$cur = CGI->new();                         # current request
if ($cur->param("message")) {              # good, we got a message
    $cur->param("date", scalar localtime); # set to the current time
    @entries = ($cur);                     # save message to array
}

If we are being called via submission of a form, then the $cur object now contains information about the input text given to the form. The form we supply (see below) has two input fields: a name field for the name of the user, and a message field for the message. In addition, the code shown above puts a date stamp on the form data after it is received. Feeding the param() method two arguments is a way to set the parameter named in the first argument to the value given in the second argument.

If we are not being called via submission of a form, but rather because the user has clicked on "Please sign our guestbook," then the query object we create here will be empty. The if test will yield a false value, and no entry will be added to the @entries array.

In either case, we proceed to check for any entries previously saved in our savefile. We will read those into the @entries array. (Recall that we have just now made the current form input, if any, the first member of this array.) But, first, we have to open the savefile:

open(CHANDLE, "+< $CHATNAME") || bail("cannot open $CHATNAME: $!");

This opens the file in nondestructive read-write mode. Alternatively, we could use sysopen(). This way a single call opens an old file (if it exists) without clobbering it, or else creates a new one:

# need to import two "constants" from Fcntl module for sysopen
use Fcntl qw( O_RDWR O_CREAT );
sysopen(CHANDLE, $CHATNAME, O_RDWR|O_CREAT, 0666)
    || bail "can't open $CHATNAME: $!";

Then we lock the file, as described earlier, and proceed to read up to a total of $MAXSAVE entries into @entries:

flock(CHANDLE, LOCK_EX) || bail("cannot flock $CHATNAME: $!");
while (!eof(CHANDLE) && @entries < $MAXSAVE) {
    $entry = CGI->new(\*CHANDLE); # pass the filehandle by reference
    push @entries, $entry;
}

eof is a Perl built-in function that tells whether we have hit the end of the file. By repeatedly passing to the new() method a reference to the savefile's filehandle[13] we retrieve the old entries - one entry per call. Then we update the file so that it now includes the new entry we (may) have just received:

seek(CHANDLE, 0, 0) || bail("cannot rewind $CHATNAME: $!");
foreach $entry (@entries) {
    $entry->save(\*CHANDLE); # pass the filehandle by reference
}
truncate(CHANDLE, tell(CHANDLE)) || bail("cannot truncate $CHATNAME: $!");
close(CHANDLE) || bail("cannot close $CHATNAME: $!");

[13] Actually, it's a glob reference, not a filehandle reference, but that's close enough.

seek, truncate, and tell are all built-in Perl functions whose descriptions you will find in any Perl reference work. Here seek repositions the file pointer to the beginning of the file, truncate truncates the indicated file to the specified length, and tell returns the current offset of the file pointer from the beginning of the file. The effect of these lines is to save only the most recent $MAXSAVE entries, beginning with the one just now received, in the savefile.

The save() method handles the actual writing of the entries. The method can be invoked here as $entry->save because $entry is a CGI object, created with CGI->new() as previously discussed.

The format of a savefile entry looks like this, where the entry is terminated by "=" standing alone on a line:

NAME1=VALUE1
NAME2=VALUE2
NAME3=VALUE3
=

Now it's time to return a fresh form to the browser and its user. (This will, of course, be the first form he is seeing if he has just clicked on "Please sign our guestbook.") First, some preliminaries:

print hr, start_form; # hr() emits html horizontal rule: <HR>

As already mentioned, CGI.pm allows us to use either straight function calls or method calls via a CGI object. Here, for basic HTML code, we've reverted to the simple function calls. But for generation of form input fields, we continue to employ object methods:

print p("Name:", $cur->textfield(
    -NAME => "name"));
print p("Message:", $cur->textfield(
    -NAME => "message",
    -OVERRIDE => 1, # clears previous message
    -SIZE => 50));
print p(submit("send"), reset("clear"));
print end_form, hr;

The textfield() method returns a text-input field for a form. The first of the two invocations here generates HTML code for a text-input field with the HTML attribute, NAME="name", while the second one creates a field with the attribute, NAME="message".

Widgets created by CGI.pm are by default sticky: they retain their values between calls. (But only during a single "session" with a form, beginning when the user clicks on "Please sign our guestbook.") This means that the NAME="name" field generated by the first textfield() above will have the value of the user's name if he has already filled out and submitted the form at least once during this session. So the input field we are now creating will actually have these HTML attributes:

NAME="name" VALUE="Sam Smith"

The second invocation of textfield() is a different matter. We don't want the message field to contain the value of the old message. So the -OVERRIDE => 1 argument pair says, in effect, "throw out the previous value of this text field and restore the default value." The -SIZE => 50 argument pair of textfield() gives the size of the displayed input field in characters. Other optional argument pairs beside those shown: -DEFAULT => 'initial value' and -MAXLENGTH => n, where n is the maximum number of input characters the field will accept.

Finally, we output for the user's delectation the current set of saved messages, including, of course, any he has just submitted:

print h2("Prior Messages");
foreach $entry (@entries) {
    printf("%s [%s]: %s",
    $entry->param("date"),
    $entry->param("name"),
    $entry->param("message"));
    print br();
}
print end_html;

As you will doubtless realize, the h2 function outputs a second-level HTML heading. For the rest, we simply iterate through the current list of saved entries (the same list we earlier wrote to the savefile), printing out date, name, and message from each one.

Users can sit there with the guestbook form, continually typing messages and hitting the submit button. This simulates an electronic bulletin-board system, letting them see each others' new messages each time they send off their own. When they do this, they call the same CGI program repeatedly, which means that the previous widget values are automatically retained between invocations. This is particularly convenient when creating multistage forms, such as those used in so-called "shopping cart" applications.