Chapter 27

Multipage Shopping Environment


CONTENTS


This chapter provides a detailed example and complete set of scripts for implementing a shopping cart. These scripts have been sold commercially and are now available exclusively on the CD-ROM at the back of this book.

This chapter describes the technical details of those scripts so that the reader can maintain the system and make modifications as desired. Many variants of the shopping cart system are possible, building upon the foundation of these scripts.

Installing the Shopping Cart Scripts

To install these scripts on a UNIX machine, follow the instructions in the README file on the CD-ROM. Be sure to set up the entries in the install.inc file to reflect local file names and preferences. The complete installation kit includes the following CGI scripts:

Managing the User ID

Recall from Chapter 26, "Shopping Baskets," that when users first arrive in the shopping area, they are issued a unique identifier. This identifier must be passed along as users move from page to page until they complete their order.

Assigning the User ID

The user enters the shopping area through the CGI script catalog.cgi. catalog.cgi issues a unique ID based on the output of counter.cgi (see Listing 27.2). install.inc contains several parameters that are used throughout the script. It provides one convenient place where nearly every item that should be set at install-time can be accessed.

catalog.cgi

If catalog.cgi is called with an existing ID as an argument, that ID is used (see Listing 27.1). This way, users who come back through the first page do not inadvertently get separated from their ID.


Listing 27.1  catalog.cgi-Issues Each User a Unique Transaction ID

#!/usr/bin/perl
#@(#)catalog.cgi  1.2
# Copyright 1995-1996 DSE, Inc.
# All Rights Reserved

require "install.inc";
require "counter.cgi";

if ($ARGV[0] !~ /\w/)
  {
     $order = &counter;
   }
else
{
     $order = $ARGV[0];
}

print "Location: $outputPage?$order+$pageDirectory/catalog1.html\n\n";
exit;

Once the visitor has an order ID, the script redirects the user to the first catalog page, using a redirection script (page.cgi, referenced in $outputPage) that substitutes the order ID for the string $order.

counter.cgi

If catalog.cgi determines that the user does not have a unique ID yet, it calls counter.cgi. There are many different ways to generate a unique ID. Some scripts call a random number generator. Others use the date and time. (In UNIX, the current date/time is represented by the number of seconds since midnight, January 1, 1970-a moment known as the epoch.)

Both of these methods suffer from the fact that it's possible for two users to get the same ID. Although it's unlikely in any one instance, given enough installed copies and the size of the Web, it becomes almost inevitable that someone, somewhere, will be issued an ID that is already active on that machine.

A better solution would be to use a number guaranteed to be unique, such as the date/time followed by the process ID or a unique count. The system described in this chapter follows the latter approach. When the system is installed, the counter file is seeded with a starting number (typically one). Each time the counter is run, the counter file is read and its contents are incremented.

When you use this approach, there is still a possibility that two users could attempt to access the counter file at the same time. This possibility should be eliminated if possible. Some systems provide a mechanism called a semaphore that restricts access to critical sections of code to one process (or user) at a time.

To make semaphores work correctly they must be atomic-that is, it must be possible to read them and set them in one single action. To see why this is so, consider what happens if we use a nonatomic semaphore:

Time
Action
0
User 1 reads the semaphore and finds it clear
1
User 2 reads the semaphore and finds it clear
2
User 1 sets the semaphore and proceeds
3
User 2 sets the semaphore and proceeds

Two users are now simultaneously accessing the critical section of code-the very thing we hoped to prevent.

Atomic semaphores have been available in UNIX for years. Unfortunately, the system call that implements them was built one way in the System V flavor of UNIX and a different way in the Berkeley flavor, so the exact code depends upon the heritage of the local UNIX.

POSIX.1 and XPG3 (two major open standards for UNIX), as well as System V, specify a call named fcntl that includes atomic locking among its capabilities. 4.3BSD specifies flock, a call specifically included to provide file locking.

flock is a simpler call than fcntl (particularly in Perl) so it is used in the following example. To run this code on a machine that doesn't support flock, comment out the calls to flock and uncomment the calls to fcntl.


Listing 27.2  counter.cgi-Generates the ID

#@(#)counter.cgi  1.2
# Copyright 1995-1996 DSE, Inc.
# All Rights Reserved

sub counter
{
#require "fcntl.ph";
$debug = 0;

$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";}
     $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 shorts 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;

Note that the file-locking mechanism operates on a separate lock file, not on the counter text file itself. The intent is to provide a layer of insulation between the locking mechanism and the protected file so that changes to the locking mechanism do not affect the counter file.

Passing the ID from Page to Page

Once users leave catalog.cgi, they move to the first product page in the site. The product page may have a single product or more than one. If the product information is simple, all the information can be stored in the URL. If the user has options to choose, the page can be implemented as a series of forms. Listing 27.3 shows the HTML for a simple product:


Listing 27.3  product.html-Adding a Simple Product to the Cart

<P>Here is a description of the product. It is a fine product.
</P>
<A HREF="/cgi-bin/ShoppingCart/update.cgi?$
orderID+1+Item1+Fine%20Item+19.95">Put Item 1 in my cart.</A>

The fields in this URL are as follows:

Note
Remember that this is a worldwide Web. Unless otherwise stated, prices are usually given in U.S. dollars, abbreviated to USD. Don't make users guess. Tell them the price and the currency.

If the product comes with various options such as sizes, colors, or styles, it is often appropriate to give users the choice right on the product page as they are placing their order. The following code shows how to do this:

<P>Here is a description of the product. It is a fine product.
</P>
<FORM METHOD=POST ACTION="/cgi-bin/ShoppingCart/update.cgi">
<INPUT TYPE=Hidden NAME=orderID VALUE=$orderID>
<INPUT TYPE=Text NAME=NewQuantity VALUE=1>Quantity<BR>
<INPUT TYPE=Hidden NAME=NewItemNumber VALUE=Item1>
<INPUT TYPE=Hidden NAME=NewItemDescription VALUE="Fine Item">
<INPUT TYPE=Radio NAME=NewItemStyle VALUE=Blue CHECKED>Blue<BR>
<INPUT TYPE=Radio NAME=NewItemStyle VALUE=Green>Green<BR>
<INPUT TYPE=Radio NAME=NewItemStyle VALUE=Red>Red<BR>
<INPUT TYPE=Radio NAME=NewItemSize VALUE=Small>Small<BR>
<INPUT TYPE=Radio NAME=NewItemSize VALUE=Medium CHECKED>Medium<BR>
<INPUT TYPE=Radio NAME=NewItemSize VALUE=Large>Large<BR>
<INPUT TYPE=Hidden NAME=NewPrice VALUE="19.95">
<INPUT TYPE=Submit VALUE=Order>
</FORM>

Putting Up Product Pages

Note in the previous code the use of the embedded Perl variable $orderID. Before these scripts work, that variable must be replaced with the user's real order ID. To do this, all HTML pages in this package are sent to the user through a script called page.cgi, shown in Listing 27.4


Listing 27.4  page.cgi-Putting Up All Pages Using page.cgi

#!/usr/bin/perl
#@(#)page.cgi     1.3
# Copyright 1995-1996 DSE, Inc.
# All Rights Reserved

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);

