cgi banner gif

home ch01 ch02 ch03 ch04 ch05 ch06 ch07 ch08 ch09 quick reference


Chapter 5: Other Form Examples

A guest book

A shopping cart


In Chapter 4, you learned the basics about handling form input and created a simple CGI script for handling a feedback form. This chapter continues the discussion of forms by presenting two more examples, the guest book and the shopping cart (both of which are defined in their respective sections). Along with feedback forms, guest books and shopping carts are among the most popular uses of forms and form handlers.


A guest book

A Web site guest book is the online equivalent of guest books you may have seen at art galleries, museums, bed and breakfasts, or retail stores. In these establishments, a guest book consists of a blank book in which guests may enter their names and addresses. If you flipped back through the book, you would see information about many people who had visited that location.

For your online guest book, you want the user to be able to view previous entries. You also want that user to be able to enter his or her own information to the top of the list. Why the top of the list? Well, with paper guest books, the page is usually turned to the most recent entry, so that guests can easily add their names to the end of the list. With online guest books, if you added the newest guest to the end of the list, this listing would be the last thing a user would see. Users signing in would have to scroll though the entire list just to see their entries in the book.

You can accomplish all of these requirements with a single CGI script. Your guest book script will display the information of previous visitors and process the form submission of current visitors. When your script receives a new entry to the guest book, it will add the entry to the top of your guest book list and display the current list to the user.

Setting Up the Sign-In Form

As with the feedback form example in Chapter 4, you first need to set up the sign-in form that the user will fill out to add his or her entry to your guest book. To do this, you must decide what information to request from the user. For this guest book example, you will ask for the user's name, e-mail address, home page URL, city, state, and country. You can capture all of these items by using the single-line text input element. You will also include a text area in which the user can add any comments. The HTML for this guestbook.html (guestbk.htm if your system limits you to an eight-character file name and a three-character file name extension) file would be the following:

<HTML>
<HEAD>
<TITLE>Guest book Sign In</TITLE>
</HEAD>
<BODY>
<H1>Guest book Sign In</H1>
To sign our guest book, please fill out the fields below. If you do not want to provide 
some of the information, just leave those fields blank, but keep in mind 
that you must include your name to be added to the guest book.
<FORM METHOD=POST ACTION="/cgi-bin/guestbook.pl">
<P><B>Name</B><BR><INPUT NAME="name" SIZE=42>
<P><B>E-mail Address</B><BR><INPUT NAME="email" SIZE=42>
<P><B>Home Page URL</B><BR><INPUT NAME="url" SIZE=42>
<P><B>City</B><BR><INPUT NAME="city" SIZE=20>
<P><B>State</B><BR><INPUT NAME="state" SIZE=2>
<P><B>Country</B><BR><INPUT NAME="country" SIZE=20>
<P><B>Comments</B><BR><TEXTAREA NAME="comments" ROWS=10 COLS=38></TEXTAREA>
<P><INPUT TYPE="submit" VALUE="Sign In"> <INPUT TYPE="reset" VALUE="Reset">
</FORM>
</BODY>
</HTML>

Figure 5.1 shows how the sign-in form will appear in Netscape.



Note: This HTML code contains all of the necessary elements for gathering the information you need. However, for the sake of simplicity, and because this is a book on CGI scripting and not Web page design, this HTML code does not generate a great looking Web page. For your actual Web page, I would recommend using some other elements (graphics and HTML tags like the <TABLE> tag) to improve the design.


Figure 5.1: The guest book sign-in form

Handling the Input from the Sign-In Form

With the sign-in form complete, you can start working on the CGI script for handling the guest book. The first thing to do is to receive the input from the sign-in form, process it, and then write it to your guest book file. The guest book file is a text file that holds all of the user sign ins. When you receive a sign in, you place the new information at the top of the guest book file.

As in the form example from Chapter 4, you first need to receive the data from the user's browser and decode it. To do so, you can use the same User_Data subroutine that you used in the preceding chapter.


# Decode the user data an place it in the
# data_received associative array.
%data_received = &User_Data();

In this example, unlike the feedback form in Chapter 4, you write this information to a file, later displaying the contents of this file as part of a HTML page. Remember from Chapter 3 that if Server Side Includes are enabled for your Web server, someone could enter a Server Side Include as one of the entries of your guest book and have the results of that directive displayed in his or her Web browser. You can prevent this possibility by parsing the user's input for Server Side Include directives and removing any that you find. You will do this by creating a new subroutine called No_SSI.

sub No_SSI {
  local (*data) = @_;

  foreach $key (sort keys(%data)) {
    $data{$key} =~ s/<!--(.|\n)*-->//g;
  }
}

This subroutine receives one parameter, the associative array that contains the user's input data. It then loops over each element in the array checking for Server Side Include directives. The line

$data{$key} =~ s/<!--(.|\n)*-->//g;

is what actually does the work. It is a Perl regular expression that performs the search and substitution. The leading "s" tells the Perl interpreter to replace everything between the first and second slashes with the material between the second and third slashes. In this case, the pattern <!--(.|\n)*--> will match any properly formatted Server Side Include, which will be replaced with nothing (in other words, will be deleted) because there is nothing between the second and third slashes. The "g" at the end of the line tells the Perl interpreter to change all occurrences instead of only the first one it finds.

You now have the user's data properly decoded and any Server Side Includes removed. The next thing to do is to enter the information into your guest book file. You do this by placing all of the elements of the user's information into a single string, with HTML tags. This string is then added to the first line of the guest book file.

When you set up the guest book form, you told users they had to enter their names. That was the only mandatory field. To make sure this field has a value, you will place an if statement around the code to enter the entry into the guest book file. This if statement checks whether the user's name has been entered. (Actually, it only checks whether the string is not blank. The user could enter any valid string.) If there is a value, the user's information is placed in the $new_guest string and that string is added to the beginning of the guest book file. If the user did not enter a valid string for the name field, he or she is prompted to do so.


if ($data_received{"name"} ne "") {
  $new_guest = "<B>Name:</B> $data_received{\"name\"}<BR>\n";
  $new_guest .= "<B>Data:</B> $date<BR>\n";

  $new_guest .= "<B>E-Mail:</B> <A HREF=\"mailto:$data_received{\"email\"}\">$data_received{\"email\"}</A><BR>\n" if 
$data_received{"email"} ne "";
  $new_guest .= "<B>Home Page URL:</B> <A 
HREF=\"$data_received{\"url\"}\">$data_received{\"url\"}</A><BR>\n" if 
$data_received{"url"} ne "";
  $new_guest .= "$data_received{\"city\"}, " if $data_received{"city"} ne "";
  $new_guest .= "$data_received{\"state\"} " if $data_received{"state"} ne "";
  $new_guest .= "$data_received{\"country\"}<BR>\n" if $data_received{"country"} ne "";
  $new_guest .= "<B>Comments:</B> $data_received{\"comments\"}\n" if $data_received{"comments"} ne "";

  $new_guest .= "<P><HR><P>\n";

  open(GUESTBOOK,"$guestbookfile") || die "Content-type: text/text\n\nCannot open $guestbookfile";
  @guestbook = <GUESTBOOK>;
  close(GUESTBOOK);

  unshift(@guestbook, $new_guest);
  open(GUESTBOOK,">$guestbookfile") || die "Content-type: text/text\n\nCannot open $guestbookfile";
  print GUESTBOOK @guestbook;
  close(GUESTBOOK);
} else {
  print "Content-type: text/html\n\n";
  print "<H1>Sign-In Unsuccessful</H1>\n";
  print "You must enter your name to be added to the guest book.";
}

Notice how each of the first lines within the if statement are in the form

$new_guest .= "some string" if $data_received{"some element"} ne "";

By adding the fields in this manner, you only add the specified element if the user entered a value for it. For example, the line that adds the user's city is

$new_guest .= "$data_received{\"city\"}, " if $data_received{"city"} ne "";

This line appends the name of the city, $data_received{"city"}, to the string $new_guest if the user entered a string in the city field of the guest book form.

After all of the user's information and HTML tags have been appended to the variable $new_guest, the guest book file is opened, the contents of the file are placed within the array @guestbook, and the file is closed. The name of the guest book file is stored in the $guestbookfile string. The code for placing the path and file name of the guest book file within the $guestbookfile string is shown in Listing 5.1 at the end of this section. The line


unshift(@guestbook, $new_guest);

makes the string $new_guest the first element of the @guestbook array, moving all other contents over one index in the array. The guest book file is then opened again and the contents of the @guestbook array are printed to the file, overwriting any previous contents.

Notice the difference between the first and second open statements. The file name in the second open statement is preceded by the greater than (>) character. To Perl this means that the file is being opened for output. When a file is opened with this status, all of the previous contents of the file are overwritten. The less than (<) character means to open the file for input, and >> means to append to the file. If none of these special characters are specified, the file is opened for input, as is the case with the first open statement in the example.

If the user did not enter a valid string for the name field of the guest book form, the else portion of the if...else statement is executed. The code in this section prints a response to the user's Web browser stating that he or she needs to enter a value for the name field.

