By Randal L. Schwartz
One problem that seems to plague many Web programmers is how to achieve "one click" processing. No, I'm not talking about A
mazon's patented technology. I'm talking about circumventing that annoyance that occurs when a user clicks on a form's submit
button two or three times before a response is returned to the browser.
This is no big deal if the request is idempotent: that is, if a repeated request generates a consistent result, and each in
dividual request doesn't change the state of the world incrementally. However, many form submissions do intend to change the s
tate of the world somehow. For example, a shopping cart form might have a "buy this item" button, and multiple clicks might en
d up filling the cart with many copies of an item when the customer only wanted one. A survey form might return multiple votes
from a single participant. Or, multiple copies of a message might appear in a guestbook or chat room.
warnings of security holesnot to mention those evil sites with pop-up advertising windows. Some companies are even insta
But luckily, there's a simple server-side solution. Simply generate the form with a unique serial number embedded in a hidd
en field, and record that number in a lightweight server-side database. When the form is submitted, verify that the serial num
ber is still in the database, and if it is, process the form (and remove the serial number from the database). If the serial n
umber is absent, either redirect users back to the form page, or proceed to the next step if necessary, as it's likely you've
already processed the form.
I decided to take the idea a bit further. Sometimes, when a script that both processes a form and handles its response is u
pdated, a new field might be added, or the meaning of an existing field could be altered. Once this happens, if an instance of
the old form is submitted, its parameter names might no longer quite fit.
So as long as we're using a hidden field to pass a serial number to the script, let's also include the modification time of the script itself. That way, we can reject any invocations intended for an older version of the script.
Sure, an error message can be a bit annoying to the users; but it's much better than the annoyance of subtle (and potentially dangerous) mismatches
of parameter data.
Specifically, we'll be storing three things from
stat: the device number, the inode number, and the ctime valu
e. A script can't be changed without altering at least the last item (unless we change it twice within one second), so this is
pretty robust. The program that does all of this is in
Lines 1 through 3 appear at the start of nearly every CGI program I write. They turn on taint checking, warnings, and compi
ler restrictions, and unbuffer standard output.
Line 5 pulls in the CGI.pm module, with all of the functions imported into the current (main) namespace.
Lines 7 though 19 set up the lightweight database. I'm using the very slick File::Cache module from the CPAN, which writes
temporary information into /tmp in a nice, controlled way, with expiration times. The author (DeWitt Clinton) is working on an
update to this module called Cache::Cache, which offers a more generic architecture, but unfortunately, a stable version of t
hat module isn't ready as I write this column.
Lines 10 through 14 connect to the "database." By default, the serial numbers will expire in one hour, which should be enou
gh time for an unprocessed form to be filled out while still being considered valid. Note that this time is significant only f
or those forms that are presented to the user, but never submitted. Unsubmitted forms accumulate entries in the database, whi
le forms that are properly submitted clear the serial number from the database immediately.
Lines 16 through 19 handle the occasional purging of the old entries, once an hour. We don't do this every time we open the
cache or on every cache update (the default modes provided by File::Cache), because it's really more work than it needs to be
. (As an aside, I asked the author of File::Cache if he would add an expiration feature to help with this problem. He has now
included one in Cache::Cache as a configuration option, thus freeing us from having to code this functionality ourselves. Joy.)
Line 21 computes a script ID: the string that will change only when the script is edited. Calling stat on Perl's
$0 variable gets the info for the current script, from which we select the device, inode, and ctime numbers. These are then
joined into a single string.
Line 23 prints the HTTP/CGI header, the HTML header, and an H1 heading. Lines 25 to 48 handle the invocation of the script
when parameters are presentfor instance, in response to a form action. Because that happens second, I'll return to these
in a moment.
Lines 50 to 68 handle the initial invocation of the script, when it generates the form with a unique serial number. First,
the script calculates a serial number (which I'm calling
session) in lines 53 to 55. The value is created as a pa
rameter, so we can easily generate a sticky hidden field. I'm using the MD5 algorithm from Apache::Session. It's apparently su
fficient for the Apache people, so it's sufficient for me, but if you're really paranoid, check out Math::TrulyRandom in the C
Line 58 updates the database (
cache) by creating an entry keyed by the serial session ID. The value is set to
the script ID, which we'll verify when the form is submitted to ensure that the same script that generated the form is also pr
ocessing the form.
Lines 61 to 68 are your standard form stuff. The only interesting thing I've done here is to create a pop-up menu with an "
other" entry, giving users the option to key in their own text. I simply make a text field with the same name as the pop-up me
nu, and then have the script process them together as a multivalued field. We'll see how that works in a moment.
The hidden field is included as part of the form in line 67. We'll have the script look for the field before it accepts a form submission.
And as long as we're looking down here, line 72 always prints the end of the HTML.
OK, back up to the form response part, starting in line 26. First, line 30 tries to fetch the hidden serial number from the session parameter.
If that parameter is defined, lines 31 to 34 look for the session number in the database (
cache). If it's foun
d, we remove it, thus preventing any other submission with the same session key. There's a minor race condition here, but I've tried to keep these pretty close together to minimize the window. If you're concerned about that, replace the File::Cache database with a database that can handle atomic test-and-update.
Additionally, the value of that database entry must also match the script ID of the current script. If not, we've updated the script after the form was generated, so we should reject the form data intended for the older version of the script.
But, if everything matches up, then it's time to really process the data, starting in line 37.
Lines 39 and 40 grab the name, setting it to
(Unspecified) if it wasn't there or was empty.
Line 41 grabs the color. Now recall that there are actually two fields called color in the form: the pop-up menu, and the t
ext field following it. The result from
param('color') will most likely be a two-element list. Using
grep, we remove
-other- if it's present. Thus, if a color was selected from the pop-up, we now have that color p
lus perhaps whatever was typed in the box. If
-other- was selected, then we have just the value of the other box.
We'll save the first element of that result into
$color. And so, with a bit of magic, we have the pop-up color,
-other-, in which case we have the text field.
This all presumes that browsers return the values in the order specified on the form. If I recall correctly, that's merely
a common convention rather than a strict ruleso things could get messed up. (Please write me if you know otherwise, and
I'll follow up in a future column.)
Once we've retrieved the values from the form, we "process" the data, simply by having the script send it back out to the s
creen in a nice way in lines 43 and 44. In a real application, this is where the real meat would be. In our example, we
escape the values before we output them as HTML, because a user might have included less-than signs or ampersands in the input, and we wouldn't want that to mess up the output.
Lines 46 and 47 handle cases in which a form has been submitted without the proper session ID. These situations might mean
that someone doubled-clicked on the submit button; that they pressed reload on the form's results page; that the instance of t
he form was more than an hour old; that the form was generated by a prior version of this script; or that someone just faked t
he data. In any case, the script merely informs the user of the problem and provides a link to start over.
And there you have it: the secret to "one click" (no trademark here) processing, along with solutions to a couple of other
interesting issues regarding pop-up forms and script maintenance. Until next time, enjoy!
Randal (firstname.lastname@example.org) has coauthored the must-have standards: Programming Perl, Learning Perl, and Effective Perl Programming.