There are only two lines of Perl magic in this script:

s/"/\\"/g;
$result = eval qq/"$_\n"/;

Sending a double-quoted string like VALUE='$orderID' through eval can confuse eval, so the first line replaces the double quotation marks with backslash double quotation marks. These escaped double quotation marks cause no problem in eval. The qq/ in the second line is Perl's generalized double-quoting mechanism. eval views the line as double-quoted without having to add still more layers of escaped double quotation marks.

The upshot of these two lines is that a line with no Perl variables passes from $input to $result unchanged, but a line like

<INPUT TYPE=Hidden NAME=orderID VALUE=$orderID>

is transformed into something like

<INPUT TYPE=Hidden NAME=orderID VALUE=245>

which is, of course, exactly what is needed at the client's end of the transaction.

Caution
Using eval on input that comes from the user exposes the site to a security risk. One way to reduce the risk is to verify that the REFERER site is valid, as shown in formmail.pl in Chapter 7, "Extending HTML's Capabilities with CGI." If still more security is needed, consider the solutions described Chapter 28, "Fully Integrated Shopping Environment."

Caution
Embedding the price in the HTML opens the site to the prospect that a malicious user could build a version of the form with a low price. Checking the REFERER variable will help, but the best practice is to invoice based only on the item number, not on any information t hat comes from the HTML page.

Putting Items in Carts

Once the HTML pages are set up with product information and a proper order ID, it is up to the user to put an item in the cart. As shown above, the work of updating the cart is done by the update.cgi script shown in Listing 27.5.


Listing 27.5  update.cgi-The Cart Is Updated by update.cgi

#!/usr/local/bin/perl
#@(#)update.cgi   1.5
# Copyright 1995-1996 DSE, Inc.
# All Rights Reserved

# html.cgi contains routines that allow us to speak HTTP
require "html.cgi";

# kart.cgi contains routines to open and close the shopping cart
require "kart.cgi";
require "install.inc";

# make sure arguments are passed using the POST method
if ($ENV{'REQUEST_METHOD'} eq 'POST' )
{
   read (STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
   
   # Split the name-value pairs on '&'
   @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;
   }
}
elsif ($ENV{'REQUEST_METHOD'} eq 'GET' )
{
  # Get the query in phrase1+phrase2 format from the QUERY_STRING
  # environment variable.
  $query = $ENV{'QUERY_STRING'};

  #if the query string is null, then there is no data appended to the URL.

  if ($query !~ /\w/)
  {
    # No argument
    &die('Error: No product data found.');
   }
   else
   {
        $query =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
        ($FORM{'orderID'}, $FORM{'NewQuantity'}, $FORM{'NewItemNumber'}, 
          $FORM{'NewItemDescription'}, $FORM{'NewPrice'}, $FORM{'nextPage'}) =
               split('\+',$query);

     # the GET version doesn't use Style and Size
     $FORM{'NewItemStyle'} = "None";
     $FORM{'NewItemSize'} = "None";
    }
}
else
{
  &die "Not started via POST or GET; REQUEST_METHOD is $REQUEST_METHOD \n";
}

# and here is where we do the work
if ($success == &openCart ($FORM{'orderID'}))
{
  workingItemNumber = $FORM{'NewItemNumber'} ."\t". $FORM{'NewItemStyle'} ."\t". 
     $FORM{'NewItemSize'};
     
  # do the update
  $cart{$workingItemNumber} = 
          ($FORM{'NewItemStyle'} . "\t" . $FORM{'NewItemSize'} . "\t" . 
          $FORM{'NewQuantity'} . "\t" . $FORM{'NewItemDescription'} . "\t" . 
          $FORM{'NewPrice'} );

  # and write it out
  $result = &closeCart($FORM{'orderID'});
  if ($success == $result)
  {
     if ($FORM{'nextPage'} !~ /^$/)
          {
       print "Location: $upcgipath?$FORM{'orderID'}+$FORM{'nextPage'}\n\n";
          }
     else
     {
       print "Location: $ENV{HTTP_REFERER}\n\n";
     }
   }
   else
   {
     &html_header('Error');
          print "Could not close cart file.\n";
     &html_trailer;
    }
}
else
{
     &html_header('Error');
        print "Could not access cart file.\n";
     &html_trailer;
}

Several points are worth noting in this script. First, it can be started either by POST or by GET. If it is started by GET, it makes "dummy" style and size variables. These lines could be changed if a Webmaster wanted to have a link that directly selected a particular size or style (such as during a sale).

Second, note that the implementation of the cart is hidden. The cart routines guarantee that following an openCart, the associative array cart will hold the contents of the cart.

Tip
In programming, it is a good idea to keep as much of the implementation hidden as possible. This way, a maintenance programmer can improve the implementation without breaking the rest of the code, so maintenance costs are lower.

Be sure to close the cart using closeCart after the contents are updated. This design style allows a developer to change the implementation of cart without having to rewrite routines like update.cgi.