When the user does enter a correct value for the name field and all of the code under the if block is executed, the user's data is added to the guest book file. However, in the preceding example no information is sent back to the user's Web browser. Because the user just entered information for your guest book, it would be appropriate to now display the contents of your guest book with the new entry at the top. Because you have to have the code for displaying the guest book in another part of your guest book script (for when the user just wants to display the guest book without adding an entry first), the best way to do this is to call a subroutine that displays the guest book. (If you didn't call a subroutine, you would have to place the code for displaying the guest book in multiple places in your script file.) All you need to add to the previous code is the line


&Display_Book($guestbookfile);

immediately after the second close(GUESTBOOK); statement.

Displaying the Contents of the Guest Book

As mentioned, you still need a subroutine that displays the contents of the guest book file. Because you already placed all relevant HTML tags along with the guest book entries, you just need to print the contents of the file preceded with and followed by the appropriate HTML header and footer. You do this with the following code:

sub Display_Book {
  local ($guestbookfile) = @_;
  local (@guestbook);
 
  open(GUESTBOOK,"$guestbookfile") || die "Content-type: text/text\n\nCannot open
$guestbookfile";
  @guestbook = <GUESTBOOK>;
  close(GUESTBOOK);
 
  print "Content-type: text/html\n\n";
  print "<HTML><HEAD><TITLE>My Guest Book</TITLE></HEAD><BODY>";
  print "<H1>My Guest Book</H1>";
  print @guestbook;
  print "</BODY></HTML>";
}

This subroutine opens the guest book file, places all of the contents in the array @guestbook, prints the parsed header and preceding HTML tags, prints the contents of the @guestbook array (which is the contents of the guest book file), and prints the ending HTML tags.

Putting It All Together: The guestbook.pl Script

Now you have all the pieces of the guest book script. All that you need to do is to put them together. At the beginning of this section you learned that you can do the entire guest book with one script. You may be thinking, "Don't we need two scripts, one to display the guest book and one to add entries to the guest book?" Well, even if you aren't thinking that, the answer is no. When a CGI script is called, it is called with a certain method, such as GET or POST. If no method is specified, as when the script is called from an <A> tag, the method defaults to GET. You can use this fact to your advantage. Remember that when the user signs in with the guest book form, the method is POST. So, all you have to do is check which method is used. If it is POST, the user is trying to sign the guest book. If it is GET, the user is trying to view the guest book. To make your guest book script more readable, you can place the code for adding the user's data to the guest book in a subroutine called Add_Guest. Then check the method that was used and call the appropriate subroutine. If the method is POST, you call the Add_Guest subroutine; otherwise call the Display_Book subroutine to just display the guest book. To accomplish this, use the Perl statement



$ENV{"REQUEST_METHOD"} eq "POST" ?  &Add_Guest($file) : &Display_Book($file);

This statement checks the conditional--everything before the question mark (?). If the conditional is true, it executes the expression between the question mark and the colon (:). If the conditional is false, the expression after the colon is executed.

Listing 5.1 contains the Perl code for the completed guest book script, and Figure 5.2 shows how Netscape would display the contents of the guest book. If you are using guestbook.pl on a Windows machine, remember to remove the #!/usr/local/bin/perl line. Also, for both Windows and UNIX machines, you need to change the path to the guestbook.dat file to the correct path for your machine. Windows users need the path to look something like


$file = "c:\\robertm\\guestbook.dat";


A Shopping Cart

A shopping cart is a CGI script that enables your users to create a list of items they want to purchase from inventory displayed on your Web pages. The shopping cart CGI script is analogous to a store's shopping cart. With a real shopping cart, you walk through the store, placing items in the cart. When you are finished shopping, you pay for everything at once at the check stand. Likewise, with the shopping cart script you can browse through Web pages, placing items in your virtual shopping cart. When you're done shopping, you can purchase all the items in your cart at the virtual checkout counter. If you did not have a shopping cart CGI script, you would have to buy the items one at a time.

Listing 5.1: The guestbook.pl File
#!/usr/local/bin/perl

$file = "/users/robertm/guestbook.dat";
$date = localtime(time);

$ENV{"REQUEST_METHOD"} eq "POST" ?  &Add_Guest($file) :
&Display_Book($file);

sub Add_Guest {
  local ($guestbookfile) = @_;
  local (%data_received, $new_guest, @guestbook);

  # Decode the user data and place it in the
  # data_received associative array.
  %data_received = &User_Data();

  &No_SSI(*data_received);

  if ($data_received{"name"} ne "") {
    $new_guest = "<B>Name:</B> $data_received{\"name\"}<BR>\n";
    $new_guest .= "<B>Data:</B> $date<BR>\n";

    $new_guest .= "<B>E-Mail:</B> <A 
HREF=\"mailto:$data_received{\"email\"}\">$data_received{\"email\"}</A>
<BR>\n" if $data_received{"email"} ne "";
    $new_guest .= "<B>Home Page URL:</B> <A 
HREF=\"$data_received{\"url\"}\">$data_received{\"url\"}</A><BR>\n" if 
$data_received{"url"} ne "";
    $new_guest .= "$data_received{\"city\"}, " if 
$data_received{"city"} ne "";
    $new_guest .= "$data_received{\"state\"} " if 
$data_received{"state"} ne "";
    $new_guest .= "$data_received{\"country\"}<BR>\n" if 
$data_received{"country"} ne "";
    $new_guest .= "<B>Comments:</B> $data_received{\"comments\"}\n" if 
$data_received{"comments"} ne "";

    $new_guest .= "<P><HR><P>\n";

    open(GUESTBOOK,"$guestbookfile") || die "Content-type: 
text/text\n\nCannot open $guestbookfile";
    @guestbook = <GUESTBOOK>;
    close(GUESTBOOK);

    unshift(@guestbook, $new_guest);
    open(GUESTBOOK,">$guestbookfile") || die "Content-type: 
text/text\n\nCannot open $guestbookfile";
    print GUESTBOOK @guestbook;
    close(GUESTBOOK);

    &Display_Book($guestbookfile);

  } else {
    print "Content-type: text/html\n\n";
    print "<H1>Sign-In Unsuccessful</H1>\n";
    print "You must enter your name to be added to the guest book.";
  }
}

sub Display_Book {
  local ($guestbookfile) = @_;
  local (@guestbook);

  open(GUESTBOOK,"$guestbookfile") || die "Content-type: 
text/text\n\nCannot open $guestbookfile";
  @guestbook = <GUESTBOOK>;
  close(GUESTBOOK);

  print "Content-type: text/html\n\n";
  print "<HTML><HEAD><TITLE>My Guest Book</TITLE></HEAD><BODY>";
  print "<H1>My Guest Book</H1>";
  print @guestbook;
  print "</BODY></HTML>";
}

sub No_SSI {
  local (*data) = @_;

  foreach $key (sort keys(%data)) {
    $data{$key} =~ s/<!--(.|\n)*-->//g;
  }

}

sub User_Data {
  local (%user_data, $user_string, $name_value_pair,
         @name_value_pairs, $name, $value);

  # If the data was sent via POST, then it is available
  # from standard input. Otherwise, the data is in the
  # QUERY_STRING environment variable.
  if ($ENV{"REQUEST_METHOD"} eq "POST") {
    read(STDIN,$user_string,$ENV{"CONTENT_LENGTH"});
  } else {
    $user_string = $ENV{"QUERY_STRING"};
  }

  # This line changes the + signs to spaces.
  $user_string =~ s/\+/ /g;

  # This line places each name/value pair as a separate
  # element in the name_value_pairs array.
  @name_value_pairs = split(/&/, $user_string);

  # This code loops over each element in the name_value_pairs
  # array, splits it on the = sign, and places the value
  # into the user_data associative array with the name as the
  # key.
  foreach $name_value_pair (@name_value_pairs) {
    ($name, $value) = split(/=/, $name_value_pair);

    # These two lines decode the values from any URL
    # hexadecimal encoding. The first section searches for a
    # hexadecimal number and the second part converts the
    # hex number to decimal and returns the character
    # equivalent.
    $name =~
      s/%([a-fA-FØ-9][a-fA-FØ-9])/pack("C",hex($1))/ge;
    $value =~
       s/%([a-fA-FØ-9][a-fA-FØ-9])/pack("C",hex($1))/ge;

    # If the name/value pair has already been given a value,
    # as in the case of multiple items being selected, then
    # separate the items with a " : ".
    if (defined($user_data{$name})) {
      $user_data{$name} .= " : " . $value;
    } else {
      $user_data{$name} = $value;
    }
  }
  return %user_data;
}



Figure 5.2: Example contents of the guest book

When maintaining a shopping cart, the main difficulty is identifying which shopping cart belongs to which user. If your shopping cart script cannot keep track of each user's shopping cart, items will be put in the wrong carts and users will end up buying incorrect items. There are two basic ways you can keep track of shopping carts--with temporary files and a cart number or with the Netscape cookie (which is described in a moment). This section first develops a shopping cart script that uses temporary files as the shopping carts. After that you learn about the Netscape cookie and use a revised shopping cart script that makes use of the cookie. In both of these examples, you develop a shopping cart for the store Virtual Stationers, which has three categories of products: paper, envelopes, and writing instruments.

The File-Based Shopping Cart

In the file-based shopping cart, you construct a CGI script that creates a unique file for every user. Within this file, you store the names and quantities of the items the user selects from your Web pages. The user must be able to add items, view the items, and modify or delete items within the file. The shopping cart CGI script must generate the HTML pages for each user.

You might wonder why the HTML pages must be generated from your CGI script. These HTML pages contain forms that have an input field that allows the users to indicate how many items to add to their shopping carts. When a user enters a number and presses the Add Items to Cart push button, these items and quantities are sent to your CGI script for addition to the user's shopping cart. But how do you identify which shopping cart belongs to that user? Normally, the user's Web browser does not send any unique information as part of the HTTP header that accompanies the POST request of the form. So, there is no information to use as a unique identifier. This means you have to create a unique identifier and assign it to each user who has a shopping cart. This unique identifier can be any name, number, or combination of characters, as long as you can guarantee that only one user at a time will receive that identifier.

In addition, you still have to get the user's Web browser to send this unique identifier every time the user presses one of the push buttons on your forms. To have this identifier included with the information sent from the user's Web browser, you must include it as a hidden field of the form. That way, when the form is submitted, the unique identifier is sent. However, this presents another problem. How do you get a user's unique identifier in a hidden field of the HTML page sent to that user's Web browser? If you included the line


<INPUT TYPE=hidden NAME="cart_no" VALUE="32719">

in your HTML page (where cart_no is the unique identifier), every user requesting your HTML page would have the cart_no 32719. Remember, HTML pages are static. Every user will see the same page. The only way around this problem is to have the HTML pages created by your CGI script.

When a user first visits your Web site, you assign a unique identifier for a cart_no. Then, every other page that that user requests is generated from your shopping cart script. This way, when the user requests another page, you can have the link send the cart_no to your CGI script, which will then place that cart_no in the HTML page it sends back to the user's Web browser. For example, when the user wants to go to the Envelopes section of the Virtual Stationers Web site, the HTML code would look like this:


<A HREF="/cgi-
bin/shcart.pl?cart_no=82894734.345&page=envelope">Envelopes</A>

This URL calls the shopping cart script and passes it the cart_no identifier, which is the unique identifier you are using, and the name of the page that the user is requesting. Within the shopping cart script, the HTML code necessary for the Envelopes Web page is generated and sent back to the user's Web browser.

The HTML Templates

When developing the shopping cart, you first need to decide how the HTML pages will look. These pages--which display the inventory the user can select from--could be generated entirely from your CGI script. However, to minimize the HTML tags in your CGI script, you will keep most of the HTML tags in separate files with .tmpl (for template) extensions (use .tml if your system restricts you to a three-character file name extension). When the user requests one of these Web pages, your CGI script will read in the template file, change the cart_no value to the unique number for that user, and send the HTML code to the user's Web browser. So, when you create these template files, you need to leave a placeholder for the cart_no value. If you use a placeholder, the value can easily be found and replaced in your CGI script. For example, Listing 5.2 shows the template file for the Virtual Stationers home page and Figure 5.3 displays the vshome.tmpl file as it would appear when your shopping cart script sends it to a user's Web browser for display.

Listing 5.2: The vshome.tmpl File
<HTML>
<HEAD>
<TITLE>Virtual Stationers</TITLE>
</HEAD>
<BODY>
<H1>Virtual Stationers</H1>
Your source for paper, envelopes, and writing instruments.
<P>
<UL>
<LI><A HREF="/cgi-bin/shcart.pl?cart_no=XXXX&page=paper">Paper</A>
<LI><A HREF="/cgi-bin/shcart.pl?cart_no=XXXX&page=envelopes">Envelopes</A>
<LI><A HREF="/cgi-bin/shcart.pl?cart_no=XXXX&page=writing">Writing Instruments</A>
</UL>
</BODY>
</HTML>



Notice how the value for cart_no is XXXX. This is the placeholder mentioned earlier. In the Display_Store_Page subroutine, which will be developed in the next section, this template would be read into an array, all of the XXXX values would be changed to the user's cart_no, and the array with the HTML code would be sent back to the user's Web browser. By using this same placeholder idea, you can now put together the HTML pages for the paper, envelopes, and writing instruments sections of the Virtual Stationers Web site. Listings 5.3, 5.4, and 5.5 include the HTML for these pages. Figures 5.4, 5.5, and 5.6 show how these listings would appear in Netscape.

Figure 5.3: The vshome.tmpl file

Listing 2.1: The display.pl CGI Script
<HTML>
<HEAD>
<TITLE>Virtual Stationers - Paper</TITLE>
</HEAD>
<BODY>
<H1>Virtual Stationers - Paper</H1>
<FORM METHOD=POST ACTION="/cgi-bin/shcart.pl">
<INPUT TYPE=hidden NAME="cart_no" VALUE="XXXX">
<TABLE BORDER=1>
<TR>
<TH>Item</TH>
<TH>Price</TH>
<TH>Quantity</TH>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>White 2Ølb bond, 1 Ream (5ØØ sheets)</TD>
<TD VALIGN=top ALIGN=right>$5</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="White 2Ølb, 1 Ream::5" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>White 2Ølb bond, 1 Case (1Ø Reams)</TD>
<TD VALIGN=top ALIGN=right>$35</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="White 2Ølb, 1 Case::35" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Canary 2Ølb bond, 1 Ream (5ØØ sheets)</TD>
<TD VALIGN=top ALIGN=right>$6</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Canary 2Ølb, 1 Ream::6" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Canary 2Ølb bond, 1 Case (1Ø Reams)</TD>
<TD VALIGN=top ALIGN=right>$4Ø</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Canary 2Ølb, 1 Case::4Ø" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Blue 2Ølb bond, 1 Ream (5ØØ sheets)</TD>
<TD VALIGN=top ALIGN=right>$6</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Blue 2Ølb, 1 Ream::6" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Blue 2Ølb bond, 1 Case (1Ø Reams)</TD>
<TD VALIGN=top ALIGN=right>$4Ø</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Blue 2Ølb, 1 Case::4Ø" SIZE=4></TD>
</TR>
</TABLE>
<P>
<INPUT TYPE=submit NAME="submit" VALUE="Add Items to Cart"> 
<INPUT TYPE=submit NAME="submit" VALUE="View Cart Contents"> 
<INPUT TYPE=submit NAME="submit" VALUE="Checkout">
<P>
[ <A HREF="/cgi-
bin/shcart.pl?cart_no=XXXX&page=envelopes">Envelopes</A> | <A 
HREF="/cgi-bin/shcart.pl?cart_no=XXXX&page=writing">Writing 
Instruments</A> ]
</FORM>
</BODY>
</HTML>



Figure 5.4: The paper page

Listing 5.4: The envelope.tmpl File
<HTML>
<HEAD>
<TITLE>Virtual Stationers - Envelopes</TITLE>
</HEAD>
<BODY>
<H1>Virtual Stationers - Envelopes</H1>
<FORM METHOD=POST ACTION="/cgi-bin/shcart.pl">
<INPUT TYPE=hidden NAME="cart_no" VALUE="XXXX">
<TABLE BORDER=1>
<TR>
<TH>Item</TH>
<TH>Price</TH>
<TH>Quantity</TH>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>#1Ø White (5ØØ)</TD>
<TD VALIGN=top ALIGN=right>$5</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="#1Ø White (5ØØ)::5" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>#1Ø Window (5ØØ)</TD>
<TD VALIGN=top ALIGN=right>$6</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="#1Ø Window (5ØØ)::6" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>6x9 Padded Mailer (1Ø)</TD>
<TD VALIGN=top ALIGN=right>$7</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="6x9 Padded Mailer (1Ø)::7" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>9x12 Clasp (1ØØ)</TD>
<TD VALIGN=top ALIGN=right>$6</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="9x12 Clasp (1ØØ)::6" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>1Øx13 Clasp (1ØØ)</TD>
<TD VALIGN=top ALIGN=right>$8</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="1Øx13 Clasp (1ØØ)::8" SIZE=4></TD>
</TR>
</TABLE>
<P>
<INPUT TYPE=submit NAME="submit" VALUE="Add Items to Cart"> 
<INPUT TYPE=submit NAME="submit" VALUE="View Cart Contents"> 
<INPUT TYPE=submit NAME="submit" VALUE="Checkout">
<P>
[ <A HREF="/cgi-bin/shcart.pl?cart_no=XXXX&page=paper">Paper</A> | <A 
HREF="/cgi-bin/shcart.pl?cart_no=XXXX&page=writing">Writing 
Instruments</A> ]
</FORM>
</BODY>
</HTML>



Figure 5.5: The envelopes page

Listing 5.5: The writing.tmpl File
<HTML>
<HEAD>
<TITLE>Virtual Stationers - Writing Instruments</TITLE>
</HEAD>
<BODY>
<H1>Virtual Stationers - Writing Instruments</H1>
<FORM METHOD=POST ACTION="/cgi-bin/shcart.pl">
<INPUT TYPE=hidden NAME="cart_no" VALUE="XXXX">
<TABLE BORDER=1>
<TR>
<TH>Item</TH>
<TH>Price</TH>
<TH>Quantity</TH>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Ballpoint pen, black (12)</TD>
<TD VALIGN=top ALIGN=right>$1</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Ballpoint pen, black (12)::1" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Ballpoint pen, blue (12)</TD>
<TD VALIGN=top ALIGN=right>$1</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Ballpoint pen, blue (12)::1" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Highlighter, yellow (2)</TD>
<TD VALIGN=top ALIGN=right>$1</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Highlighter, yellow (2)::1" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>#2 Pencil (12)</TD>
<TD VALIGN=top ALIGN=right>$2</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="#2 Pencil (12)::2" SIZE=4></TD>
</TR>
</TABLE>
<P>
<INPUT TYPE=submit NAME="submit" VALUE="Add Items to Cart"> 
<INPUT TYPE=submit NAME="submit" VALUE="View Cart Contents"> 
<INPUT TYPE=submit NAME="submit" VALUE="Checkout">
<P>
[ <A HREF="/cgi-bin/shcart.pl?cart_no=XXXX&page=paper">Paper</A> | <A 
HREF="/cgi-bin/shcart.pl?cart_no=XXXX&page=envelopes">Envelopes</A> ]
</FORM>
</BODY>
</HTML>


Figure 5.6: The writing instruments page

All three of these HTML pages include a quantity form that allows users to select how many items to add to their shopping carts. Each form also contains a hidden field with the cart_no value. This hidden field passes the cart_no to the CGI script if the user submits the form by pressing one of the three buttons on the page--Add Items to Cart, View Cart Contents, and Checkout. When users request a quantity of items to be added to their shopping carts, notice that the name/value pairs getting sent to the script are in the format "item description::price=quantity."

The last HTML template file you need is the file for displaying the shopping cart's contents to the user. This file is slightly different from the previous templates. It will be empty to begin with, because the contents will vary depending on the specific user's cart contents. Also, it replaces the Add Items to Cart and View Cart Contents push buttons with the Make Changes push button. Listing 5.6 contains the HTML code for the template file that displays the cart's contents and Figure 5.7 shows how this would look when a user has a few items selected.

Listing 5.6: The display.tmpl File
<HTML>
<HEAD>
<TITLE>Virtual Stationers - Contents of Your Shopping Cart</TITLE>
</HEAD>
<BODY>
<H1>Virtual Stationers</H1>
<H2>Contents of Your Shopping Cart</H2>
<FORM METHOD=POST ACTION="/cgi-bin/shcart.pl">
<INPUT TYPE=hidden NAME="cart_no" VALUE="XXXX">
<TABLE BORDER=1>
<TR>
<TH>Item</TH>
<TH>Price</TH>
<TH>Quantity</TH>
<TH>Item Subtotal</TH>
</TR>
</TABLE>
<P>
<INPUT TYPE=submit NAME="submit" VALUE="Make Changes"> 
<INPUT TYPE=submit NAME="submit" VALUE="Checkout">
<P>
[ <A HREF="/cgi-bin/shcart.pl?cart_no=XXXX&page=paper">Paper</A> | <A 
HREF="/cgi-bin/shcart.pl?cart_no=XXXX&page=envelopes">Envelopes</A> | 
<A HREF="/cgi-bin/shcart.pl?cart_no=XXXX&page=writing">Writing</A> ]
</FORM>
</BODY>
</HTML>



The File-Based Shopping Cart Script

Now that you've created the HTML templates, you need to write the shopping cart script. This script must assign the unique cart_no to each user; display the Web pages; and add, display, and modify the contents of the user's shopping cart. To make the script easier to read, write, and debug, it's best to create a subroutine for each one of these actions. The subroutines for the shopping cart script are Display_Store_Page, Assign_Cart_Number, Add_Items_To_Cart, Display_Cart_Contents, Modify_Cart_Contents, and Checkout.

Figure 5.7: The display page

The Display_Store_Page Subroutine

The Display_Store_Page subroutine is the function that parses the template files you created earlier, replaces the cart_no place holder with the correct cart number for the user, and returns the HTML page to the user's Web browser. Listing 5.7 includes the Perl code for the Display_Store_Page subroutine.

Listing 5.7: The Display_Store_Page Subroutine
sub Display_Store_Page {
  local (%data) = @_;

  # Windows users need to change the '/'s below to 
  # '\\'s. For example '/vshome.tmpl' would be
  # '\\vshome.tmpl' for Windows machines.
  local (%products) = 
       ( 'home', $path . '/vshome.tmpl', 
         'paper', $path . '/paper.tmpl',
         'envelopes', $path . '/envelope.tmpl',
         'writing', $path . '/writing.tmpl', );
  local (@template);

  $data{'cart_no'} = &Assign_Cart_Number unless $data{'cart_no'};
  $data{'page'} = 'home' unless $data{'page'}; 

  open(TEMPLATE, "$products{$data{'page'}}") || die "Content-type: 
text/html\n\nCannot open templates!";
  @template = <TEMPLATE>;
  close(TEMPLATE);

  foreach (@template) {
    s/XXXX/$data{'cart_no'}/ge;
  }

  print "Content-type: text/html\n\n";
  print @template;
  
}



Most of this subroutine should look familiar. It starts by receiving the user-supplied data as a parameter and places the data into the %data associative array. Then some other local variables are declared. The associative array %products has the name of the Web pages as the keys and the path to the associated template as the values. The variable $path is a global variable that will be set at the beginning of the shopping cart script. Listing 5.12 at the end of this section demonstrates where this variable will be set. After the local variables are declared, you should check whether the user has already been assigned a cart number. The line

$data{'cart_no'} = &Assign_Cart_Number unless $data{'cart_no'};

reads like "Assign a cart number to the variable $data{'cart_no'} unless it already has a value." The next line checks whether a page has been specified. If this subroutine is called and no page is specified, it will display the home page.

The next few lines open the appropriate template file and read in the contents to the @template array. Then each element of the @template array is checked for the cart_no placeholder XXXX, which is replaced with the real cart_no. Next the parsed header and the modified contents of the @template array are sent back to the Web browser.

The Assign_Cart_Number subroutine is only called from the Display_Store_Page subroutine. Its function is to assign a unique cart number to any user who does not already have one. It also deletes old shopping carts that are no longer in use. Listing 5.8 contains the Perl code for the Assign_Cart_Number subroutine.

Listing 5.8: The Assign_Cart_Number Subroutine
sub Assign_Cart_Number {
  local ($cart_no);

  # Windows users need to change the "ls $path/carts |" string
  # to "dir $path\\carts |".
  open(FILES, "ls $path/carts |") || die "Content-type: 
text/html\n\nCannot list existing carts!";

  while (<FILES>) {
    chop;
    unlink $_ unless -M $_ < 1;
  }
  close(FILES);

  srand(time|$$);
  $cart_no = time . "." . int(rand(999));

  # Windows users must change the string ">$path/carts/$cart_no"
  # to ">$path\\carts\\$cart_no".
  open(CART, ">$path/carts/$cart_no") || die "Content-type: 
text/html\n\nCannot open new cart!";
  close(CART);
  return $cart_no;
}



The first open statement just lists all the shopping carts currently in the carts directory. The carts directory is the subdirectory you create to store all of the shopping carts. Essentially, the string "ls $path/carts |" ("dir $path\\carts |" for Windows machines) is executed and the results are sent on the input stream FILES. The while loop checks each file received from the FILES stream, deleting it unless it was last modified less than a day ago. In other words, all shopping carts that haven't been touched for over a day are deleted. This keeps your directory from filling up with shopping carts that are no longer in use.

The next lines should look familiar. They are similar to the lines used to assign a file name for the Windows version of feedback.pl in Chapter 4. They create a unique cart number by using the current date and a random one- to three-digit extension.

The second open statement creates the shopping cart file by opening the file for output. Recall from the guest book example that the > operator preceding the file name means to open the file for output. If the file does not already exist, as is the case here, it is created. After the file is created, the output stream to the file is closed and the cart number is returned.

The Add-Items_To_Cart Subroutine

The user selects items from the store's Web pages by placing a quantity value (a number greater than 0) in the Quantity field of the form. The user than adds the selected items to his shopping cart by pressing the Add Items to Cart push button. When the user presses this push button, the shopping cart script will execute and the Add_Items_To_Cart subroutine will be called.

This subroutine must take the items selected, format the information for the shopping cart file, and place it within the file. This can all be accomplished with the Perl code shown in Listing 5.9.

Listing 5.9: The Add_Items_To_Cart Subroutine
sub Add_Items_To_Cart {
  local (%data) = @_;
  local ($cart_item, @add_items);

  foreach $key (%data) {
    if (($key ne "cart_no") && ($key ne "submit")) {
      if ($data{$key} > Ø) {
        $cart_item = join('::', $key, $data{$key}); 
        $cart_item .= "\n";
        push(@add_items, $cart_item);
      }
    }
  }

  # Windows users need to change the string 
">>$path/carts/$data{'cart_no'}"
  # to ">>$path\\carts\\$data{'cart_no'}".
  open(CART, ">>$path/carts/$data{'cart_no'}") || die "Content-type: 
text/html\n\nCannot open cart!";
  print CART @add_items;
  close(CART);

  &Display_Cart_Contents(%data);
}



The foreach loop takes each element received in the %data associative array, which is the user-supplied data, formats it, and places it in the @add_items array. What "formats it" means is to put it in a standard format that you will recognize in your other subroutines. Remember from the earlier HTML templates section that the name/value pairs getting sent to the script are in the format "item description::price=quantity." For example, if the user selected 3 reams of the 20lb white paper, the name/value pair would be "White 20lb, 1 Ream::5=3". So, this foreach loop takes each name/value pair and creates a string that looks like "White 20lb, 1 Ream::5::3\n" for this example. It then places that string into the @add_items array.

After all of the items the user selected are added to the @add_items array, the user's shopping cart is opened. Remember that the >> operator preceding the file name in an open statement means to append to the file. Once the cart file is opened, the items are appended to the file. The routine finishes with a call to the Display_Cart_Contents subroutine, which sends the entire shopping cart contents back to the user's Web browser. So, every time users make an addition to their cart, they see the current contents of the cart.

The Display_Cart_Contents Subroutine

The Display_Cart_Contents subroutine is used when the user selects the View Cart Contents push button as well as at the end of the Add_Items_To_Cart and Modify_Cart_Contents subroutines. It opens the user's shopping cart and displays all of the items in the cart. Because you want the user to be able to modify the quantity of items originally selected, you should place the quantity in an text input field. For the shopping cart display, you also calculate the subtotal of items selected. Listing 5.10 shows the Perl code for the Display_Cart_Contents subroutine.

The first part of this subroutine is similar to the Display_Store_Page subroutine. It opens and reads in the display.tmpl template file and changes the XXXX placeholder to the real cart number stored in the $data{'cart_no'} variable. The second open statement opens the user's shopping cart file and places all of the contents into the array @cart_raw.

Listing 5.10: The Display_Cart_Contents Subroutine
sub Display_Cart_Contents {
  local (%data) = @_;
  local (@template, @cart_raw, @cart_contents, $item, $price,
         $quantity, $cost, $sub);

  # Windows users need to change the string "$path/display.tmpl"
  # to "$path\\display.tmpl"
  open(TEMPLATE, "$path/display.tmpl") || die "Content-type: 
text/html\n\nCannot open template!";
  @template = <TEMPLATE>;
  close(TEMPLATE);

  foreach (@template) {
    s/XXXX/$data{'cart_no'}/ge;
  }

  # Windows users need to change the string 
"$path/carts/$data{'cart_no'}"
  # to $path\\carts\\$data{'cart_no'}".
  open(CART, "$path/carts/$data{'cart_no'}") || die "Content-type: 
text/html\n\nCannot open cart!";
  @cart_raw = <CART>;
  close(CART);

  $sub = Ø;
  foreach (@cart_raw) {
    chop;
    
    ($item, $price, $quantity) = split(/::/);
    $cost = $price * $quantity;
    $sub += $cost;

    push(@cart_contents, "<TR>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=left>$item</TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=right>\$$price</TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=left><INPUT TYPE=text 
NAME=\"$item\:\:$price\" VALUE=\"$quantity\" SIZE=4></TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=right>$cost</TD>\n");
    push(@cart_contents, "</TR>\n");
  }

  push(@cart_contents, "<TR>\n");
  push(@cart_contents, "<TD COLSPAN=4 ALIGN=right>Subtotal = 
\$$sub</TD>\n");
  push(@cart_contents, "</TR>\n");

  splice(@template, 16, Ø, @cart_contents);
  print "Content-type: text/html\n\n";
  print @template;
}



Once all of the shopping cart items are read in from the shopping cart file, you just need to format the data. The foreach loop does this. It takes each item that was added to the shopping cart file and breaks it up. Recall from the Add_Items_To_Cart subroutine that each line in the shopping cart file will be in the format item description::item price::quantity selected. First, each item is split into the individual entities--description, price and quantity--and its cost is calculated. All this information, along with the HTML tags, is then placed into the @cart_contents array. Notice that the quantity is placed in an input field so that the user can modify the amount he or she originally chose.

After each of the cart's items have been formatted for display and added to the @cart_contents array, the subtotal of all items is also added to the @cart_contents array. Then the contents of the @cart_contents array are placed within the template for the display HTML page. This is done with the splice function, in the line


splice(@template, 16, Ø, @cart_contents);

which inserts the contents of the @cart_contents array into the @template array, beginning at the index 16. Because Perl arrays are indexed starting at 0, this insertion begins at the seventeenth element of the @template array. When the insertion is completed, the parsed header and the contents of the @template array are sent back to the user's Web browser.

The Modify_Cart_Contents Subroutine

The Modify_Cart_Contents subroutine is only called when the user presses the Make Changes push button on the Web page displaying the shopping cart contents. This subroutine is similar to the Add_Items_To_Cart subroutine. Because all items are sent to the CGI script in the user data, regardless of whether the quantities were changed, it is easiest to re-create the contents of the shopping cart file. Listing 5.11 contains the Perl code for the Modify_Cart_Contents subroutine.

Like the Add_Items_To_Cart subroutine, this subroutine first loops over all of the items sent in the user-supplied data and formats them in the standard format being used for the shopping cart files. The only difference between this subroutine and the Add_Items_To_Cart subroutine is that you don't want to append the new contents to the shopping cart file. Instead, you want to overwrite the current shopping cart file with the items that were just formatted. You do this simply by changing the >> operator preceding the file name in the open statement of the add subroutine to the > operator. Remember, the >> operator means append to and the > operator means output to (overwriting if necessary).

Listing 5.11: The Modify_Cart_Contents Subroutine
sub Modify_Cart_Contents {
  local (%data) = @_;
  local ($cart_item, @add_items);

  foreach $key (%data) {
    if (($key ne "cart_no") && ($key ne "submit")) {
      if ($data{$key} > Ø) {
        $cart_item = join('::', $key, $data{$key}); 
        $cart_item .= "\n";
        push(@add_items, $cart_item);
      }
    }
  }

  # Windows users need to change the string 
">$path/carts/$data{'cart_no'}"
  # to ">$path\\carts\\$data{'cart_no'}".
  open(CART, ">$path/carts/$data{'cart_no'}") || die "Content-type: 
text/html\n\nCannot open cart!";
  print CART @add_items;
  close(CART);

  &Display_Cart_Contents(%data);
}


The Checkout Subroutine

The Perl code for a Checkout subroutine will vary depending on how your ordering system works, so it is not included here. You may want your Checkout subroutine to display all the items in the shopping cart; a subtotal for the items; any applicable tax, shipping, and handling; and a total. You may also need fields to accept credit card information if this is how you want to handle the transaction. If you already have a computerized ordering system, you may wish to place this order right into that system, or send the order via e-mail to one of your employees so he or she can handle it. In any case, all the code you would write for the Checkout subroutine would go after the declaration of local variables, as in

 sub Checkout {
   local (%data) = @_;
 
   # Place your code here.
 }

Putting It All Together: The shcart.pl File

Now that you've completed all the code for Shopping Cart subroutines, you can put together your shopping cart script. Let's call the file shcart.pl. As in the guest book example, you want to remove any Server Side Includes that the user may have placed in the input fields. Also, you can determine whether the user requested a different Web page, such as the Envelopes page, or pressed a push button by checking the value for the REQUEST_METHOD environment variable. However, in this example, unlike the guest book example, the user could have pressed a variety of push buttons. To accommodate this, you can write one more subroutine that checks the value of the name/value pair of the submit button and calls the appropriate subroutine. This subroutine, the Which_Post subroutine, is included in Listing 5.12, which contains the Perl code for the entire shcart.pl example. Remember to remove the first line and make the other specified changes if you are using this script on a Windows system.

The Netscape Cookie

From the shopping cart script, you might notice the difficulty of storing and retrieving information for the various users. As a solution to this problem, Netscape Communications Corporation (Netscape, for short) implemented a method for storing state information on the client side. State information is just information from previous requests that under normal circumstances is lost. The state information is placed within an object and sent from a CGI script through the Web server to the Web browser. When appropriate, the Web browser sends the object along with its standard HTTP request header. The new object that Netscape created is called a cookie, which is named that for what they say is "no compelling reason."

A cookie first comes into existence from the server side where it is created. Usually, a cookie is created within a CGI script, which sends the cookie to the Web server as a parsed header. The cookie is then sent to the Web browser as part of the standard response header. When it receives the cookie, the Web browser stores it in a file. Each cookie contains a range of URLs under which it is valid, and the Web browser sends the cookie as part of the HTTP request header when the cookie is within its valid domain. For example, if a user of my Web site received a cookie from one of my scripts that was valid on my entire domain, the browser would store the cookie as being valid for all URLs ending with robertm.com. When the Web browser makes a request from the robertm.com domain, the browser sends the cookie along with the standard HTTP request header.

Listing 5.12: The shcart.pl File
#!/usr/local/bin/perl

# For Windows systems, this line would need to look
# like $path = "c:\\robertm";
$path = "/users/robertm";
%data_received = &User_Data;
&No_SSI(*data_received);

$ENV{'REQUEST_METHOD'} eq "POST" ? &Which_Post(%data_received)  : 
&Display_Store_Page(%data_received);

sub Which_Post {
  local (%data) = @_;

  &Display_Cart_Contents(%data) if $data{'submit'} eq "View Cart 
Contents";
  &Add_Items_To_Cart(%data) if $data{'submit'} eq "Add Items to Cart";
  &Modify_Cart_Contents(%data) if $data{'submit'} eq "Make Changes";
  &Checkout(%data) if $data{'submit'} eq "Checkout";

}

sub Display_Store_Page {
  local (%data) = @_;

  # Windows users need to change the '/'s below to 
  # '\\'s. For example '/vshome.tmpl' would be
  # '\\vshome.tmpl' for Windows machines.
  local (%products) = 
       ( 'home', $path . '/vshome.tmpl', 
         'paper', $path . '/paper.tmpl',
         'envelopes', $path . '/envelope.tmpl',
         'writing', $path . '/writing.tmpl', );
  local (@template);

  $data{'cart_no'} = &Assign_Cart_Number unless $data{'cart_no'};
  $data{'page'} = 'home' unless $data{'page'}; 

  open(TEMPLATE, "$products{$data{'page'}}") || die "Content-type: 
text/html\n\nCannot open templates!";
  @template = <TEMPLATE>;
  close(TEMPLATE);

  foreach (@template) {
    s/XXXX/$data{'cart_no'}/ge;
  }

  print "Content-type: text/html\n\n";
  print @template;
  
}

sub Assign_Cart_Number {
  local ($cart_no);

  # Windows users need to change the "ls $path/carts |" string
  # to "dir $path\\carts |".
  open(FILES, "ls $path/carts |") || die "Content-type: 
text/html\n\nCannot list existing carts!";

  while (<FILES>) {
    chop;
    unlink $_ unless -M $_ < 1;
  }
  close(FILES);

  srand(time|$$);
  $cart_no = time . "." . int(rand(999));

  # Windows users must change the string ">$path/carts/$cart_no"
  # to ">$path\\carts\\$cart_no".
  open(CART, ">$path/carts/$cart_no") || die "Content-type: 
text/html\n\nCannot open new cart!";
  close(CART);
  return $cart_no;
}

sub Add_Items_To_Cart {
  local (%data) = @_;
  local ($cart_item, @add_items);

  foreach $key (%data) {
    if (($key ne "cart_no") && ($key ne "submit")) {
      if ($data{$key} > Ø) {
        $cart_item = join('::', $key, $data{$key}); 
        $cart_item .= "\n";
        push(@add_items, $cart_item);
      }
    }
  }

  # Windows users need to change the string 
">>$path/carts/$data{'cart_no'}"
  # to ">>$path\\carts\\$data{'cart_no'}".
  open(CART, ">>$path/carts/$data{'cart_no'}") || die "Content-type: 
text/html\n\nCannot open cart!";
  print CART @add_items;
  close(CART);

  &Display_Cart_Contents(%data);
}

sub Modify_Cart_Contents {
  local (%data) = @_;
  local ($cart_item, @add_items);

  foreach $key (%data) {
    if (($key ne "cart_no") && ($key ne "submit")) {
      if ($data{$key} > Ø) {
        $cart_item = join('::', $key, $data{$key}); 
        $cart_item .= "\n";
        push(@add_items, $cart_item);
      }
    }
  }

  # Windows users need to change the string 
">$path/carts/$data{'cart_no'}"
  # to ">$path\\carts\\$data{'cart_no'}".
  open(CART, ">$path/carts/$data{'cart_no'}") || die "Content-type: 
text/html\n\nCannot open cart!";
  print CART @add_items;
  close(CART);

  &Display_Cart_Contents(%data);
}

sub Display_Cart_Contents {
  local (%data) = @_;
  local (@template, @cart_raw, @cart_contents, $item, $price,
         $quantity, $cost, $sub);

  # Windows users need to change the string "$path/display.tmpl"
  # to "$path\\display.tmpl"
  open(TEMPLATE, "$path/display.tmpl") || die "Content-type: 
text/html\n\nCannot open template!";
  @template = <TEMPLATE>;
  close(TEMPLATE);

  foreach (@template) {
    s/XXXX/$data{'cart_no'}/ge;
  }

  # Windows users need to change the string 
"$path/carts/$data{'cart_no'}"
  # to $path\\carts\\$data{'cart_no'}".
  open(CART, "$path/carts/$data{'cart_no'}") || die "Content-type: 
text/html\n\nCannot open cart!";
  @cart_raw = <CART>;
  close(CART);

  $sub = Ø;
  foreach (@cart_raw) {
    chop;
    
    ($item, $price, $quantity) = split(/::/);
    $cost = $price * $quantity;
    $sub += $cost;

    push(@cart_contents, "<TR>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=left>$item</TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=right>\$$price</TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=left><INPUT TYPE=text 
NAME=\"$item\:\:$price\" VALUE=\"$quantity\" SIZE=4></TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=right>$cost</TD>\n");
    push(@cart_contents, "</TR>\n");
  }

  push(@cart_contents, "<TR>\n");
  push(@cart_contents, "<TD COLSPAN=4 ALIGN=right>Subtotal = 
\$$sub</TD>\n");
  push(@cart_contents, "</TR>\n");

  splice(@template, 16, Ø, @cart_contents);
  print "Content-type: text/html\n\n";
  print @template;
}

sub Checkout {
  local (%data) = @_;

  # Place your code here.
}

sub No_SSI {
  local (*data) = @_;

  foreach $key (sort keys(%data)) {
    $data{$key} =~ s/<!--(.|\n)*-->//g;
  }

}

sub User_Data {
  local (%user_data, $user_string, $name_value_pair,
         @name_value_pairs, $name, $value);

  # If the data was sent via POST, then it is available
  # from standard input. Otherwise, the data is in the
  # QUERY_STRING environment variable.
  if ($ENV{'REQUEST_METHOD'} eq "POST") {
    read(STDIN,$user_string,$ENV{'CONTENT_LENGTH'});
  } else {
    $user_string = $ENV{'QUERY_STRING'};
  }

  # This line changes the + signs to spaces.
  $user_string =~ s/\+/ /g;

  # This line places each name/value pair as a separate
  # element in the name_value_pairs array.
  @name_value_pairs = split(/&/, $user_string);

  # This code loops over each element in the name_value_pairs
  # array, splits it on the = sign, and places the value
  # into the user_data associative array with the name as the
  # key.
  foreach $name_value_pair (@name_value_pairs) {
    ($name, $value) = split(/=/, $name_value_pair);

    # These two lines decode the values from any URL
    # hexadecimal encoding. The first section searches for a
    # hexadecimal number and the second part converts the
    # hex number to decimal and returns the character
    # equivalent.
    $name =~
      s/%([a-fA-FØ-9][a-fA-FØ-9])/pack("C",hex($1))/ge;
    $value =~
       s/%([a-fA-FØ-9][a-fA-FØ-9])/pack("C",hex($1))/ge;

    # If the name/value pair has already been given a value,
    # as in the case of multiple items being selected, then
    # separate the items with a " : ".
    if (defined($user_data{$name})) {
      $user_data{$name} .= " : " . $value;
    } else {
      $user_data{$name} = $value;
    }
  }
  return %user_data;
}



When a CGI script creates a cookie, it sends a Set-Cookie statement as part of the parsed header. The Set-Cookie statement can take up to five attributes, which are shown in Table 5.1.

Cookies remain in effect until they expire, and they can be modified or deleted. To modify a cookie, send another Set-Cookie parsed header with the exact same [NAME] and path attributes. For example, suppose you previously set a cookie with the following Set-Cookie statement


Set-Cookie: login=robertm; path=/

To modify the cookie you would need to send another Set-Cookie statement such as

Set-Cookie: login=rmcdaniel; path=/



Table 5.1: The Set-Cookie Attributes
Attribute Description
[NAME] The NAME attribute is the only required attribute for the Set-Cookie statement. It is used in conjunction with an associated value, such as name=value. However, it is slightly different than the other attributes in that it does not have to be called NAME. For example, in the Set-Cookie statement Set-Cookie: login=robertm the string login=robertm is the name/value attribute for the Set-Cookie statement.
expires This attribute specifies the date and time when the cookie will expire. Its format is expires=date, where date is in the format Weekday, DD-Mon-YY HH:MM:SS GMT. The time zone (GMT) does not have to be specified for the expires attribute because GMT is the only valid time zone. If the expires attribute is not specified, the cookie will expire when the user shuts down his or her Web browser.
domain This attribute stores the domain name of the range of URLs for which the cookie is valid. When the browser compares the current domain with the domain value for a cookie, it does tail matching. Tail matching is the comparison of the last parts of the domain name to see if there is a match. For example, a domain attribute of robertm.com would tail match www.robertm.com and home.domain.robertm.com. If the domain attribute is not specified, the domain for the cookie is set to the domain name of the server generating the cookie.
path The path attribute is used to specify a subset of URLs under which the cookie is valid. It is specified as subdirectories of the domain. For example, if the domain were set to www.robertm.com and the path were set to /shopping-cart, the cookie would only be valid for URLs starting with www.robertm.com/shopping-cart (only valid for requests of items under the shopping cart subdirectory). If the path attribute is not assigned, it is given the same path as the path contained in the URI of the response header that contains the cookie.
secure This attribute takes no values. If it is present, the Web browser only sends the cookie if it has a secure connection with the Web server with which it is making the request. A secure connection for cookies means that the server is a SSL server (Secure Sockets Layer server). If the secure attribute is not present, the Web browser sends the cookie over any type of connection.



Deleting a cookie is similar to modifying it. You must send the exact same [NAME] attribute and include an expires time that is in the past. So, if you sent

Set-Cookie: login=rmcdaniel; expires Monday, Ø1-Jan-95 ØØ:ØØ:Ø1

the previous cookie with login=rmcdaniel would expire (be deleted).

When a browser sends a cookie as part of the request header, it is made available to your CGI script along with the other HTTP request header environment variables. The variable would be HTTP_COOKIE. The values of the cookie are all placed in one string, separated by a semicolon and space. For example, if you set two cookies with the following statements


Set-Cookie: login=robertm; path=/
Set-Cookie: password=mypass; path =/

the browser would send the following cookie whenever it entered your domain (until the cookies expired)

login=robertm; password=mypass

Cookies were introduced by Netscape but have not yet been adopted by all software manufacturers and do not work for all Web browsers. However, the two most widely used Web browsers, Netscape Navigator and Internet Explorer, both support cookies.

Adding the Cookie to the Shopping Cart

Now that you know about cookies, you can change the shopping cart script to use them. Instead of storing the carts in files on your server, you can store the shopping cart contents on the user's own machine.

Revising the HTML Pages

Because you do not need to keep track of which cart number belongs to which user, you do not need to use all of the template files used in the previous version. You can change the files vshome.tmpl, paper.tmpl, envelopes.tmpl, and writing.tmpl into HTML files that the users will actually see. Listings 5.13, 5.14, 5.15, and 5.16 show these modified files. All of the HTML pages, including the display.tmpl page for the cookie shopping cart example, appear in the figures for the templates of the file-based shopping cart example.


Listing 5.13: The vshome.html File
<HTML>
<HEAD>
<TITLE>Virtual Stationers</TITLE>
</HEAD>
<BODY>
<H1>Virtual Stationers</H1>
Your source for paper, envelopes, and writing instruments.
<P>
<UL>
<LI><A HREF="paper.html">Paper</A>
<LI><A HREF="envelope.html">Envelopes</A>
<LI><A HREF="writing.html">Writing Instruments</A>
</UL>
</BODY>
</HTML>



Listing 5.14: The paper.html File
<HTML>
<HEAD>
<TITLE>Virtual Stationers - Paper</TITLE>
</HEAD>
<BODY>
<H1>Virtual Stationers - Paper</H1>
<FORM METHOD=POST ACTION="/cgi-bin/ckcart.pl">
<TABLE BORDER=1>
<TR>
<TH>Item</TH>
<TH>Price</TH>
<TH>Quantity</TH>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>White 2Ølb bond, 1 Ream (5ØØ sheets)</TD>
<TD VALIGN=top ALIGN=right>$5</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="White 2Ølb, 1 Ream::5" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>White 2Ølb bond, 1 Case (1Ø Reams)</TD>
<TD VALIGN=top ALIGN=right>$35</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="White 2Ølb, 1 Case::35" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Canary 2Ølb bond, 1 Ream (5ØØ sheets)</TD>
<TD VALIGN=top ALIGN=right>$6</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Canary 2Ølb, 1 Ream::6" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Canary 2Ølb bond, 1 Case (1Ø Reams)</TD>
<TD VALIGN=top ALIGN=right>$4Ø</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Canary 2Ølb, 1 Case::4Ø" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Blue 2Ølb bond, 1 Ream (5ØØ sheets)</TD>
<TD VALIGN=top ALIGN=right>$6</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Blue 2Ølb, 1 Ream::6" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Blue 2Ølb bond, 1 Case (1Ø Reams)</TD>
<TD VALIGN=top ALIGN=right>$4Ø</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Blue 2Ølb, 1 Case::4Ø" SIZE=4></TD>
</TR>
</TABLE>
<P>
<INPUT TYPE=submit NAME="submit" VALUE="Add Items to Cart"> 
<INPUT TYPE=submit NAME="submit" VALUE="View Cart Contents"> 
<INPUT TYPE=submit NAME="submit" VALUE="Checkout">
<P>
[ <A HREF="envelope.html">Envelopes</A> | <A HREF="writing.html">Writing Instruments</A> ]
</FORM>
</BODY>
</HTML>



Listing 5.15: The envelope.html File
<HTML>
<HEAD>
<TITLE>Virtual Stationers - Envelopes</TITLE>
</HEAD>
<BODY>
<H1>Virtual Stationers - Envelopes</H1>
<FORM METHOD=POST ACTION="/cgi-bin/ckcart.pl">
<TABLE BORDER=1>
<TR>
<TH>Item</TH>
<TH>Price</TH>
<TH>Quantity</TH>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>#1Ø White (5ØØ)</TD>
<TD VALIGN=top ALIGN=right>$5</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="#1Ø White (5ØØ)::5" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>#1Ø Window (5ØØ)</TD>
<TD VALIGN=top ALIGN=right>$6</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="#1Ø Window (5ØØ)::6" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>6x9 Padded Mailer (1Ø)</TD>
<TD VALIGN=top ALIGN=right>$7</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="6x9 Padded Mailer (1Ø)::7" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>9x12 Clasp (1ØØ)</TD>
<TD VALIGN=top ALIGN=right>$6</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="9x12 Clasp (1ØØ)::6" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>1Øx13 Clasp (1ØØ)</TD>
<TD VALIGN=top ALIGN=right>$8</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="1Øx13 Clasp (1ØØ)::8" SIZE=4></TD>
</TR>
</TABLE>
<P>
<INPUT TYPE=submit NAME="submit" VALUE="Add Items to Cart"> 
<INPUT TYPE=submit NAME="submit" VALUE="View Cart Contents"> 
<INPUT TYPE=submit NAME="submit" VALUE="Checkout">
<P>
[ <A HREF="paper.html">Paper</A> | <A HREF="writing.html">Writing Instruments</A> ]
</FORM>
</BODY>
</HTML>



Listing 5.16: The writing.html File
<HTML>
<HEAD>
<TITLE>Virtual Stationers - Writing Instruments</TITLE>
</HEAD>
<BODY>
<H1>Virtual Stationers - Writing Instruments</H1>
<FORM METHOD=POST ACTION="/cgi-bin/ckcart.pl">
<TABLE BORDER=1>
<TR>
<TH>Item</TH>
<TH>Price</TH>
<TH>Quantity</TH>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Ballpoint pen, black (12)</TD>
<TD VALIGN=top ALIGN=right>$1</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Ballpoint pen, black (12)::1" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Ballpoint pen, blue (12)</TD>
<TD VALIGN=top ALIGN=right>$1</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Ballpoint pen, blue (12)::1" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>Highlighter, yellow (2)</TD>
<TD VALIGN=top ALIGN=right>$1</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="Highlighter, yellow (2)::1" SIZE=4></TD>
</TR>
<TR>
<TD VALIGN=top ALIGN=left>#2 Pencil (12)</TD>
<TD VALIGN=top ALIGN=right>$2</TD>
<TD VALIGN=top ALIGN=left><INPUT TYPE=text NAME="#2 Pencil (12)::2" SIZE=4></TD>
</TR>
</TABLE>
<P>
<INPUT TYPE=submit NAME="submit" VALUE="Add Items to Cart"> 
<INPUT TYPE=submit NAME="submit" VALUE="View Cart Contents"> 
<INPUT TYPE=submit NAME="submit" VALUE="Checkout">
<P>
[ <A HREF="paper.html">Paper</A> | <A HREF="envelope.html">Envelopes</A> ]
</FORM>
</BODY>
</HTML>



Notice that all of the XXXX cart_no placeholders have been removed. You no longer need these placeholders because the shopping cart is actually on the user's machine. Without the placeholders, there is no need to generate these files from a CGI script. Therefore all of the links to the other pages (the links to the paper, envelopes, and writing instruments pages) no longer call the shcart.pl script.

The display.tmpl file, however, needs to remain a template. Even though you do not need to change any cart_no place holders, your shopping cart script still needs to generate the contents and place it in a HTML file. Listing 5.17 shows the revised display.tmpl file for the cookie version of the shopping cart.

Listing 5.17: The display.tmpl File
<HTML>
<HEAD>
<BASE HREF="http://www.robertm.com/display.html">
<TITLE>Virtual Stationers - Contents of Your Shopping Cart</TITLE>
</HEAD>
<BODY>
<H1>Virtual Stationers</H1>
<H2>Contents of Your Shopping Cart</H2>
<FORM METHOD=POST ACTION="/cgi-bin/ckcart.pl">
<TABLE BORDER=1>
<TR>
<TH>Item</TH>
<TH>Price</TH>
<TH>Quantity</TH>
<TH>Item Subtotal</TH>
</TR>
</TABLE>
<P>
<INPUT TYPE=submit NAME="submit" VALUE="Make Changes"> 
<INPUT TYPE=submit NAME="submit" VALUE="Checkout">
<P>
[ <A HREF="paper.html">Paper</A> | <A 
HREF="envelope.html">Envelopes</A> | <A 
HREF="writing.html">Writing</A> ]
</FORM>
</BODY>
</HTML>



The Cookie-Based Shopping Cart Script

If you use the cookie, the shopping cart script becomes much easier. You no longer need the functions Display_ Store_Page or Assign_Cart_Number, and the script only gets called when the user presses one of the push buttons. For the cookie-based shopping cart, you need subroutines for adding, displaying, and modifying as well as for checkout.

The Add_Items_To_Cart Subroutine

When you use cookies, the Add_Items_To_Cart subroutine becomes very easy. Because multiple Set-Cookie parsed headers can be sent back to the Web browser, you just need to send a Set-Cookie for every item the user selected from your Web page. The Perl code in Listing 5.18 loops over all of the data received when the user pressed the Add Items to Cart push button and sends a Set-Cookie parsed header for each one. The cookie that is sent contains only the [NAME] and path attributes. The [NAME] attribute in each instance is the item description and price separated by the two colons. For example, if the user selected 3 reams of white paper, the Set-Cookie statement sent to the Web browser would look like this:

Set-Cookie: White 2Ølb, 1 Ream::5=3; path=/

Listing 5.18: The Add_Items_To_Cart Subroutine
sub Add_Items_To_Cart {
  local (%data) = @_;

  foreach $key (%data) {
    if (($key ne "submit") && ($data{$key} > Ø)) {
      $data{'new_item'} .= "; " if $data{'new_item'};
      $data{'new_item'} .= "$key=$data{$key}";
      print "Set-Cookie: $key=$data{$key}; path=/\n";
    }
  }

  &Display_Cart_Contents(%data);
}



Within the foreach loop, two lines assign elements to the array element $data{'new_item'}. You will learn the purpose of these lines in the next section, which discusses the Display_Cart_Contents subroutine.

The Display_Cart_Contents Subroutine

The cookie version of the Display_Cart_Contents subroutine is a bit more challenging because of the three different ways it can be called: It can be called by the user pressing the View Cart Contents push button, it can be called at the end of the Add_Items_To_Cart subroutine, and it can be called at the end of the Modify_Cart_Contents subroutine. In the first instance, the contents of the HTTP_COOKIE environment variable will contain all of the items to be displayed. The raw data of the cart elements will be taken from the HTTP_COOKIE variable, instead of the shopping cart file as in the file version of the shopping cart. The foreach that loops over the elements of the @cart_raw array is only slightly different than the same loop in the file-based example. Replace the single split statement in the file-based example with the two lines

($item_price, $quantity) = split(/=/);
($item, $price) = split(/::/, $item_price);

These lines first split name/value pairs of the cookie and then split the [NAME] attribute at the two colons. Listing 5.19 contains the complete Perl code for the Display_Cart_Contents subroutine.

Listing 5.19: The Display_Cart_Contents Subroutine
sub Display_Cart_Contents {
  local (%data) = @_;
  local (@template, @cart_raw, @cart_contents, $item, $price,
         $item_price, $quantity, $cost, $sub);

  # Windows users need to change the string "$path/display.tmpl"
  # to "$path\\display.tmpl".
  open(TEMPLATE, "$path/display.tmpl") || die "Content-type: 
text/html\n\nCannot open template!";
  @template = <TEMPLATE>;
  close(TEMPLATE);

  if ($data{'new_item'}) {
    $ENV{'HTTP_COOKIE'} .= "; " if $ENV{'HTTP_COOKIE'}; 
    $ENV{'HTTP_COOKIE'} .= $data{'new_item'}
  }

  $ENV{'HTTP_COOKIE'} = $data{'items'} if $data{'items'};
  @cart_raw = split(/; /, $ENV{'HTTP_COOKIE'});

  $sub = Ø;
  foreach (@cart_raw) {
    ($item_price, $quantity) = split(/=/);
    ($item, $price) = split(/::/, $item_price);
    $cost = $price * $quantity;
    $sub += $cost;

    push(@cart_contents, "<TR>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=left>$item</TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=right>\$$price</TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=left><INPUT TYPE=text 
NAME=\"$item\:\:$price\" VALUE=\"$quantity\" SIZE=4></TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=right>$cost</TD>\n");
    push(@cart_contents, "</TR>\n");
  }

  push(@cart_contents, "<TR>\n");
  push(@cart_contents, "<TD COLSPAN=4 ALIGN=right>Sub-Total = 
\$$sub</TD>\n");
  push(@cart_contents, "</TR>\n");

  splice(@template, 16, Ø, @cart_contents);
  print "Content-type: text/html\n\n";
  print @template;
  
}



When Display_Cart_Contents is called at the end of the Add_Item_To_Cart subroutine, the HTTP_COOKIE variable contains only the items that were previously in the cart, not the new items. The HTTP_COOKIE variable only reflects these additions the next time the script is called. To accommodate this situation, any items that have been added are placed in the $data{'new_item'} variable with the lines

$data{'new_item'} .= "; " if $data{'new_item'};
$data{'new_item'} .= "$key=$data{$key}";

in the Add_Items_To_Cart subroutine. Then, in Display_Cart_Contents, these items are appended to the HTTP_COOKIE variable in the lines

if ($data{'new_item'}) {
    $ENV{'HTTP_COOKIE'} .= "; " if $ENV{'HTTP_COOKIE'};
    $ENV{'HTTP_COOKIE'} .= $data{'new_item'}
  }

By checking whether $data{'new_item'} is not empty, you guarantee that the lines appending information onto the HTTP_COOKIE environment variable only take place when the Display_Cart_Contents subroutine is called from the Add_Items_To_Cart subroutine.

A similar problem occurs when Display_Cart_Contents is called from the Modify_Cart_Contents subroutine. The HTTP_COOKIE variable no longer holds the current contents of the user's shopping cart. The user has requested changes that are not yet reflected in this variable. The Modify_Cart_Contents subroutine allows for this by creating a new version of the contents of the HTTP_COOKIE variable. This new version of the contents has been placed in the variable $data{'items'}. So, if the variable is not empty, Display_Cart_Contents is being called from the Modify_Cart_Contents subroutine and the contents of the HTTP_COOKIE variable must be replaced with the contents of the $data{'items'} variable. You do this with the following Perl code:


$ENV{'HTTP_COOKIE'} = $data{'items'} if $data{'items'};

The Modify_Cart_Contents Subroutine

Like Add_Items_To_Cart, the Modify_Cart_Contents subroutine just needs to loop over all of the items returned in the %data associative array and return a Set-Cookie parsed header for each one. In the file-based example, remember that you removed any items whose quantities had been changed to a value less than 0. In this case, you need to send a Set-Cookie parsed header with an expiration date in the past to remove these items. Listing 5.20 shows the Perl code for the Modify_Cart_Contents subroutine.
Listing 5.20: The Modify_Cart_Contents Subroutine
sub Modify_Cart_Contents {
  local (%data) = @_;

  foreach $key (%data) {
    if ($key ne "submit") {
      if ($data{$key} > Ø) {
        $data{'items'} .= "; " if $data{'items'};
        $data{'items'} .= "$key=$data{$key}";
        print "Set-Cookie: $key=$data{$key}; path=/\n";
      } else {
        print "Set-Cookie: $key=$data{$key}; path=/; expires=Monday, 
Ø1-Jan-95 ØØ:ØØ:Ø1\n";
      }
    }
  }

  &Display_Cart_Contents(%data);
}



The section about the Display_Cart_Contents subroutine mentioned that the new contents for the HTTP_COOKIE variable were placed in the $data{'items'} variable. You do this with the following two lines of code:

$data{'items'} .= "; " if $data{'items'};
$data{'items'} .= "$key=$data{$key}";

The first line checks whether the variable already contains some items. If so, it appends a semicolon and space. The next line appends the next item to the list of all current items.

The Checkout Subroutine

As in the file-based shopping cart example, it's up to you to develop the contents of the Checkout subroutine. All the code for the Checkout subroutine would go after the declaration of local variables, as in

sub Checkout {
  local (%data) = @_;

  # Place your code here.
}

Putting It All Together: The ckcart.pl File

Now that you've finished all the subroutines for the cookie-based shopping cart, you can place them all together in a file called ckcart.pl. As in the file-based shopping cart, you need the Which_ Post subroutine to call the appropriate subroutine, depending on which push button the user pressed. Because the ckcart.pl script does not need to be called with the GET method, you do not need to check the REQUEST_METHOD environment variable before calling this subroutine. Listing 5.21 contains the Perl code for the ckcart.pl file. Remember to remove the first line and make all noted changes if you are using it on a Windows system.


Listing 5.21: The ckcart.pl File
#!/usr/local/bin/perl

# Windows users will need to change this line
# to look like $path = "c:\\robertm";
$path = "/users/robertm";
%data_received = &User_Data;
&No_SSI(*data_received);

&Which_Post(%data_received);

sub Which_Post {
  local (%data) = @_;

  &Display_Cart_Contents(%data) if $data{'submit'} eq "View Cart 
Contents";
  &Add_Items_To_Cart(%data) if $data{'submit'} eq "Add Items to Cart";
  &Modify_Cart_Contents(%data) if $data{'submit'} eq "Make Changes";
  &Checkout(%data) if $data{'submit'} eq "Checkout";

}

sub Add_Items_To_Cart {
  local (%data) = @_;

  foreach $key (%data) {
    if (($key ne "submit") && ($data{$key} > Ø)) {
      $data{'new_item'} .= "; " if $data{'new_item'};
      $data{'new_item'} .= "$key=$data{$key}";
      print "Set-Cookie: $key=$data{$key}; path=/\n";
    }
  }

  &Display_Cart_Contents(%data);
}

sub Modify_Cart_Contents {
  local (%data) = @_;

  foreach $key (%data) {
    if ($key ne "submit") {
      if ($data{$key} > Ø) {
        $data{'items'} .= "; " if $data{'items'};
        $data{'items'} .= "$key=$data{$key}";
        print "Set-Cookie: $key=$data{$key}; path=/\n";
      } else {
        print "Set-Cookie: $key=$data{$key}; path=/; expires=Monday, 
Ø1-Jan-95 ØØ:ØØ:Ø1\n";
      }
    }
  }

  &Display_Cart_Contents(%data);
}

sub Display_Cart_Contents {
  local (%data) = @_;
  local (@template, @cart_raw, @cart_contents, $item, $price,
         $item_price, $quantity, $cost, $sub);

  # Windows users need to change the string "$path/display.tmpl"
  # to "$path\\display.tmpl".
  open(TEMPLATE, "$path/display.tmpl") || die "Content-type: 
text/html\n\nCannot open template!";
  @template = <TEMPLATE>;
  close(TEMPLATE);

  if ($data{'new_item'}) {
    $ENV{'HTTP_COOKIE'} .= "; " if $ENV{'HTTP_COOKIE'}; 
    $ENV{'HTTP_COOKIE'} .= $data{'new_item'}
  }

  $ENV{'HTTP_COOKIE'} = $data{'items'} if $data{'items'};
  @cart_raw = split(/; /, $ENV{'HTTP_COOKIE'});

  $sub = Ø;
  foreach (@cart_raw) {
    ($item_price, $quantity) = split(/=/);
    ($item, $price) = split(/::/, $item_price);
    $cost = $price * $quantity;
    $sub += $cost;

    push(@cart_contents, "<TR>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=left>$item</TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=right>\$$price</TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=left><INPUT TYPE=text 
NAME=\"$item\:\:$price\" VALUE=\"$quantity\" SIZE=4></TD>\n");
    push(@cart_contents, "<TD VALIGN=top ALIGN=right>$cost</TD>\n");
    push(@cart_contents, "</TR>\n");
  }

  push(@cart_contents, "<TR>\n");
  push(@cart_contents, "<TD COLSPAN=4 ALIGN=right>Sub Total = 
\$$sub</TD>\n");
  push(@cart_contents, "</TR>\n");

  splice(@template, 16, Ø, @cart_contents);
  print "Content-type: text/html\n\n";
  print @template;
  
}

sub Checkout {
  local (%data) = @_;

  #Place your code here.
}

sub No_SSI {
  local (*data) = @_;

  foreach $key (sort keys(%data)) {
    $data{$key} =~ s/<!--(.|\n)*-->//g;
}

}

sub User_Data {
  local (%user_data, $user_string, $name_value_pair,
         @name_value_pairs, $name, $value);

  # If the data was sent via POST, then it is available
  # from standard input. Otherwise, the data is in the
  # QUERY_STRING environment variable.
  if ($ENV{'REQUEST_METHOD'} eq "POST") {
    read(STDIN,$user_string,$ENV{'CONTENT_LENGTH'});
  } else {
    $user_string = $ENV{'QUERY_STRING'};
  }

  # This line changes the + signs to spaces.
  $user_string =~ s/\+/ /g;

  # This line places each name/value pair as a separate
  # element in the name_value_pairs array.
  @name_value_pairs = split(/&/, $user_string);

  # This code loops over each element in the name_value_pairs
  # array, splits it on the = sign, and places the value
  # into the user_data associative array with the name as the
  # key.
  foreach $name_value_pair (@name_value_pairs) {
    ($name, $value) = split(/=/, $name_value_pair);

    # These two lines decode the values from any URL
    # hexadecimal encoding. The first section searches for a
    # hexadecimal number and the second part converts the
    # hex number to decimal and returns the character
    # equivalent.
    $name =~
      s/%([a-fA-FØ-9][a-fA-FØ-9])/pack("C",hex($1))/ge;
    $value =~
       s/%([a-fA-FØ-9][a-fA-FØ-9])/pack("C",hex($1))/ge;

    # If the name/value pair has already been given a value,
    # as in the case of multiple items being selected, then
    # separate the items with a " : ".
    if (defined($user_data{$name})) {
      $user_data{$name} .= " : " . $value;
    } else {
      $user_data{$name} = $value;
    }
  }
  return %user_data;
}


home ch01 ch02 ch03 ch04 ch05 ch06 ch07 ch08 ch09 quick reference