Chapter 9

Making a User's Life Simpler with Multipart Forms


CONTENTS


HTML forms are most programmers' first introduction to CGI scripting. Chapter 7, "Extending HTML's Capabilities with CGI," introduces formmail.pl, a CGI script that reads the contents of a form and sends it on to a designated recipient. As the complexity of the form grows, some Webmasters want to split it so that each page of the form depends upon the answers to the page before it. To build a multipart form, the concept of "state" must be added to HTTP.

Recall from Chapter 4, "Designing Faster Sites," that the Web protocol, HTTP, is stateless-that is, the server sees each request as a stand-alone transaction. When a user submits page 2 of a multipart form, the server and CGI scripts have no built-in mechanism for associating this user's page 2 with his or her page 1. These state-preserving mechanisms have to be grafted onto HTTP using any of several techniques.

Why Use Multipart Forms?

Forms often spell the difference between a mere brochure and an effective, interactive Web site. For many purposes, a simple form that accepts user feedback and e-mails it to the site owner is sufficient. But sometimes, something more is required. This section shows some examples of when multipart forms are required.

A User Survey

Every six months (around April 10 and October 10), the Graphic, Visualization, and Usability Center of Georgia Tech conducts a survey of Web users. (The results of the survey make for interesting reading; they are available at http://www.cc.gatech.edu/gvu/user_surveys/.) Each survey contains several dozen questions spanning multiple categories. For example, the second GVU survey had four categories:

While one might design this survey into one large page, that approach has the drawback that the page would be very large, requiring users to wait while it loaded.

Furthermore, the multipage questionnaire is designed to be adaptive; the answer to one question determines which questions the user will see later. Users like to be able to fill out the questionnaire over multiple sessions.

Figures 9.1 through 9.4 illustrate these features of the survey.

Figure 9.1: The GVU Welcome screen.

Figure 9.2: The GVU Section menu.

Figure 9.3: A user filling out the trigger to an adaptive question.

Figure 9.4: A user getting the adaptive response.

The GVU staff elected to implement their questionnaire as a multipart form. Each page contains hidden fields that help the software link together the responses of a user.

A detailed discussion of GVU's software approach is available at http://www.cc. gatech.edu/gvu/user_surveys/survey-09-1994/html-paper/survey_2_paper. html#method.

A Tech Support Application

During the mid-eighties, progress was made on "expert systems" to guide technical support specialists in troubleshooting and repairing complex systems. Despite excess hype and subsequent disillusionment, there has been quite a bit of success in this area. The typical diagnostic expert system starts out by asking a series of questions to get general information. It uses this information to select topics that might benefit from further inquiry. It concludes when it has enough information to recommend a course of action.

Imagine a Web site giving access to such a system to end users. Figures 9.5 through 9.8 provide an example from the automotive industry.

Figure 9.5: On the Welcome page the user identifies the major problem with the car.

The first screen (refer to Fig. 9.5) asks the user about general symptoms. Suppose the user says the car won't start. When the user submits the page, the system considers two possible scenarios:

In a typical design follow-up, questions are selected by the likelihood of failure and the "cost" of getting an answer. For example, checking to see if the fuel pump is pumping requires asking the user to open the hood and remove the air filter. Checking the electrical system may be as simple as asking the user to turn on the headlights and see if they light. In the example shown in Figure 9.6, the system decides to ask the user about the electrical system.

Figure 9.6: The Step #1 page is example of first-level hypothesis checking.

Following this logic, the system concludes that there is indeed an electrical problem. It continues to ask the user to make various tests for spark and the state of the battery and finally determines that a cable has broken in the ignition system. In Figure 9.7, the system is getting the user's help in identifying the cable.

Figure 9.7: An example of second-level hypothesis checking.

The system continues to get information from the user, such as the make, model and year of the car, until it can identify the specific part that is defective (as shown in Fig. 9.8).

Figure 9.8: On the Auto Help Recommendation page, the system offers options for getting the cable to the user.

Upon user confirmation, the system could schedule a service technician to deliver and install the cable on the customer's car.

A Membership Site

Suppose that a computer club serves members who have a variety of interests and that it would like to serve each member in such a way as to meet his or her needs. The first time a member visits the site, that member fills out a user profile that describes his or her interests (see Fig. 9.9). On subsequent visits, the member is directed to the parts of the site that best meet his or her needs. The site would continue to build and refine a user profile based on the member's interests.

Figure 9.9: The Computer Club Welcome page, as seen by a first-time visitor.

This user has identified himself as a Macintosh SE/30 user who is interested in the Internet and in computer games. The next time this user visits the site, the site has tailored itself to his interests (see Fig. 9.10).

Figure 9.10: The Computer Club Welcome page, as seen by a returning visitor.

The system knows that the user has a Mac SE/30, which has a 9-inch monochrome screen, so the system does not tell this user about games that require a large screen or color.

The system has on file an announcement that a local Internet Service Provider (ISP) is upgrading its lines to 28.8 Kbps, but the system doesn't know what speed modem this user has-so it asks him. When the system finds out that the user has a 28.8 Kbps modem, it gives the user the announcement about the ISP, but it skips the announcement that the club has made arrangements with a local computer store to offer faster modems to members at a discount (see Fig. 9.11).

Figure 9.11: The system checks the user's modem speed.

Finally, the system notes that there was a swap meet that this user might have been interested in, but the date for the swap meet is past, so the system skips that announcement (see Fig. 9.12) but leaves it on the "Other" list for the user's reference.

Figure 9.12: The system sifts through the choices.

Tip
Chapter 24, "User Profiles and Tracking," provides a fuller treatment of user profiles.

Keeping State Data in a Stateless World

Multipart forms are certainly useful, but there are certain challenges to setting them up. Recall from Chapter 4, "Designing Faster Sites," that HTTP is a stateless protocol. A client connects, GETs information, and disconnects. There are four mechanisms available for passing information from one page to the next.

PATH_INFO

Each script gets a CGI environment variable called PATH_INFO. PATH_INFO contains information that is stored after the script in the calling path. Suppose the script's URL is http://www.xyz.com/cgi-bin/myScript.cgi. The user can call http://www.xyz.com/cgi-bin/myScript.cgi/my/special/data. The script myScript.cgi will run, and PATH_INFO will contain /my/special/data.

PATH_INFO offers very little that the QUERY_STRING (described next) doesn't do as well or better. There is one circumstance, however, in which PATH_INFO is essential.

Suppose a site is using the QUERY_STRING to pass information around from page to page. A typical relative URL might be /cgi-bin/xyz/myPage.shtml?122. Now the user wants to click on a server-side, clickable imagemap. If the imagemap were run on the client side, there would be no problem, but if the client doesn't handle client-side imagemaps, this option is not available. When the browser handles the click on an image that has ISMAP set, it replaces the current query string with the coordinates of the click. Any state information in the QUERY_STRING is lost, but any information stored in the PATH_INFO is safe.

Listing 9.1 shows an example of code that passes an order ID through a server-side clickable imagemap.


Listing 9.1  smWheel.cgi-Passes State Information Through a Server-Side Imagemap with PATH_INFO

#!/usr/bin/perl
# smWheel.cgi
# arguments are x, y, order number
require "html.cgi";
require "install.inc";
($X, $Y) = split (/,/, $ARGV[0]);
($empty, $orderID ) = split (/\//, $ENV{PATH_INFO});
#$file    = $pageDirectory . "/" . $file;
# set up default
$file = "index.html";
if ($X >= 30 && $X < 90 && $Y < 30) 
{
  $file = "newsevents.html";
}
elsif ($X < 30 && $Y < 60)
{
  $file = "specials.html";
}
elsif ($X < 30)
{
  $file = "catalog.html";
}
elsif ($X >= 30 && $X < 90 && $Y > 90)
{
  $file = "feedback.html";
}
elsif ($X > 90 && $Y > 60)
{
  $file = "links.html";
}
elsif ($X > 90 && $Y < 60)
{
  $file = "chatarea.html";
} 
$file = $pageDirectory . "/" . $file;
# read the page
open (PAGE, $file) || &die ("System error: unable to read page file $file\n");
print "Content-type: text/html\n\n";
while ($input = <PAGE>)
{
  $_ = $input;
  s/"/\\"/g;
  $result = eval qq/"$_\n"/;
print $result;
}
# and close the file
close (PAGE);

This script does the following:

  1. Strips the order ID out of PATH_INFO, where it has been stored.
  2. Reads the coordinates of the click from the script argument.
  3. Uses the coordinates to decode where on the imagemap the user clicked and which file the user wants next.
  4. Once it assembles the file name, it reads the file line by line, evaluates each line, and sends the line out to the client.

Some of the lines have the embedded string $orderID, typically set up as a GET parameter. The eval changes these occurrences to the actual order number, so on the next page, the user sees links like /cgi-bin/page.cgi?188+/users/xyz/specials.html. The use of GET's QUERY_STRING to pass state is quite common, and is taken up next.

QUERY_STRING

To pass state in a query string, we need to do two things:

Note
Shopping cart scripts (also known as shopping basket scripts) are systems that allow a user to add items one at a time to an order, then checkout and purchase all of the items on the order at once. Shopping cart scripts require some form of state preservation in order to remember which user ordered what.
Shopping carts are discussed in detail in Part VII, "Advanced CGI Applications: Commercial Applications."

Listings 9.2 and 9.3 contain two routines from a shopping cart script that generate state if it's needed. Listing 9.2, catalog.cgi, shows how to assign a unique ID to the user with the QUERY_STRING environment variable.


Listing 9.2  catalog.cgi-Issues a New Order Number If Needed

#!/usr/bin/perl
# catalog.cgi
# issues a new order number if needed
require "install.inc";
require "counter.cgi";
if ($ARGV[0] !~ /\w/)
  {
     $order = &counter;
   }
else
{
     $order = $ARGV[0];
}
print "Location: $outputPage?$order+$pageDirectory/specials.html\n\n";
exit;

In catalog.cgi, $outputPage is defined in install.inc to point to page.cgi., which, in turn, works very much like smWheel.cgi-it reads in the order ID and the page to be opened from the QUERY_STRING. Then it goes through the page file, evaling each line to set $orderID to its proper value. The result of those evals is sent back to the client. Listing 9.3 shows page.cgi.


Listing 9.3  page.cgi-Runs an HTML File Through the Perl Interpreter and Displays It

#!/usr/bin/perl
# page.cgi
require 'html.cgi';
# arguments are order number and path to file
$orderID = $ARGV[0];
$file    = $ARGV[1];
# read the page
open (PAGE, $file) || &die ("System error: unable to read page file\n");
print "Content-type: text/html\n\n";
while ($input = <PAGE>)
{
#print ">$input\n";
  $_ = $input;
  s/"/\\"/g;
  $result = eval qq/"$_\n"/;
print $result;
}
# and close the file
close (PAGE);

The work of setting up a token is done in subroutine counter.

# counter.cgi
sub counter
{
#require "sys/fcntl.ph";
$debug = 0;
# use these constants for flock()
$LOCK_SH = 1;
$LOCK_EX = 2;
$LOCK_NB = 4;
$LOCK_UN = 8;
$counterfile = "counter.txt";
# Read and increment the counter file
     $result = open(LOCK,">$counterfile.lock") || 
               &die ("System error: unable to open lock file.\n");
 if ($debug) {print "$result\n";}
      # Choose flock() or our own fcntlLock, depending 
  # upon the local Unix
#     $result = flock (LOCK, $LOCK_EX);
      $result = &fcntlLock(&F_WRLCK);
      if ($debug) {print "Lock result $result\n";}
      $result = open(COUNTERFILE,"<$counterfile") || 
         &die ("System error: unable to open counter file.\n");
      if ($debug) {print "$result\n";}
  $counter = <COUNTERFILE>;
  if ($counter == undef)
  {
          # if at first you don't succeed...
  $counter = <COUNTERFILE>;
  if ($counter == undef)
  {
    &die ("System error: unable to read counter file.\n");
  }
 }
     $counter++;
     if ($counter > 1000000) {$counter = 1};
     open(COUNTERFILE,">$counterfile") || 
        &die ("System error: unable to open counter file for write.\n");
     print COUNTERFILE $counter ||
       &die ("System error: unable to write counter file.\n");
     
#    flock(LOCK,$LOCK_UN);
     &fcntlLock(&F_UNLCK);
     close(LOCK);
     close(COUNTERFILE);
     unlink("$counterfile.lock");
     # now evaluate the counter in place to return it efficiently
     $counter;
}
sub fcntlLock
{
  ($LOCKWORD) = @_;
  $arg_t = "ssllll"; #two short and four longs
  $arg [0] = $LOCKWORD;
  $arg [1] = 0;
  $arg [2] = 0;
  $arg [3] = 0;
  $arg [4] = 0; 
  $arg [5] = 0;
  $arg [6] = 0;
  $arg = pack ($arg_t, @arg);
  ($reval = fcntl (LOCK, &F_SETLKW, $arg)) || ($retval = -1);
  $retval;
}
1;

When catalog.cgi runs, it first checks to see if it has been called with an order ID as its ARGV[0]. If it has, it uses it. If it has not, it calls counter.cgi. counter.cgi locks a semaphore file and then reads and increments the counter from its lock file. Finally, it clears the semaphore and exits, returning the new order ID.

Hidden Fields

Another popular technique is to use hidden fields on forms. This technique works only when the design allows the use of forms, but of course that is what this chapter is all about. Listing 9.4 gives a sample of code from the shopping cart script. In this example, the user has just filled out a form like the one in Figure 9.13.

Figure 9.13: Orderform.html.


Listing 9.4  orderForm.cgi-Collects the User's Contact Info and Puts Up a Payment Form

#!/usr/local/bin/perl
# orderForm.cgi
#html.cgi contains routines that allow us to speak HTML.
require "html.cgi";
require "kart.cgi";
$orderID = $ARGV[0];
if ($ENV{'REQUEST_METHOD'} eq 'POST')
{
  # Using POST, so data is on standard in
  read (STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
  
  # Split the fields using '&'
  @pairs = split(/&/, $buffer);
  
  # Now split the pairs into an associative array
  foreach $pair (@pairs)
  {
    ($name, $value) = split(/=/, $pair);
    $value =~ tr/+/ /;
    $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
    $FORM{$name} = $value;
  }
  
    
  # Check for illegal metacharacters.
  if ($FORM{email} !~ /^$/ && $FORM{email} !~ /^[a-zA-Z0-9_\-+ \t\/@%.]+$/)
  {
    &html_header("Illegal Characters");
    print "Your e-mail address contains illegal \n";
    print "characters. Please notify \n";
    print "the webmaster: \n";
    print "<A HREF=\"mailto:morganm\@dse.com\">morganm\@dse.com</A>\n";
    &html_trailer;
  }
  else
  {
    # start the HTML stream back to the client
    &html_header("Order Form");
    print "<FORM METHOD=\"POST\"
    ACTION=\"http://www.dse.com/cgi-bin/dse/xyz/checkout.cgi?$orderID\">\n";
    print "Name: $FORM{name}<BR>\n\n";
    print "Address1: $FORM{address1}<BR>\n";
    print "Address2: $FORM{address2}<BR>\n";
    print "City: $FORM{city}<BR>\n";
    print "State: $FORM{state}<BR>\n";
    print "Zip: $FORM{zip}<BR>\n";
    print "Country: $FORM{country}<BR>\n";
    print "Work Phone: $FORM{workphone}<BR>\n";
    print "Home Phone: $FORM{homephone}<BR>\n";
    print "Payment Method: $FORM{payment}<BR>\n";
    if ($success == &openCart ($orderID))
    { 
      $cartCount = %cart;
      if ($cartCount != 0)
      {
        print "<PRE>\n";
        print "Item       Qty Description                    Style
              Size         Each\n";
        while (($itemID, $data) = each (%cart))
        {
         printf ("%-10s %3d %-30s %-10s %-10s \$%6.2f\n", 
               $itemNumber, $quantity, $itemDescription, $style, $size, $price);
               
        print "</PRE>\n";
        }
      }
      else
      {
         print "You have no items in order number $orderID<BR>\n";
      }
    }
    else
    {
       print "Order number $orderID does not exist.<BR>\n";
    }
    print "<INPUT TYPE=Hidden NAME=name VALUE=$FORM{name}>";
    print "<INPUT TYPE=Hidden NAME=address1 VALUE=$FORM{address1}>";
    print "<INPUT TYPE=Hidden NAME=address2 VALUE=$FORM{address2}>";
    print "<INPUT TYPE=Hidden NAME=city VALUE=$FORM{city}>";
    print "<INPUT TYPE=Hidden NAME=state VALUE=$FORM{state}>";
    print "<INPUT TYPE=Hidden NAME=zip VALUE=$FORM{zip}>";
    print "<INPUT TYPE=Hidden NAME=country VALUE=$FORM{country}>";
    print "<INPUT TYPE=Hidden NAME=workphone VALUE=$FORM{workphone}>";
    print "<INPUT TYPE=Hidden NAME=homephone VALUE=$FORM{homephone}>";
    $theTypeOfPaymentChosen = $FORM{payment};
    print "<INPUT TYPE=Hidden NAME=payment VALUE=$theTypeOfPaymentChosen>\n";
    
    # modify the following as attributes change.
    if ($theTypeOfPaymentChosen eq 'Mail')
    {
      print "Please print off order form and mail with your payment<BR>\n";
      print "<INPUT TYPE=\"submit\" VALUE=\"SendOrder\">\n";
      print "<INPUT TYPE=\"reset\" VALUE=\"Clear\"><BR>\n";
    }
    elsif ($theTypeOfPaymentChosen eq 'COD')
    {
       print "<INPUT TYPE=Hidden NAME=name VALUE=$FORM{name}>";
       print "<INPUT TYPE=Hidden NAME=address1 VALUE=$FORM{address1}>";
       print "<INPUT TYPE=Hidden NAME=address2 VALUE=$FORM{address2}>";
       print "<INPUT TYPE=Hidden NAME=city VALUE=$FORM{city}>";
       print "<INPUT TYPE=Hidden NAME=state VALUE=$FORM{state}>";
       print "<INPUT TYPE=Hidden NAME=zip VALUE=$FORM{zip}>";
       print "<INPUT TYPE=Hidden NAME=country VALUE=$FORM{country}>";
       print "<INPUT TYPE=Hidden NAME=workphone VALUE=$FORM{workphone}>";
       print "<INPUT TYPE=Hidden NAME=homephone VALUE=$FORM{homephone}>";
       print "<INPUT TYPE=Hidden NAME=payment VALUE=$theTypeOfPaymentChosen>\n";
       print "\$4.50 will be added to your total price<BR>\n";
       print "<INPUT TYPE=\"submit\" VALUE=\"SendOrder\">\n";
       print "<INPUT TYPE=\"reset\" VALUE=\"Clear\"><BR>\n";
    }
    elsif ($theTypeOfPaymentChosen eq 'CreditCard')
    {
      print "<INPUT TYPE=Hidden NAME=name VALUE=$FORM{name}>";
      print "<INPUT TYPE=Hidden NAME=address1 VALUE=$FORM{address1}>";
      print "<INPUT TYPE=Hidden NAME=address2 VALUE=$FORM{address2}>";
      print "<INPUT TYPE=Hidden NAME=city VALUE=$FORM{city}>";
      print "<INPUT TYPE=Hidden NAME=state VALUE=$FORM{state}>";
      print "<INPUT TYPE=Hidden NAME=zip VALUE=$FORM{zip}>";
      print "<INPUT TYPE=Hidden NAME=country VALUE=$FORM{country}>";
      print "<INPUT TYPE=Hidden NAME=workphone VALUE=$FORM{workphone}>";
      print "<INPUT TYPE=Hidden NAME=homephone VALUE=$FORM{homephone}>";
      print "<INPUT TYPE=Hidden NAME=payment VALUE=$theTypeOfPaymentChosen>\n";
      print "Card Number:<BR><input name=\"text\" size=\"44\"><BR>\n";
      print "Expiration Date:<BR><input name=\"text\" size=\"44\"><BR>\n";
      print "<INPUT TYPE=\"submit\" VALUE=\"SendOrder\">\n";
      print "<INPUT TYPE=\"reset\" VALUE=\"Clear\"><BR>\n";
    }
    else
    {
      print "<INPUT TYPE=Hidden NAME=name VALUE=$FORM{name}>";
      print "<INPUT TYPE=Hidden NAME=address1 VALUE=$FORM{address1}>";
      print "<INPUT TYPE=Hidden NAME=address2 VALUE=$FORM{address2}>";
      print "<INPUT TYPE=Hidden NAME=city VALUE=$FORM{city}>";
      print "<INPUT TYPE=Hidden NAME=state VALUE=$FORM{state}>";
      print "<INPUT TYPE=Hidden NAME=zip VALUE=$FORM{zip}>";
      print "<INPUT TYPE=Hidden NAME=country VALUE=$FORM{country}>";
      print "<INPUT TYPE=Hidden NAME=workphone VALUE=$FORM{workphone}>";
      print "<INPUT TYPE=Hidden NAME=homephone VALUE=$FORM{homephone}>";
      print "<INPUT TYPE=Hidden NAME=payment VALUE=$theTypeOfPaymentChosen>\n";
      print "Unknown payment method. Please notify webmaster:<A HREF=\"mailto:
      morganm\@dse.com\">morganm\@dse.com</A>\n";
    }
    &html_trailer;
  } # end of 'if (e-mail no good) then reject else process page'    
} # end if 'if METHOD==POST'
else
{
  &html_header("Error");
  print "Not started via POST\n";
  &html_trailer;
}

The finished result of the page with the credit card payment mechanism is shown in Figure 9.14.

Figure 9.14: The second half of the order form (with "credit card" selected as the payment mechanism).

Cookies

"Cookies" began life as a Netscapism, but they are now supported by a dozen or more browsers. With the continuing rise of Netscape Navigator, cookies may soon be the preferred way to keep persistent data on the Web.

To start using a cookie in a multipart form, a CGI script must ask the user's browser to set up a cookie. The script sends a header like this:

Set-Cookie: NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; secure

Let's go through these fields one at a time:

NAME

The CGI script sets the name to something meaningful for this script. In a multipart survey for the XYZ company, NAME might be set to "PRODUCT=BaffleBlaster". NAME is the only required field in Set-Cookie.

expires

Once a server asks the browser to set up a cookie, that cookie remains on the user's hard drive until the cookie expires. When the user visits the site again, the browser presents its cookie, and a CGI script can read the information stored in it. For some applications, a cookie might be useful for an indefinite period. For others, the cookie has a definite lifetime. In the example of the survey, the cookie is not useful after the survey ends. The CGI script can force the cookie to expire by sending an expiration date, using the standard HTTP date notation shown in Chapter 4, "Designing Faster Sites." For example,

print "Set-Cookie: NAME=XYZSurvey12; expires=Mon, 03-Jun-96 00:00:00 GMT;"

Once the expiration date has been reached, the cookie is no longer stored or given out. If no expiration date is given, the cookie expires when the user exits the browser. For multipart forms, it is often appropriate to leave the expires field off.

Unexpired cookies are deleted from the client's disk if certain internal limits are hit. For example, Netscape has a limit of 300 cookies, with no more than 20 cookies per path and domain. The maximum size of one cookie is 4K.

domain

Each cookie has a domain for which it is valid. When a CGI script asks a browser to set up or send its cookie, the browser compares the URL of the server with the domain attributes of its cookies. The browser looks for a tail match. That is, if the cookie domain is xyz.com, the domain will match www.xyz.com, or pluto.xyz.com, or mercury.xyz.com. If the domain is one of the seven special top-level domains, the browser expects there to be at least two periods in the matching domain. If the domain is not one of the special seven, there must be at least three periods. The seven special domains are COM, EDU, NET, ORG, GOV, MIL, and INT. Thus www.xyz.com matches xyz.com, but atl.ga.us does not match ga.us.

If no domain is specified, the browser uses the name of the server as the default domain name.

Order is important in Set-Cookie. Do not put the domain before the name, or the browser will become confused.

path

If the server domain tail-matches a cookie's domain attribute, the browser performs a path match. The purpose of path-matching is to allow multiple cookies per server. For example, a user visiting www.xyz.com might take a survey at http://www.xyz.com/survey/ and get a cookie named XYZSurvey12. That user might also report a tech support problem at http://www.xyz.com/techSupport/ and get a cookie called XYZTechSupport. Each of these cookies should set the path so that the appropriate cookie is retrieved later.

Tip
Note that, due to a defect in Netscape 1.1 and earlier, cookies that have an expires attribute must have their path explicitly set to "/" in order for the cookie to be saved correctly. As the old versions of Netscape disappear from the Net, this fact will become less significant.

Paths match from the top down. A cookie with path /techSupport matches a request on the same domain from /techSupport/wordProcessingProducts/.

By default, the path attribute is set to the path of the URL that responded with the Set-Cookie request.

secure

A cookie is marked secure by putting the word secure at the end of the request. A secure cookie will be sent only to a server offering HTTPS (HTTP over SSL). Netscape Communications offers a secure commercial server that provides HTTPS.

By default, cookies are sent in the clear over nonsecure channels.

Requesting Cookies

When the multiple cookies are returned, they are separated by "; " (a semicolon and a space). This feature makes it easy to split cookies into an array, using Perl.

Listing 9.5 shows some simple code to handle cookies. This script is adapted from work done by Jeff Carnahan of Terminal Productions.


Listing 9.5  magic.pl-Shows How to Set and Get Cookies

#!/usr/local/bin/perl
#
# - Magic Perl Program By Jeff Carnahan <tails@hooked.net> 
#   of Terminal Productions <URL:  http://www.terminalp.com/ >
#
# - Original Concept & Design Taken From: 
#   <URL:  http://http://www.illuminatus.com/cookie/ > - Thanks Andy!
#
$expDate = 'Wednesday, 09-Nov-99 23:12:40 GMT'; 
         # ^^ The Cookie Expiration Date
$thePath = '/';
         # ^^ The Minimum path for the cookie to be active on $theDomain.
$theDomain = '.terminalp.com';
         # ^^ The Domain Ending for this cookie (From: http://www.terminalp.com)

$header = "Content-type: text/html\nSet-Cookie:";
&Detect_Cookie;

sub Detect_Cookie {
  if ($ENV{'HTTP_COOKIE'} =~ /Visitor/) {
       # ^^ Does The Cookie Have A Visitor Variable?

  $visit_number = &GetMyId($ENV{'HTTP_COOKIE'});
       # ^^ Get the Number of Visit's from the Cookie Info

  &AllDone;

  } else {   # Else, No Cookie Exists For The Visitor Record!

    $theData = "Visitor=1\; expires=$expDate\; path=$thepath\;
Domain=$theDomain\n\n";

    $someText = "$header $theData";
    print $someText;
       # ^^ $someText is now the complete header we will send out...

    print "<HTML><BODY><H1>This is Your First Time Visiting!!! Thanks For
    Coming By!</H1></BODY></HTML>";
  }
}

# Get The ID Number From The Cookie Info
sub GetMyId { 
  # Put All Cookie's in an Array
  @cookieDough = split (/; /,@_[0]);

  # For Each Cookie Do:
  foreach(@cookieDough){
    # Convert Array To Hash Info              
    ($key, $val) = split (/=/,$_); 
    # Store Variable in Array Hash
    $cookieJar{$key} = $val;    
  } # ---   End FOREACH    ---

  return $cookieJar{'Visitor'}; # Return The Cookie Value "Visitor"

}  # --- End SUB "GetMyId" ---

sub AllDone {
  # Increase The Visit Number by 1.
  $visit_number++;  

  $theData = "Visitor=$visit_number\; expires=$expDate\; path=$thepath\;
Domain=$theDomain\n\n";

  $someText = "$header $theData";
  print $someText;
     # ^^ $someText is now the complete header we will send out...

  print "<HTML><BODY><H1>You have loaded this page $visit_number Times!
Thanks for coming by again!</H1></BODY></HTML>";
}

Java and JavaScript

Two new entries to the Web world are Java and JavaScript. Java is an object-oriented language developed by Sun Microsystems. It is compiled on the server and downloaded to the client as an "applet" embedded in the HTML.

JavaScript is a scripting language developed by Netscape. It is loosely based on Java, but is designed to be easier to use than Java. It is stored in source form in the HTML and executes on the client.

Note
Netscape Communications' new generation of servers, which includes FastTrack and the Enterprise Server, support a Netscape tool called LiveWire. Using LiveWire, a Webmaster can arrange for JavaScript to run on the server-the results of that execution are sent to the client.

Validating forms is a natural application for JavaScript. For example, a Web developer can use code like that shown in Listing 9.6.


Listing 9.6  validate.html-Validates a Field Using JavaScript

<HTML>
<HEAD>
<SCRIPT LANGUAGE="JavaScript">
<!--
function runValidate(form)  
{
  Ret = false;
  Ret = looksLikeEmail(form);
}

function looksLikeEmail(form) 
{
  Ctrl = form.email;
  if (Ctrl.value == "" || Ctrl.value.indexOf ('@', 0) == -1) 
  {
    PromptAndFocus (Ctrl, "Please enter a valid e-mail address.")
    return (false);
  } else
  return (true);
}

function runSubmit (form, button)  
{
  if (!runValidate(form)) 
      return;
  document.test.submit();
  return;
}

function PromptAndFocus (Ctrl, PromptStr) 
{
    alert (PromptStr);
  Ctrl.focus();
  return;
}

function loadDoc() 
{
  // initial focus; use if needed
  document.test.email.focus ();
  return;
}
//-->
</SCRIPT>
</HEAD>
<BODY onLoad="loadDoc()">
<FORM NAME="test" METHOD=POST ACTION="http://www.dse.com/cgi-bin/query" >

Enter an e-mail address (e.g. morganm@dse.com): <BR>
<INPUT TYPE="text" NAME="email">
<P>
<INPUT TYPE="button" NAME="Submit" VALUE="Submit" 
onClick="runSubmit(this.form, this)">
</FORM>
</HTML>

While this script only validates a single field (and that simplistically) the method runValidate could be extended to validate all of the forms's data, and the method looksLikeEmail could be made more sophisticated.

When this page is loaded by a JavaScript-aware browser such as Netscape Navigator 2.0, the functions are loaded into the browser's memory. Then the body loads. The onLoad attribute causes loadDoc to run, which sets the initial focus to the e-mail field.

The page then waits for the user to submit the form. If the user has left the e-mail field blank, or has not entered a string that includes an at (@) sign, the script determines that this string is not an e-mail address. It calls PromptAndFocus, which displays the error message and then moves the focus to the offending field.

When the user finally gets the input data right, runSubmit calls the submit method on the test form with

document.test.submit()

and control passes to the CGI script on the host.

Tip
Until the day comes when all browsers understand JavaScript, it is good practice to build CGI scripts to back up JavaScript. For example, because the syntax of e-mail addresses is easy to verify, the CGI script can check the e-mail address. If the operation were a complex one, the programmer might decide to pass a token to the CGI script that says, in essence, "JavaScript was here." If the CGI script sees the token, it accepts the input from JavaScript as valid. If the CGI script does not see the token, the CGI script assumes that the JavaScript did not run, and performs all the validation itself.

Adding a Buyer's Form to the Real Estate Site

Using the principles described in this chapter, you can use a multipart buyer's form to enhance the sample real estate site introduced in Chapter 1, "How to Make a Good Site Look Great."

Designing the Form

The sample form will have four pages. It will collect information from the prospective buyer, including variant pages depending upon whether the buyer is currently renting or already owns his or her own home.

The First Page and the Script That Produces It

The first page of the form, shown in Figure 9.15, captures general-purpose information such as name and address, so you can get back to the buyer. The script that handles this form, shown in Listing 9.7, checks to see if the browser handles cookies. If the cookie is empty (because the browser is cookie-challenged), the script writes the information out in hidden fields.

Figure 9.15: The first page of the buyer's form captures their contact information and whether they currently rent or own their home.


Listing 9.7  buyersForm.cgi-Processes Page 1 of the Buyers' Form

#!/usr/local/bin/perl

require "html.cgi";
require "hasCookie.cgi";

if ($ENV{'REQUEST_METHOD'} eq 'POST')
{
  read (STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
  @pairs = split(/&/, $buffer);

  # Now split the pairs into an associative array
  foreach $pair (@pairs)
  {
    ($name, $value) = split(/=/, $pair);
    $value =~ tr/+/ /;
    $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
    $FORM{$name} = $value;    
  }

  $OwnerOrRenter = $FORM{"1.OwnOrRent"};

# Find out whether there is a cookie available in the browser
# If browser is cookie-aware it returns a non-empty cookie
  if (&hasCookie)
  {
    print "Content-type: text/html\n";

# Let browser use defaults for expires, domain, and path 
    print "Set-Cookie: NAME=";
    while (($key, $value) = each %FORM)
    {
      print "\t$key\'$value";
    }
    print "\n\n";
  }

  if ($OwnerOrRenter eq 'Own')
  {
    $Title = "Questions for Homeowners";
  }
  else
  {
    $Title = "Questions for Renters";
  }

  print "<HTML>\n";
  print "<HEAD>\n";
  print "<TITLE>$Title</TITLE>\n";
  print "</HEAD>\n";
  print "<H1>$Title</H1>\n";
  print "<FORM METHOD=POST ACTION=\"/cgi-bin/dse/Buyers/step3.cgi\">\n";
  if (!&hasCookie)
  {
    while (($key, $value) = each %FORM)
    {
      print "<INPUT TYPE=Hidden NAME=$key VALUE=\"$value\">";
    }
  }
  if ($OwnerOrRenter eq 'Own')
  {
    print "<P> ";
    print "Please complete the following questions.";
    print "</P>";
    print "<DL>";
    print "<DT>My home is currently for sale:";
    print "<INPUT TYPE=Checkbox NAME=2.ForSale Value=Yes></DT>";
    print "<DT>My home has been on the market</DT>";
    print "<DD>";
    print "<INPUT TYPE=radio NAME=2.WeeksOnMarket VALUE=0  CHECKED>less than a 
    week.</DD>"; 
    print "<DD>";
    print "<INPUT TYPE=radio NAME=2.WeeksOnMarket VALUE=1-4>between one and four
    weeks.</DD>"; 
    print "<DD><INPUT TYPE=radio NAME=2.WeeksOnMarket VALUE=4-8>four to eight 
    weeks.</DD>";
    print "<DD><INPUT TYPE=radio NAME=2.WeeksOnMarket VALUE=9+>more than two 
    months.</DD>";                    
    print "<DT>I am asking \$<INPUT TEXT NAME=2.Asking  SIZE=10  ></DT>";                           
    print "<DT>I will need to sell my present home in order to purchase another."; 
    print "<INPUT TYPE=Checkbox NAME=2.NeedToSell Value=Yes></DT>";           
    print "<DT>My monthly mortgage payments are \$";  
    print "<INPUT TEXT NAME=2.MonthlyMortgage SIZE=10></DT>";
    print "<DT>I have a </DT>";
    print "<DD><INPUT TYPE=radio NAME=2.MortgageType VALUE=VA>VA loan.</DD>";
    print "<DD><INPUT TYPE=radio NAME=2.MortgageType VALUE=FHA>FHA loan.</DD>";
    print "<DD>";
    print "<INPUT TYPE=radio NAME=2.MortgageType 
    VALUE=Conventional>Conventional loan.</DD>";
    print "</DL>";
    print "<INPUT TYPE=\"Submit\" VALUE=\"Continue\">";
    print "<INPUT TYPE=\"Reset\" VALUE=\"Clear Form\">";
    print "</FORM>";
  }
  else
  {
    # must be a renter
    print "<P>";
    print "Please complete the following questions.\n";
    print "</P>";
    print "<DL>";
    print "<DT>I am currently renting,</DT>";
    print "<DD>and have a lease";
    print "<INPUT TYPE=Checkbox NAME=2.HaveALease Value=Yes></DD>";
    print "<DD>which expires<INPUT TEXT NAME=2.LeaseExpires SIZE=8></DD>";
    print "<DT>My monthly rent payment is \$<INPUT TEXT NAME=2.MonthlyRent 
    SIZE=10></DT>";             
    print "</DL>";
    print "<INPUT TYPE=\"Submit\" VALUE=\"Continue\">";
    print "<INPUT TYPE=\"Reset\" VALUE=\"Clear Form\">";
    print "</FORM>";
  }
  &html_trailer;
} 
else
{
  &die("Not started by POST\n");
}

Listing 9.8 shows the hasCookie subroutine, which checks the browser to see if it sends back an HTTP_COOKIE environment variable.


Listing 9.8  hasCookie.cgi-Checks If the Browser Is Capable of Handling Cookies

sub hasCookie
{
  $ret = 0;
  if ($ENV{'HTTP_COOKIE'} != /^$/)
  {
    $ret = 1;
  }
  $ret;
}

Reading the Results of Page 1 and Putting Up a Variant Page 2

When the user fills in page 1, the cookie (or the hidden fields) are read by the page-1 processor. If the user is a renter, a page with questions for renters is produced; otherwise, a page with questions for homeowners is output. Either way, the resulting form is submitted to step3.cgi, the code shown in Listing 9.9.


Listing 9.9  step3.cgi-Processes the Renter or Homeowner Page and Puts Up the Final Page

#!/usr/local/bin/perl

require "html.cgi";
require "hasCookie.cgi";

if ($ENV{'REQUEST_METHOD'} eq 'POST')
{
  read (STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
  @pairs = split(/&/, $buffer);

  # Now split the pairs into an associative array
  foreach $pair (@pairs)
  {
    ($name, $value) = split(/=/, $pair);
    $value =~ tr/+/ /;
    $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
    $FORM{$name} = $value;    
  }

# Find out whether there is a cookie available in the browser
# If browser is cookie-aware it returns a non-empty cookie
  if (&hasCookie)
  {
     print "Content-type: text/html\n";
     @cookie = split(/; /, $ENV{HTTP_COOKIE});
     foreach $cookie (@cookie)
     {
        ($component) = split(/; /, $cookie);
        if ($component =~ /^NAME=/)
        {
          $component =~ s/NAME=//;
          (@fields) = split(/\t/, $component);
          foreach $field (@fields)
          {
            ($aName, $aValue) = split(/\'/, $field);
            $FORM{$aName} = $aValue;
          }
          
          # if we found NAME, quit the loop
          last;
        }
     }

      # Let browser use defaults for expires, domain, and path 
      print "Set-Cookie: NAME=";
      while (($key, $value) = each %FORM)
      {
        print "$key\'$value\t";
      }
      print "\n\n";
   }

  print "<HTML>\n";
  print "<HEAD>\n";
  print "<TITLE>Housing Preferences</TITLE>\n";
  print "</HEAD>\n";
  print "<H1>Housing Preferences</H1>\n";
  print "<FORM METHOD=POST ACTION=\"/cgi-bin/dse/Buyers/step4.cgi\">\n";
  if (!&hasCookie)
  {
    while (($key, $value) = each %FORM)
    {
      print "<INPUT TYPE=Hidden NAME=$key VALUE=\"$value\">";
    }
  }
  print "<P> ";
  print "Please complete the following questions.";
  print "</P>";
  print "<DL>";
  print "<DT>I need</DT>";
  print "<DD><INPUT TEXT NAME= Bedrooms  SIZE=8> bedrooms.</DD>";
  print "<DT>and would prefer</DT>";
  print "<DD><INPUT TYPE=radio NAME=Bathrooms VALUE=1  CHECKED>One bath.</DD>";
  print "<DD><INPUT TYPE=radio NAME=Bathrooms VALUE=1.5>One and a half baths.</DD>";
  print "<DD><INPUT TYPE=radio NAME=Bathrooms VALUE=2>Two baths.</DD>";
  print "<DD><INPUT TYPE=radio NAME=Bathrooms VALUE=2.5>Two and a half baths.</DD>";
  print "<DD><INPUT TYPE=radio NAME=Bathrooms VALUE=\"3 or more\">Three or more   
  baths.</DD>";
  print "<DT>I would also like the following additional rooms and features:</DT>";
  print "<DD>";
  print "Family Room<BR>";          
  print "<INPUT TYPE=Radio NAME=FamilyRoom Value=Absolutely  CHECKED>Absolutely ";  
  print "<INPUT TYPE=Radio NAME=FamilyRoom Value=Desired>Desired "; 
  print "<INPUT TYPE=Radio NAME=FamilyRoom Value=No>Not needed<BR>";
  print "</DD>";
  print "<DD>";
  print "Eat-in Kitchen<BR>";       
  print "<INPUT TYPE=Radio NAME=EatInKitchen Value=Absolutely  CHECKED>Absolutely ";  
  print "<INPUT TYPE=Radio NAME=EatInKitchen Value=Desired>Desired  "; 
  print "<INPUT TYPE=Radio NAME=EatInKitchen Value=No>Not needed<BR></DD>";
  print "<DD>Separate Dining Room<BR>";
  print "<INPUT TYPE=Radio NAME=DiningRoom Value=Absolutely  CHECKED>Absolutely ";   
  print "<INPUT TYPE=Radio NAME=DiningRoom Value=Desired>Desired "; 
  print "<INPUT TYPE=Radio NAME=DiningRoom Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Study<BR>";                
  print "<INPUT TYPE=Radio NAME=Study Value=Absolutely  CHECKED>Absolutely ";  
  print "<INPUT TYPE=Radio NAME=Study Value=Desired>Desired "; 
  print "<INPUT TYPE=Radio NAME=Study Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Maid's Room or in-law quarters<BR>";      
  print "<INPUT TYPE=Radio NAME=Quarters Value=Absolutely  CHECKED>Absolutely ";   
  print "<INPUT TYPE=Radio NAME=Quarters Value=Desired>Desired "; 
  print "<INPUT TYPE=Radio NAME=Quarters Value=No>Not needed<BR></DD>";
  print "<DL>";
  print "<DD>With Bath";
  print "<INPUT TYPE=Checkbox NAME=QuartersWithBath Value= Yes></DD>";
  print "</DL>";
  print "<DD>";
  print "Room over the garage<BR>";
  print "<INPUT TYPE=Radio NAME=RoomOverGarage Value=Absolutely  CHECKED>Absolutely ";   
  print "<INPUT TYPE=Radio NAME=RoomOverGarage Value=Desired>Desired "; 
  print "<INPUT TYPE=Radio NAME=RoomOverGarage Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Water view<BR>";           
  print "<INPUT TYPE=Radio NAME=WaterView  Value=Absolutely  CHECKED>Absolutely ";  
  print "<INPUT TYPE=Radio NAME=WaterView  Value=Desired>Desired "; 
  print "<INPUT TYPE=Radio NAME=WaterView  Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Waterfront<BR>";           
  print "<INPUT TYPE=Radio NAME=Waterfront Value=Absolutely  CHECKED>Absolutely "; 
  print "<INPUT TYPE=Radio NAME=Waterfront Value=Desired>Desired "; 
  print "<INPUT TYPE=Radio NAME=Waterfront Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Deep water<BR>";           
  print "<INPUT TYPE=Radio NAME=DeepWater Value=Absolutely  CHECKED>Absolutely ";   
  print "<INPUT TYPE=Radio NAME=DeepWater Value=Desired>Desired ";
  print "<INPUT TYPE=Radio NAME=DeepWater Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Wooded lot<BR>";           
  print "<INPUT TYPE=Radio  NAME=WoodedLot  Value=Absolutely  CHECKED>Absolutely ";   
  print "<INPUT TYPE=Radio  NAME=WoodedLot  Value=Desired>Desired "; 
  print "<INPUT TYPE=Radio  NAME=WoodedLot  Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Car port<BR>";            
  print "<INPUT TYPE=Radio  NAME=CarPort  Value=Absolutely  CHECKED>Absolutely ";  
  print "<INPUT TYPE=Radio  NAME=CarPort  Value=Desired>Desired "; 
  print "<INPUT TYPE=Radio  NAME=CarPort  Value=No>Not needed<BR ></DD>";
  print "<DD>";
  print "Single Garage<BR>";        
  print "<INPUT TYPE=Radio  NAME=SingleGarage  Value=Absolutely  CHECKED>Absolutely ";  
  print "<INPUT TYPE=Radio  NAME=SingleGarage  Value=Desired>Desired ";  
  print "<INPUT TYPE=Radio  NAME=SingleGarage  Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Double Garage<BR>";        
  print "<INPUT TYPE=Radio  NAME=DoubleGarage  Value=Absolutely  CHECKED>Absolutely ";   
  print "<INPUT TYPE=Radio  NAME=DoubleGarage  Value=Desired>Desired ";   
  print "<INPUT TYPE=Radio  NAME=DoubleGarage  Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Air Conditioning<BR>";     
  print "<INPUT TYPE=Radio  NAME=AC  Value=Absolutely  CHECKED>Absolutely ";  
  print "<INPUT TYPE=Radio  NAME=AC  Value=Desired>Desired ";   
  print "<INPUT TYPE=Radio  NAME=AC  Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Central Air<BR>";          
  print "<INPUT TYPE=Radio  NAME=CentralAir  Value=Absolutely  CHECKED>Absolutely ";   
  print "<INPUT TYPE=Radio  NAME=CentralAir  Value=Desired>Desired ";   
  print "<INPUT TYPE=Radio  NAME=CentralAir  Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Pool<BR>";
  print "<INPUT TYPE=Radio  NAME=Pool  Value=Absolutely  CHECKED>Absolutely ";   
  print "<INPUT TYPE=Radio  NAME=Pool  Value=Desired>Desired ";   
  print "<INPUT TYPE=Radio  NAME=Pool  Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Tennis Court<BR>";         
  print "<INPUT TYPE=Radio  NAME=TennisCourt  Value=Absolutely  CHECKED>Absolutely ";   
  print "<INPUT TYPE=Radio  NAME=TennisCourt  Value=Desired>Desired ";  
  print "<INPUT TYPE=Radio  NAME=TennisCourt  Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Townhouse<BR>";            
  print "<INPUT TYPE=Radio  NAME=Townhouse  Value=Absolutely  CHECKED>Absolutely ";   
  print "<INPUT TYPE=Radio  NAME=Townhouse  Value=Desired>Desired ";   
  print "<INPUT TYPE=Radio  NAME=Townhouse  Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Condominium<BR>";          
  print "<INPUT TYPE=Radio  NAME=Condominium  Value=Absolutely  CHECKED>Absolutely ";  
  print "<INPUT TYPE=Radio  NAME=Condominium  Value=Desired>Desired "; 
  print "<INPUT TYPE=Radio  NAME=Condominium  Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Single-family Home<BR>";   
  print "<INPUT TYPE=Radio  NAME=SingleFamily  Value=Absolutely  CHECKED>Absolutely "; 
  print "<INPUT TYPE=Radio  NAME=SingleFamily  Value=Desired>Desired "; 
  print "<INPUT TYPE=Radio  NAME=SingleFamily  Value=No>Not needed<BR></DD>";
  print "<DD>";
  print "Fenced Yard<BR>";          
  print "<INPUT TYPE=Radio  NAME=FencedYard  Value=Absolutely  CHECKED>Absolutely ";   
  print "<INPUT TYPE=Radio  NAME=FencedYard  Value=Desired>Desired ";  
  print "<INPUT TYPE=Radio  NAME=FencedYard  Value=No>Not needed<BR></DD>";
  print "</DL>";
  print "<P>";
  print "<INPUT TYPE=checkbox  NAME=MailingList  VALUE=Yes CHECKED>Please put me on  
  your mailing list for new"; 
  print " developments in residential real estate.";
  print "</P>";
  print "Please enter any additional information you want us to know.<BR>";
  print "<TEXTAREA NAME= comments  ROWS=8 COLS=40></TEXTAREA>";
  print "<P>";
  print "<INPUT TYPE=submit  VALUE=\"Send Form\">";
  print "<INPUT TYPE=reset  VALUE=\"Clear Form\">";
  print "</FORM>";
  print "</FORM>";
  &html_trailer;
} 
else
{
  &die("Not started by POST\n");
}

Note that, if the contact information from the last form is stored in the cookie, it is pulled out and put into the associative array FORM. This way, the rest of the processing is the same whether cookies were available or not. Until cookies become universal, there is not much advantage in using cookies, since each script has to handle the non-cookie case as well. As the number of visitors who have cookie-aware browsers like Netscape Navigator 2.0 continues to increase, the Webmaster may decide to drop support for non-cookie-aware browsers. When that happens, this code can be simplfied.

Reading the Variant Pages

If the user is a renter, you can capture relevant information into the cookie, or the hidden fields, as shown in step3.cgi. The renters page is shown in Figure 9.16. Then send out page 3, which asks a list of questions about the buyer's housing preferences. The same script reads the homeowner's; it captures the information and transfers control to page 3.

Figure 9.16: The Renter's page of the Buyer's form only appears if the visitor identifies himself as a renter on page 1.

On the first two pages the fields are numbered (for example, 1.email) so that, when they are sent to the site owner, a simple sort will put the fields in a meaningful order. A more sophisticated backend can be developed if the Webmaster is willing to give up some generality.

Processing the Final Page

Page 3 pulls all the information out of the cookie (or the hidden fields), combines it with the data from this form, and sends the combined data to the realtor. This is shown in Listing 9.10.


Listing 9.10  step4.cgi-Puts All the Stored Information Together and Sends It to the Site Owner

#!/usr/local/bin/perl
# step4.cgi

require "html.cgi";
require "ctime.pl";
require "hasCookie.cgi";

# Point this e-mail address at the person who should get the visitor's info
$recipient = "ckepilino@dse.com";

# After sending the e-mail to the recipient, transfer control to the $Thanks page.
$Thanks = "http://www.dse.com/Buyers/Thanks.html";

if ($ENV{REQUEST_METHOD} eq 'POST') 
{
  read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
  @pairs = split(/&/, $buffer);
  foreach $pair (@pairs)
  {
    ($name, $value) = split(/=/, $pair);
    $value =~ tr/+/ /;
    $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
    $FORM{$name} = $value;
  }
  if (&hasCookie)
  {    
    # Read the cookie into FORM so we can send it out
    @cookie = split(/; /, $ENV{HTTP_COOKIE});
    foreach $cookie (@cookie)
    {
      ($component) = split(/; /, $cookie);
      if ($component =~ /^NAME=/)
      {
        $component =~ s/NAME=//;
        (@fields) = split(/\t/, $component);
        foreach $field (@fields)
        {
          ($aName, $aValue) = split(/\'/, $field);
          $FORM{$aName} = $aValue;
        }
          
        # if we found NAME, quit the loop
        last;
      }
   }
  }
  open (MAIL, "| sendmail -t");
  print MAIL "To: $recipient\n";
  { 
   if ($FORM{'1.email'} ne "")
   {
     print MAIL "From: $FORM{'1.firstName'} $FORM{'1.lastName'} <$FORM{'1.email'}>\n";
     print MAIL "Reply-to: $FORM{'1.email'}\n";
   }
   foreach $key (sort keys %FORM)
   {
     print MAIL "$key is $FORM{$key}\n";
   }
   print "Location: $Thanks\n\n";   
  } 
}
else
{
  &die("Not started using POST\n");
}

Figure 9.17: The last page of the buyer's form.