Most merchants use one item number to refer to a number of different styles and sizes. Thus, the key field to the cart associative array is the string formed from the combination of the item ID, the style, and the size (separated by tabs).

Note that the style and size are also included in the cart itself. In this way, an implementation could change the definition of the key field without having to change other parts of the code.

Finally, note the optional field nextPage. If this value is defined (either from GET or POST), then after the script runs the user is delivered to the specified page. If nextPage is undefined, the user is returned to the referring page (typically a product or catalog page).

Note
Recall from Chapter 4, "Designing Faster Sites," that the HTTP server adds various headers to the output before sending it back to the client. Some Webmasters like to avoid the overhead of having the server parse their output and supply these headers-they build their own valid HTTP headers inside the script. To tell the server not to parse the Webmaster's output and add the necessary headers, change the name of a script so that it begins with nph- (which stands for No-Parse Headers).
If you choose to use the nph- option, consider sending back a status code 204 when nextPage is to refresh the page-the client will not expect to see any data, and will leave the user on the old page. The formal definition of status code 204,from
http://www.w3.org/hypertext/WWW/Protocols/HTTP/HTRESP.html. is
"Server has received the request but there is no information to send back, and the client should stay in the same document view. This is mainly to allow input for scripts without changing the document at the same time."
That situation seems to apply here

.

Passing Through Imagemaps with PATH_INFO

As the site grows, many site owners will want to include an imagemap. Starting with Netscape 2.0, imagemaps are supported on the client. To hook up a client-side imagemap, just be sure that the destination page is called through page.cgi.

Calling a server-side imagemap is more difficult because the map coordinates are appended to the map name, replacing any information already put there by the shopping cart routines.

If the server is available in source form (such as the NCSA or Apache servers) you could modify image map.c, the built-in CGI program for handling server-side imagemaps.

For simple imagemaps, however, note that the solution shown in Listing 27.6 works and that only rectangles are supported. Note that the coordinates are intentionally hardcoded-modify them to meet the requirements of the local map.


Listing 27.6  imagemap.cgi-This Script Follows a Simple Server-Side Imagemap Inside the Shopping Area

#!/usr/bin/perl
#@(#)imagemap.cgi 1.2
# Copyright 1995, 1996 DSE, Inc. 
# All rights reserved

# arguments are x, y, and (in the PATH_INFO) order number
require "html.cgi";
require "install.inc";
($X, $Y) = split (/,/, $ARGV[0]);
($empty, $orderID ) = split (/\//, $ENV{PATH_INFO});

# set up default
$file = "index.html";
if ($X >= 30 && $X < 90 && $Y < 30) 
{
  $file = "oneCatalogPage.html";
}
elsif ($X < 30 && $Y < 60)
{
  $file = "specials.html";
}
elsif ($X < 30)
{
  $file = "anotherCatalogPage.html";
}
elsif ($X >= 30 && $X < 90 && $Y > 90)
{
  $file = "yetAnotherCatalogPage.html";
}
elsif ($X > 90 && $Y > 60)
{
  $file = "anotherPartOfTheCatalog.html";
}
elsif ($X > 90 && $Y < 60)
{
  $file = "stillAnotherPartOfTheCatalog.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 is, in essence, a modification of page.cgi. The script passes the order ID in the PATH_INFO. The map coordinates are passed in the arguments as usual. Using the map coordinates and a series of overlapping rectangles, the script decodes which of several files is the destination and then calls the same code that page.cgi uses to display the specified page.

To use PATH_INFO with a mapped image, put lines like these on the HTML page:

<A HREF="/cgi-bin/ShoppingCart/imagemap.cgi/$orderID">
<IMG WIDTH=122 HEIGHT=121 BORDER=0 ISMAP SRC="Graphics/mappedImage.gif"></A>

Passing into the Checkout Pages

From time to time, users will want to review their order. (The code for that page is shown later in this chapter.) After reviewing their order, users may elect to check out.

If there is more than one payment option, the Webmaster may opt to have one page to capture the information common to all payment options (such as name, address, and phone) and have other pages to capture information specific to each payment option (such as credit card number or account information).

When the user enters the checkout area, there is much more information to be preserved between pages. Not only must the system remember the order ID, but it must also keep track of all the information the user entered on the common information page. Since these pages are forms, the easiest way to maintain this information is as hidden fields.

When the user chooses Check Out from the Review page, the shopping cart system invokes page.cgi and passes the order ID and the destination page (in this case, orderForm.html) just as it does when moving from one product page to another.

The HTML page orderForm.html, shown in Figure 27.1, collects all the common information and allows the user to choose a payment method.

Figure 27.1 : All common information is entered on orderForm.html.

When the user submits the form, the script runs orderForm.cgi as shown in 27.7.


Listing 27.7  orderForm.cgi-This Script Processes the Common Information

#!/usr/local/bin/perl
#@(#)orderForm.cgi      1.7
# Copyright 1995, 1996 DSE, Inc. 
# All rights reserved

#html.cgi contains routines that allow us to speak HTML.
require "html.cgi";
require "kart.cgi";
require "install.inc";

$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:$webmaster\">$webmaster</A>\n";
    &html_trailer;
  }
  else
  {
   # make sure required data is entered
   if (($FORM{name} !~ /^$/) && 
      (($FORM{address1} !~ /^$/) ||($FORM{address2} !~ /^$/)) &&
      ($FORM{city} !~ /^$/) &&
      ($FORM{zip} !~ /^$/) && ($FORM{country} !~ /^$/) &&
      ($FORM{state} !~ /^$/) && 
      (($FORM{workphone} !~ /^$/) ||($FORM{homephone} !~ /^$/)) && 
      ($FORM{payment} !~ /^$/) )
   {
     # start the HTML stream back to the client
     &html_header("Order Form");
     print "<FORM METHOD=\"POST\" ACTION=\"/cgi-bin/ShoppingCart/
     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";
     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";
     print "<INPUT TYPE=Hidden NAME=ups VALUE=\"$UPS\">\n";
    
     if ($success == &openCart ($orderID))
     {  
       if (%cart == 0)
       {
         print "<HR><STRONG>But your cart is empty!</STRONG><BR>\n";
         print "Go back to the <A HREF=\"$catalog?$orderID\">main page</A>.<HR>\n";
       }
       print "<PRE>\n";
       print "Qty    Item    Description   Style      Size      Each\n";
       $subtotal = 0;
       while (($workingItemID, $rest) = each(%cart))
       { 
         ($itemID, $junk) = split('\t', $workingItemID);
         ($style, $size, $quantity, $itemDescription, $price) = split('\t', $rest);
         $lineTotal = sprintf ("\$%4.2f", $quantity * $price);
         $cquantity = &commas($quantity);
         $fprice = sprintf("\$%4.2f", $price);
         $cprice = &commas($fprice);
         $clineTotal = &commas($lineTotal);
         printf (" %-5s %-7s %-10s %-10s %5s %10s<BR>\n",
              $quantity, $itemID, $itemDescription, $style, $size,
              $cprice, $clineTotal);
         $subtotal += $price * $quantity;
       } 
        
     # put any standard text here
     # for example
       $fsubtotal = sprintf("\$%4.2f", $subtotal);
       $csubtotal = &commas($fsubtotal);
       print "Subtotal is $csubtotal<BR>\n";
       $ShippingAndHandling = &ShippingAndHandling;
       printf ("Shipping and Handling: \$%5.2f<BR>\n",$ShippingAndHandling);
       $Insurance = &Insurance;
       printf ("Insurance: \$%5.2f<BR>\n", $Insurance);
       $UPS = $FORM{ups};
       if ($UPS eq 'ups')
       {
         $Rush = &Rush;
         printf ("Rush: \$%5.2f<BR>\n", $Rush);
       }
       else
       {
         $Rush = 0;
       }
       if ($theTypeOfPaymentChosen eq 'COD')
       {
         $COD = &COD;
         printf ("COD: \$%5.2f<BR>\n", $COD);
       }
       else
       {
         $COD = 0;
       }
       $Gtotal = $subtotal + $Insurance + $ShippingAndHandling + $Rush + $COD;
       $ftotal = sprintf("\$%5.2f", $Gtotal);
       $State = $FORM{state};
       $State =~ tr/a-z/A-Z/;
       if ($State eq $TaxStateShort)
       {
         print "<B>Total before Tax is $ftotal</B>\n";
         $tax = $subtotal * $SalesTax;
         $ftax = sprintf("\$%5.2f", $tax);
         $totalWithTax = $tax + $Gtotal;
         $ftotalWithTax = sprintf("\$%5.2f", $totalWithTax);
         $fSalesTax = sprintf ("%0.2f", 100 * $SalesTax);
         print "The state of $TaxStateLong, adds $ftax
         $LocalNameOfTax.<BR>(Tax rate is $fSalesTax percent.)<BR>\n";
         print "<STRONG>Your total with tax is $ftotalWithTax</STRONG>\n";
       }
       else
       {
         print "<B>Total is $ftotal</B>\n";
       }  
      
       # modify the following as payment options change.
       if ($theTypeOfPaymentChosen eq 'Mail')
       {
         print "Please print off this order form and mail it with your
                        payment.<BR>\n";
         print "<INPUT TYPE=submit VALUE=\"Tell Us Your Order Is Coming\">\n"; 
         print "<INPUT TYPE=reset VALUE=Clear><BR>\n";
       }
       elsif ($theTypeOfPaymentChosen eq 'Phone')
       {
         print "Please print off order form and have it handy when
         you phone in your order.<BR>\n";
         print "<INPUT TYPE=submit VALUE=\"Tell Us Your Order Is Coming\">\n"; 
         print "<INPUT TYPE=reset VALUE=Clear><BR>\n";
       }
       elsif ($theTypeOfPaymentChosen eq 'COD')
       {
         print "Please print off order form and keep it for your record.<BR>\n";
         print "<INPUT TYPE=submit VALUE=\"Send Order\">\n"; 
         print "<INPUT TYPE=reset VALUE=Clear><BR>\n";
       }
       elsif ($theTypeOfPaymentChosen eq 'CreditCard')
       {
         print "<INPUT Type=Radio Name=card Value=MC CHECKED>Master Card";
         print "<INPUT Type=Radio Name=card Value=visa>Visa";
         print "<INPUT Type=Radio Name=card Value=discover>Discover<BR>\n";
         print "Card Number:<BR><input Type=Text name=number size=44><BR>\n";
         print "Expiration Date:<BR><input Type=Text name=expiration size=44><BR>\n";
         print "<INPUT TYPE=submit VALUE=\"Send Order\">\n"; 
         print "<INPUT TYPE=reset VALUE=Clear><BR>\n";
       }
       else
       {
         print "Unknown payment method.
         Please notify webmaster:<A HREF=\"mailto: $webmaster\">
           $webmaster";
         print "<INPUT TYPE=submit VALUE=\"Send Order\">\n"; 
         print "<INPUT TYPE=reset VALUE=Clear><BR>\n";
       }
    }
    else
    {
       &die("System error: cannot open cart.\n");
    }
  &html_trailer;
  }
  else
  {
    &html_header("Required data missing.");
    print "One or more of the required fields is blank.
    Please back up and complete the form.\n";
    &html_trailer;
  }
 }
} # end if 'if METHOD==POST'
else
{
  &html_header("Error");
  print "Not started via POST\n";
  &html_trailer;
}

sub commas
{
  local($_) = @_;
  1 while s/(.*\d)(\d\d\d)/$1,$2/;
  $_;
}

sub ShippingAndHandling
{
  # modify this subroutine to satisfy local S&H requirements
  if ($subtotal < 125)
  {
     $sh = 7.95;
  }
  elsif ($subtotal < 225)
  {
     $sh = 8.95;
  }
  else
  {
     $sh = 9.95;
  }
$sh;
} 

sub Insurance
{
  # modify this subroutine to satisfy local insurance requirements
  $subtotalInCents = $subtotal * 100;
  $wholeHundreds = $subtotalInCents - ($subtotalInCents % 10000);
  $fractionalHundreds = $subtotalInCents % 10000;
  if ($fractionalHundreds != 0)
  {
     $wholeHundreds += 10000;
  }
  $insurance = (.0050 * $wholeHundreds)/100;
$insurance;
}

sub Rush
{
  # modify this subroutine to satisfy local rush shipping requirements
  if ($UPS eq 'ups')
  {
    $Rush = 4.00;
  }
$Rush;
}
        
sub COD
{
  # modify this subroutine to satisfy local COD shipping requirements
  if ($theTypeOfPaymentChosen eq 'COD')
  {
    $COD = 4.75;
  }
$COD;
}

After the standard checks for illegal characters in the e-mail field and checks to make sure the required fields have been filled in, this script puts up the second part of the order form. When the payment options include mail, it is important to print out the information from the first page of the form on the second.

The reason is that when most browsers print a form, they don't print the contents of the text fields. The technique shown is the most reliable way of ensuring that the user has a printable copy of the form.

Note also that the script puts the same data up in hidden fields. This mechanism allows the follow-on script to get the information from the first form as well as the second one.

After the script opens the cart, it checks to make sure there are items in the cart. Although some programmers might consider this step unnecessary (remember that we made this same check before allowing the user to get to orderForm.html), it is important to include "belt-and-suspenders" checks for the sake of future maintenance programmers. Otherwise, a user can be charged $7.95 to ship an empty box!

Using the open cart, the order is printed a line at a time. Numeric quantities are formatted, commas are added, and the subtotal is computed. Using the subtotal, incidental charges such as shipping and handling, insurance, special rush charges, COD charges, and local taxes are added. Note that this is the first time that taxes can be computed because only now do we know where the user is located.

The incidental charges in this script give examples of four different ways of computing incidental charges. Shipping and handling is a sliding scale based on price. (A straightforward extension to this script could collect the weight of the order and base S&H on weight.)

Insurance is computed as $0.50 per hundred. The rush and COD charges are optional flat fees. Sales tax is a simple percentage, but only if the user's state matches the state in which the site owner is obligated to charge tax.

When the order is displayed on the page, the script closes by putting up a message appropriate to the type of payment method chosen and putting up the Submit and Reset buttons of the form. It is important to give the buttons meaningful names to encourage the user to actually submit the form.

Users ordering by mail or phone may neglect to submit the order, but by sending the message, they give the site owner advance notice that the order is coming. This information may be vital in maintaining adequate stock levels.

Managing the Cart

Recall that several of the routines we have seen need kart.cgi. This file provides several routines that actually implement the cart on disk, as described next.

Capturing Product Information

Remember that update.cgi, shown earlier in Listing 27.5, puts items in the cart as they are added to the order. update.cgi simply opens the cart (which makes the associative array cart valid), writes the new data to the array (with the key been assembled from the item ID, the style, and the size), and closes the cart. All the details of how the cart is managed on the disk are concealed inside kart.cgi.

Building the Cart on Disk

The key subroutines in kart.cgi are openCart and closeCart, shown in Listing 27.8.


Listing 27.8  kart.cgi-openCart and closeCart Manage the cart Implementation

#!/usr/bin/perl
#@(#)orderForm.cgi      1.3
# Copyright 1995, 1996 DSE, Inc. 
# All rights reserved
$error = -1;
$success = 0;
require "install.inc";

sub openCart
{
  local ($orderID) = @_;
  local ($filename);
  $filename = $cartPath . $orderID . ".dat";

  # does the file exist?
  if (-e $filename)
  {
    # can I access it?
    if (-r $filename && -w $filename)
    {
        # attempt to open the cart
        open (CART, $filename) || &die ("Cannot open cart file\n");
    }
    else
    {
       die ("Cannot open cart\n");
    }
  }
  else
  {
     # make one
     $actual = ">" . $filename;
     open (CART, $actual) || &die ("Cannot open actual path for write\n");

     # and reopen for read
     open (CART, $filename) || &die ("Cannot open actual path for read\n");
  }

  # one way or the other, cart is now open
  while (<CART>)
  {
    ($style, $size, $quantity, $itemNumber, $itemDescription, $price) =
      split('\t', $_);

    $workingItemNumber = $itemNumber . "\t" . $style . "\t" . $size;
    chop($price);
    $cart{$workingItemNumber} = ($style . "\t" . $size . "\t" .
    $quantity . "\t" . $itemDescription . "\t" . $price);
  }

  # and leave %cart to be passed globally
  $success;
}

#----------------

sub closeCart
{
  local ($orderID) = @_;
  local ($filename);
  $orderID = $_[0];  
  $filename = $cartPath . $orderID . ".dat";

  # does the file exist?
  if (-e $filename)
  {
    # can I access it?
    if (-r $filename && -w $filename)
    {
        # attempt to open the cart
        $actual = ">" . $filename;
        open (CART, $actual) || die ("Cannot open cart file for write\n");
    }
    else
    {
       die ("Cannot open cart\n");
    }
  }
  else
  {
       die ("Cannot find cart\n");
  }
   while (($workingItemNumber, $rest) = each(%cart))
  {
    ($style, $size, $quantity, $itemDescription, $price) =
      split('\t', $rest);
     ($itemNumber, $junk) = split ('\t', $workingItemNumber);

     print CART "$style\t$size\t$quantity\t$itemNumber\t$itemDescription\t$price\n";
  }
  close (CART);
$success;
}
1;

These subroutines reveal that the internal implementation of the cart is a flat ASCII file. When the cart is opened, it is read into an associative array. When it is closed, the associative array is unfolded back to an ASCII file. With some gain in efficiency but loss in portability, this implementation could be changed to DBM files.

Managing the Order

The order is a subtlely different concept from the cart. The cart contains items. The order is made up of a user (who has a name, address, and so forth), a cart (which has zero or more items in it), and additional information (such as whether or not the order is to be shipped rush, which incurs a separate charge. The user can watch the order as it is building by choosing any of the Review Order links.

Reviewing the Order

Choosing Review Order invokes the inkart.cgi script, shown in Listing 27.9.


Listing 27.9  inkart.cgi-Puts Up the Review Page

#!/usr/local/bin/perl
#@(#)inKart.cgi   1.3
# Copyright 1995, 1996 DSE, Inc. 
# All rights reserved

require "install.inc";

# orderForm.cgi is used for Insurance, S&H, and other special charges
require "orderForm.cgi";

# html.cgi contains routines that allow us to speak HTML
require "html.cgi";

# kart.cgi contains routines to open and close the shopping cart
require "kart.cgi";

# make sure arguments are passed using the GET method
if ($ENV{'REQUEST_METHOD'} eq 'GET' )
{
  # Get the query in phrase1+phrase2 format from the QUERY_STRING
  # environment variable.

  $query = $ENV{'QUERY_STRING'};

  #if the query string is null, then there is no search phrase
  # appended to the URL.

  if ($query !~ /\w/)
  {
    # No argument
    &html_header('Empty OrderID string.');
    &html_trailer;
   }
   else
   {
     $query =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
     ($orderID)  = split(/\+/, $query);

     # here is where we do the work
     if ($success == &openCart ($orderID))
     {
        $count = @cart;
        &html_header('Your order');

        print "Now processing order number $orderID\n";
        print ".<BR>\n";

       # find the length of the cart array
       $numberOfItems = %cart;

       if ($numberOfItems == 0)
       {
         print "There are no items in your shopping cart.\n";
       }
       else
       {
          print "<PRE>\n";
                     print "Qty Item       Description
                     Style      Size           Each      Total\n";
         print "<FORM ACTION=\"multiupdate.cgi?$orderID\" METHOD=\"POST\"><BR>\n";
         $subtotal = 0;
         while (($workingItemID, $rest) = each(%cart))
         {
           ($itemID, $junk) = split('\t', $workingItemID);
           ($style, $size, $quantity, $itemDescription, $price) = split('\t', $rest);
           $lineTotal = sprintf ("\$%4.2f", $quantity * $price);
           $cquantity = &commas($quantity);
           $fprice = sprintf("\$%4.2f", $price);
           $cprice = &commas($fprice);
           $clineTotal = &commas($lineTotal);
        print "<INPUT TYPE=text NAME=\"$workingItemID\" VALUE=$cquantity SIZE=2>";      
        printf (" %-10s %-30s %-10s %-10s %10s %10s<BR>\n",
           $itemID, $itemDescription, $style, $size, $cprice, $clineTotal);
           $subtotal += $price * $quantity;
         }


          # put any standard text here
          # for example
          $fsubtotal = sprintf("\$%4.2f", $subtotal);
          $csubtotal = &commas($fsubtotal);
          
          print "Subtotal is $csubtotal<BR>\n";
          $ShippingAndHandling = &ShippingAndHandling;
          printf ("Shipping and Handling: \$%5.2f<BR>\n",$ShippingAndHandling);
          $Insurance = &Insurance;
          printf ("Insurance: \$%5.2f<BR>\n", $Insurance);
          $Gtotal = $subtotal + $Insurance + $ShippingAndHandling;
          $ftotal = sprintf("\$%5.2f", $Gtotal);
          print "<B>Total without Tax is $ftotal</B>\n";
          print "<P>\n";
          $tax = $subtotal * $SalesTax;
          $ftax = sprintf("\$%5.2f", $tax);
          $fSalesTax = sprintf ("%0.2f", 100 * $SalesTax);
          print "If you live in the state of $TaxStateLong, we will add
          $ftax  $LocalNameOfTax.<BR>(Tax rate is $fSalesTax
          percent.)<BR>\n";
          print "</P>\n";
          print "</PRE>\n";
          print "<H3>Please Note</H3>\n";
          print "<P>If payment is by C.O.D there will be a charge of
          \$4.75 added to your bill.</P>\n";
          print "<P>Two day UPS Rush delivery is available at a cost of 
          \$4.00.</P>\n";
          print "</P>\n";

          # put up the buttons
          print "<INPUT TYPE=\"submit\" VALUE=\"Update\">\n";
          print "<INPUT TYPE=\"reset\" VALUE=\"Clear Form\">\n";
          print "</FORM>\n";
          print "<A HREF=$catalog?$orderID>Return to Catalog</A> |\n";
          print "<A HREF=$outputPage?$orderID
          +$pageDirectory/orderform.html>Check out</A><BR>\n";
       print "Use your browser\'s BACK button to return to the previous page.<BR>\n";
        }

        print "<HR>\n";
        &html_trailer;
      }
   }
}
else
{
  &html_header('Error');
  print "Not started via GET\n";
  &html_trailer;
}

inKart.cgi is special because it's a script that puts up a form which, in turn, calls a script. The user can use the Review page to change the quantity on any item. If the quantity is set to zero, the item disappears from the cart. The script that does this work is multiupdate.cgi, shown in Listing 27.10.


Listing 27.10   multiupdate.cgi-This Script Allows the User to Change the Quantity of Any Item on the Order

#!/usr/local/bin/perl
#@(#)multiupdate.cgi    1.3
# Copyright 1995, 1996 DSE, Inc. 
# All rights reserved

require "install.inc";

# html.cgi contains routines that allow us to speak HTML
require "html.cgi";

# kart.cgi contains routines to open and close the shopping cart
require "kart.cgi";

$orderID = $ARGV[0];

# make sure arguments are passed using the POST method
if ($ENV{'REQUEST_METHOD'} eq 'POST' )
{
  # using POST; look to STDIN for fields
  read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'});

  # split the name/value pairs on '&'
  @pairs = split(/&/, $buffer);
  if ($success == &openCart ($orderID))
  {
     # go through the pairs and determine the name and 
     # value of each variable
     foreach $pair (@pairs)
     {
       ($fullname, $value) = split(/=/, $pair);
     $fullname =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
       ($name, $junk) = split(/\t/, $fullname);
     $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;

     # here is the item-quantity pair
     if ($name !~ /_!Rest!/)
     {
        $value =~ s/,//;
        $NewQuantity = $value;

        # fetch the line from the cart
        $data = $cart{$fullname};
        ($style, $size, $quantity, $itemDescription, $price) = split(/\t/, $data);

        # and update the quantity
        if ($NewQuantity != 0)
        {
              $cart{$fullname} = 
           ($style . "\t" . $size . "\t" . $NewQuantity . "\t" .
           $itemDescription . "\t" . $price);
        }
        else
        {
          delete $cart{$fullname};
        }
      }
      else
      {
             # is there really a need to update from a hidden field?
             # can the values be different from what we have?
         }
      }

     # and write it out
       $result = &closeCart($orderID);
     if ($success == $result)
       {
        print "Location: $mucgipath?$orderID\n\n";
     }
     else
     {
       &html_header('Error');
          print "Could not close cart file.\n";
       &html_trailer;
        }
      }
      else
      {
          &html_header('Error');
          print "Could not access cart file.\n";
          &html_trailer;
      }
}
else
{
  &html_header('Error');
  print "Not started via POST\n";
  &html_trailer;
}

When users have the Review page open, they can change the quantities associated with each item and then submit the form. multiupdate.cgi reads in the quantity fields (each of which has the full working name, including style and size). It splits off the quantity, removes any commas the user may have entered, and splices the new quantity into the corresponding line of the cart. If the new quantity is zero, the item is deleted from the cart.

Finally, the cart is closed and the items are all written back to the disk.

Checking Out

Earlier listings show how the common information is collected and processed, and how a printable copy of the form is put up. When the visitor submits the final page of the order form, four things happen:

Optionally, unused carts can also be purged from the hard disk.

Sending the Order

The final page of the order form invokes checkout.cgi, shown in Listing 27.11.


Listing 27.11  checkout.cgi-The User Checks Out and the Site Owner Is Notified of the Order

#!/usr/local/bin/perl
#@(#)checkout.cgi 1.4
# Copyright 1995, 1996 DSE, Inc. 
# All rights reserved

require "install.inc";

# html.cgi contains routines that allow us to speak HTML
require "html.cgi";

# kart.cgi contains routines to open and close the shopping cart
require "kart.cgi";

# purge.cgi contains the routine to delete files older than a day
require "purge.cgi";

# ctime.pl contains the routine to convert time to human-readable form
# note that it doesn't come from us; it is part of Perl installation
require "ctime.pl";

$orderID = $ARGV[0];

# make sure arguments are passed using the POST method
if ($ENV{'REQUEST_METHOD'} eq 'POST' )
{
  # using POST; look to STDIN for fields
  read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'});

  # split the name/value pairs on '&'
  @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 ($FORM{email} !~ /^[a-zA-Z0-9_\-+ \t\/@%.]+$/ && $FORM{email} !~/^$/)
  {
    &html_header("Illegal Email Address");
    print "<HR><P>\n";
    print "The Email address you entered ($FORM{email}) contains illegal ";
    print "characters. Please back up, correct, then resubmit.\n";
    &html_trailer;
    exit;
  }
# make sure required data is entered (belt and suspenders)
  
  if (($FORM{name} !~ /^$/) && 
     (($FORM{address1} !~ /^$/) ||($FORM{address2} !~ /^$/)) &&
     ($FORM{city} !~ /^$/) &&
     ($FORM{zip} !~ /^$/) && ($FORM{country} !~ /^$/) &&
     ($FORM{state} !~ /^$/) && 
     (($FORM{workphone} !~ /^$/) ||($FORM{homephone} !~ /^$/)) && 
     ($FORM{payment} !~ /^$/) )
     
  {
    if ($success == &openCart ($orderID))
    { 
      $cartCount = %cart;
      if ($cartCount != 0)
      {
        # fetch the line from the cart
        # and write it all out
        open (MESSAGE, "| sendmail -t");
        print MESSAGE "To: $recipient\n";
        if ($FORM{email} ne "")
        {
       print MESSAGE "From: \"$FORM{name}\" <$FORM{email}>\n";
          print MESSAGE "Reply-to: $FORM{email}\n";
        }
        # write the actual message
        print MESSAGE "Subject: Order Number $orderID from $ENV{'REMOTE_HOST'}\n\n";
        print MESSAGE "Name: $FORM{name}\n";
        print MESSAGE "Address1: $FORM{address1}\n";
        print MESSAGE "Address2: $FORM{address2}\n";
        print MESSAGE "City: $FORM{city}\n";
        print MESSAGE "State: $FORM{state}\n";
        print MESSAGE "Zip: $FORM{zip}\n";
        print MESSAGE "Country: $FORM{country}\n";
        print MESSAGE "Work Phone: $FORM{workphone}\n";
        print MESSAGE "Home Phone: $FORM{homephone}\n";
        print MESSAGE "Payment Method: $FORM{payment}\n";
        print MESSAGE "Item      Qty Description
                    Style      Size       Each\n";
        while (($itemID, $data) = each (%cart))
        {
#print "ID:$itemID\n";
        ($itemID, $junk) = split(/\t/, $itemID);
#print "ID:$itemID\n";
           ($style, $size, $quantity, $itemDescription, $price) = split(/\t/, $data);
#print "ST:$style, Sz:$size, Q:$quantity, D:$itemDescription, P:$price)\n";
           printf MESSAGE ("%-10s %2d %-30s %-10s %-10s \$%10.2f\n",
                $itemID, $quantity, $itemDescription, $style, $size, $price);
         }

#----------------
# Now do it all over again, to the file
      $backupFile = ">" . $backupPath ."/order." . $orderID . "_" . &ctime($^T);
         open (MESSAGE, $backupFile) ||
            &html_header('Error') && print "Unable to write backup file\n" &&
            &html_trailer && die;
         print MESSAGE "To: $recipient\n";
         if ($FORM{email} ne "")
         {
           print MESSAGE "From: \"$FORM{name}\" <$FORM{email}>\n";
           print MESSAGE "Reply-to: $FORM{email}\n";
         }

         # write the actual message
         print MESSAGE "Subject: Order Number $orderID from $ENV{'REMOTE_HOST'}\n\n";
         print MESSAGE "Name: $FORM{name}\n\n";
         print MESSAGE "Address1: $FORM{address1}\n";
         print MESSAGE "Address2: $FORM{address2}\n";
         print MESSAGE "City: $FORM{city}\n";
         print MESSAGE "State: $FORM{state}\n";
         print MESSAGE "Zip: $FORM{zip}\n";
         print MESSAGE "Country: $FORM{country}\n";
         print MESSAGE "Work Phone: $FORM{workphone}\n";
         print MESSAGE "Home Phone: $FORM{homephone}\n";
         print MESSAGE "Payment Method: $FORM{payment}\n";
         print MESSAGE "Item      Qty Description
                    Style      Size       Each\n";
         while (($itemID, $data) = each (%cart))
         {  
            ($itemID, $junk) = split (/\t/, $itemID);
            ($style, $size, $quantity, $itemDescription, $price) = split(/\t/, $data);
                printf MESSAGE ("%-10s %2d %-30s %-10s %-10s \$%10.2f\n", 
                   $itemID, $quantity, $itemDescription, $style, $size, $price);
         }

#----------------

         $filename = $cartPath . $orderID . ".dat";
         if (unlink ($filename))
         {
           # if we cannot get to cron, purge here
        &purge;
        print "Location: $Thanks\n\n";
         }  
         else
         {
            &html_header('Error');
            print "Could not remove cart file.\n";
            &html_trailer;
         }
       }
       else
       {
         &html_header("Empty Cart!");
         print "<HR><P>\n";
         print "You are trying to check out with an empty cart.\n";
         print "Return to the <A HREF=$catalog?$orderID>main page.</A>";
         &html_trailer;
         exit;
       }
     } 
     else
     {
       &html_header('No cart!');
       print "<HR><P>\n";
       print "You are trying to check out but you have no cart.\n";
       print "Return to the <A HREF=$catalog?$orderID>main page.</A>";
       &html_trailer;
       exit;
     }
   } 
   else
   {
     &html_header("Required Data Missing");
     print "<HR><P>\n";
     print "You left one of the required fields blank. \n";
     print "Please back up, correct, then resubmit.\n";
     &html_trailer;
     exit;
   }
}
else
{
  &html_header('Error');
  print "Not started via POST\n";
  &html_trailer;
}  

Note that the e-mail is written using the -t option of sendmail. This option tells sendmail to take its parameters from the datastream, so the To:, From:, and other addressees are set up in the subsequent print statements.

Once this program has run, the cart is gone. It's important to get users moved forward to the Thank You page and discourage them from using their browser button to go back to the order form. If they do attempt to resubmit the order, they will be told that their cart is empty, but the experience may confuse some users and is not recommended.

Cancelling the Order

Ideally, the site owner has access to the UNIX crontab and can schedule a purge to run every day, taking with it all carts that have not been touched recently. On many sites, the site owner does not have system administration privileges, so a purge can be run from inside checkout. Each time a user makes a purchase, all old unused carts are deleted. purge is shown in Listing 27.12.


Listing 27.12-purge.cgi-Removes All carts Older Than 24 Hours

#!/usr/local/bin/perl
#@(#)purge.cgi    1.1
# Copyright 1995, 1996 DSE, Inc. 
# All rights reserved

require "install.inc";

sub purge
{
  # set up the target directory

  # set the purge limit (in seconds)
  # for reference,
  # One day = 86400
  # Two days = 172800
  # One week = 604800

  $purgeLimit = 86400;

  # capture the time the script started
  $now = $^T;

  # run through the directory
  while ($nextFile = <$cartPath/*>)
  {
    # purge appropriate files
    ($mtime) = (stat($nextFile))[9];

    if ($now - $mtime > $purgeLimit)
    {

       # since all of our files are textfiles, only add textfiles
       # to the 'goners' list
      push (@goners, $nextFile) if -T $nextFile;
    }
  }
    @cannot = grep(!unlink($_), @goners);

    die "$0: could not unlink @cannot\n" if @cannot;
}
1;

This script is simple but potentially dangerous. Make sure it's pointed to the right directory before running it. It sweeps through the directory looking at the time of last modification (mtime) of each file. If the mtime shows that the file hasn't been modified in the last purgeLimit seconds (set to 24 hours by default) and the file is a text file, the file is added to the goners list.

Finally, all files on the goners list are deleted using the unlink command; the script
complains about any files that could not be deleted. Nontext files appearing in the cart directory or files appearing in that directory that the HTTP user can't delete are both signs of a malfunction. These errors are written to the error log on most Web servers.

The oft-mentioned install.inc (see Listing 27.13) contains most of the install-time
options in one convenient file.


Listing 27.13  install.inc-Runtime Options Appear in install.inc

#@(#)install.inc  1.1
# Copyright 1995, 1996 DSE, Inc. 
# All rights reserved

# used generally
$webmaster = "morganm\@dse.com";

# from catalog.cgi
$pageDirectory = "/users/dse/pages/BobsCycle"; # also used by inkart.cgi
$outputPage="/cgi-bin/dse/BobsCycle/page.cgi"; #also used by inkart.cgi

# from checkout.cgi
$Thanks = "http://www.dse.com/BobsCycle/Thanks.html";

# To whom should the e-mail be sent?
$recipient = "kepilino@dse.com";
$cartPath = "../tmp/";  #also used in kart.cgi and purge.cgi
$backupPath = "../backup";
$catalog = "http://www.dse.com/cgi-bin/dse/BobsCycle/catalog.cgi"; #also used by inkart.cgi

# from inkart.cgi, orderform.cgi, and checkout.cgi

# What is the tax rate in your state?
$SalesTax = 0.045;

# What is the tax called in your state?
$LocalNameOfTax = "sales tax";

# What are the names (short and long) of your state?
$TaxStateShort = "VA";
$TaxStateLong = "Virginia";

# from multiupdate.cgi
$mucgipath = "/cgi-bin/dse/BobsCycle/inkart.cgi"; 

# from update.cgi
$upcgipath = "http://www.dse.com/cgi-bin/dse/BobsCycle/page.cgi";