|
Multipart
Forms and Maintaining State
chapter 13
Strategies
·
Environment Variables
·
Hidden HTML Form Fields
·
Session Files
Multipart
Forms
Tic-Tac-Toe
·
General Functions
·
Passing State Using URLs
·
Passing State Using Forms
Shopping
Carts
·
Online Course Catalog
Summary
you learn that the Web protocol is
stateless. This is inconvenient for the CGI programmer because many
applications require knowledge of previous states. One common application
that needs to maintain state information is a multipart form. Suppose you
have several HTML forms that collect information. After the last form is
filled out, you want all of the information filled out from all of the forms
to be submitted at once to the CGI program. In order to do this, each
current form must remember the input typed on previous forms.
Another common application that
requires maintaining state is a shopping cart application. Many online
stores enable you to browse a catalog and select an item you want to order.
This item is added to your "shopping cart"; you can then browse through the
other items in the catalog and add desirable items to your cart. After you
are finished browsing, you can view your cart and purchase its contents.
Every state of this application must remember what is in the shopping cart.
This chapter covers the basic
strategies for maintaining state. You see some examples of multipart forms,
develop a CGI tic-tac-toe game, and develop a type of shopping cart
application.
Strategies
You can take advantage of CGI and
HTML to maintain state over the Web. Although none of the strategies
presented here are perfect, they are satisfactory for most applications.
What sort of data do you want to
maintain across states? In a multipart form, you might want to store all the
previously entered form input in the current state. After you fill out the
last form, you want to submit all of the information you entered in previous
forms as well. You might need an identifying state, something to tell your
CGI application or the Web browser which form to use, which data to use, or
what action to take next.
There are three different methods
of maintaining state using CGI: passing the information through environment
variables, via hidden HTML form fields, or by storing the information in a
file. All three methods have different advantages and disadvantages.
|
Note |
|
A few browsers-including
Netscape and Internet Explorer-have one additional method of
maintaining state: HTTP cookies."Proprietary Extensions." |
|
Maintaining State: Server
or Client's Job? |
|
The strategies presented
here rely mostly on the server for maintaining state. The server
processes the state information, stores it in some form (an HTML form,
a URL, or a session file) and passes this information back to the
client. The next time the client talks to the server, it returns this
state information, which the server then proceeds to process again.
Even HTTP cookies use a combination of server and client communication
to maintain state.
All of these methods are
less than ideal, each with its own limitations. Programming a
relatively simple multiform application requires writing a fairly
complex CGI program. This seems a waste, because all a multiform
application is doing is collecting information from several different
pages. Programming a multiform application shouldn't be more difficult
than programming a single-form application, but it is. Consequently,
many applications that require maintenance of state are not
well-suited for the Web.
You can use new client-side
application technology (such as Java) to overcome these limitations.
For example, you could program your own multipage, non-stateless
applet in Java that loads from only one HTTP connection. After the
user is finished entering information in this applet, he or she can
submit all of the information at once. Because this multipage applet
requires only one HTTP connection, you can use the same CGI
application that processes a single form application to process this
multiple-part application.
Although new technology
promises to present improvements and strategies in tackling
conventional CGI problems such as maintaining state, the strategies
presented here are far from archaic. Choosing how you maintain state
depends on several factors, including development time and the desired
simplicity (or complexity) of the application. Maintaining state using
the CGI strategies presented here will be useful for a long time to
come. |
Environment Variables
The easiest way to maintain state
is by passing the information to the URL. For example, suppose you need to
save an ID number across several different pages. You could append the ID
number to the URL, either following a question mark (the QUERY_STRING
variable) or following a slash (the PATH_INFO variable).
http://myserver.org/cgi-bin/form?12345
http://myserver.org/cgi-bin/form/12345
If you need to store two different
variables (for example, name and ID number), you could use both.
http://myserver.org/cgi-bin/form/Bob?12345
In this case, Bob is stored in
PATH_INFO and 12345 is in QUERY_STRING.
Passing information using URLs is
useful because it doesn't require forms. You maintain state by appending the
state information to all the URLs within your document. For example, given
one of the previous URLs, all you need to do to pass the state on to the
next page is to reference the page as the following or something similar:
print "<a href=\"/cgi-bin/form?$ENV{'QUERY_STRING'}\">Next
page</a>\n";
However, using environment
variables to store state has the same disadvantage as using the GET method
for form submission. There is an upper size limit for both the length of the
URL and the storage size of the environment variable. For large amounts of
data, using environment variables is unsatisfactory.
Hidden HTML Form Fields
You can overcome the size
limitations of environment variables by maintaining state using the HTML
forms tag <input type=hidden>. The concept is similar to environment
variables. Instead of appending the environment variable to references to
the URL, you embed the state information in the form using the <input
type=hidden> information.
For example, suppose you have a
two-part form. When you click the Submit button on the second form, you want
to submit information from both forms. Remember, because these forms are
inside CGI scripts, no action attribute is needed. Your first form might
look like this:
<form method=POST>
<p>Enter your name: <input name="name"><br>
Enter your age: <input name="age"></p>
<p><input type=submit></p>
</form>
When you click Submit, the values
for "name" and "age" are passed to the CGI program. The CGI program should
return the second form with the information from the first form embedded as
<input type=hidden> tags.
<form method=POST>
<!-- state information from previous form -->
<input type=hidden name="name" value="Corwyn">
<input type=hidden name="age" value="21">
<!-- second form -->
<p>What kind of cola do you like? <input name="cola"><br>
Do you like soccer? <input type=radio name="soccer" value="yes"> Yes
<input type=radio name="soccer" value="no"> No<br>
Have you ever been to Highland Park, NJ?
<input type=radio name="NJ" value="yes"> Yes
<input type=radio name="NJ" value="no"> No</p>
<p><input type=submit></p>
</form>
When you submit this second form,
both the information from the first and second forms are submitted.
Although this method overcomes the
limitation of environment variables, it causes a new limitation: You can use
<input type=hidden> only if your application uses forms. However, not all
CGI applications that need state information use forms. Sometimes using
forms is undesirable because it adds unnecessary complications to the CGI
programming. An example of this is presented later in the "Passing State
Using Forms" section of this chapter.
Session Files
Both of the previous methods for
maintaining state are unable to maintain state over different sessions. For
example, suppose you are in the middle of filling out a long multipart form
when you have to leave for an appointment. If you quit your Web browser, you
lose all of the information you have entered. Similarly, if your session
crashes while you are typing information, you must retype everything
starting from the first form.
Using session files to maintain
state enables you to overcome this difficulty and to maintain state over
different sessions. Instead of storing state information in environment
variables or forms, store it in a file on the server. An application can
access previous state information simply by accessing this file. The format
of the file is flexible; it could be a text file, a directory containing
files with the state information, or a database.
With session files, you still need
to pass some identifying attribute of the session file from state to state
of the application by using one of the previous methods. However, if for any
reason you need to quit your Web session, you can recover the state
information by remembering your session ID.
|
Tip |
|
A good method for creating a
unique session ID is to use the time function. For example, in Perl
the code is
$sessionID = time;
This is almost certain to be
unique. For added randomness on a UNIX machine, you can append the
process ID.
$sessionID = time."-".$$;
|
Using a session file to maintain
state can be risky because, theoretically, one user can guess the session ID
of another user and capture his or her state information. You can take
several steps to make sure only the proper user has access to his or her own
state information. Remember, it is important to include session information
with files, even when you are going to delete them after the CGI program has
terminated.
For example, make the first page
of your application a login application that asks for a username and a
password. When the user enters this information, the application adds the
username and password to a password file and then generates an associated
session ID. In order to retrieve your session ID or to access your session
file, you need to re-authenticate using your username and password.
One other limitation is that it
leaves extraneous files on your server. After a user starts filling out a
form, the CGI application has no way of knowing if the user decided not to
finish filling out the form or simply left for a two-hour coffee break.
Either way, the file is left on the server. You can address this problem by
writing a program that periodically cleans out the directory for files that
haven't been touched for more than a day.
Multipart
Forms
To demonstrate a simple multipart
form, I'll design a voting booth CGI program. The specifications are very
simple:
·
Two pages. The first page asks for name, state,
and party affiliation. The second page presents a list of candidates for
whom you can vote according to your party affiliation. The candidates are
hard-coded in the application.
·
After you fill out and submit both forms, a
confirmation message is sent that tells you your name, state of residence,
and for whom you voted.
A good way to develop applications
like this is to create all of your forms first (without the state
information). The basic forms for this application are in Listings 13.1 and
13.2. Both forms contain the hidden input field "nextpage". This value
informs the CGI program which page to send next.
Listing 13.1. The first form.
<html>
<head>
<title>Voting Booth</title>
</head>
<body>
<h1>Voting Booth</h1>
<form method=POST>
<input type=hidden name="nextpage" value="two">
<p>Name: <input name="name"><br>
State: <input name="state" size=3></p>
<p><select name="party">
<option>Democrat
<option>Republican
</select></p>
<p><input type=submit value="Vote!"></p>
</form>
</body>
</html>
Listing 13.2 is the second form,
which is used if the voter is a Democrat. The only difference between this
form and the one for Republicans (Listing 13.1) is the candidates.
Listing 13.2. The second form.
<html>
<head>
<title>Vote for Whom?</title>
</head>
<body>
<h1>Vote for Whom?</h1>
<form method=POST>
<input type=hidden name="nextpage" value="three">
<select name="candidate">
<option>Bill Clinton
<option>Donkey
</select>
<p><input type=submit value=\"Vote!\"></p>
</form>
</body>
</html>
Because I'm using forms, I use the
<input type=hidden> tag to maintain state. The second form is the only one
that needs to maintain state. Before sending this form, the program embeds
hidden fields that contain the value of the information submitted from the
first page. When this second page is submitted, the application prints the
confirmation message.
The entire application, called
vote, is shown in Listing 13.3. When you first run this application, you see
the form in Listing 13.1. Suppose you type the information depicted in
Figure 13.1. Because you entered Republican as your party affiliation,
vote returns a Republican ballot, as shown in
Figure 13.2. After you make your selection, a confirmation message like
the one in
Figure 13.3 appears.
Figure 13.1 : First form from vote.
Figure 13.2 : The second form: a Republican ballot.
Figure 13.3 : The confirmation mesage.
Listing 13.3. The vote
application.
#!/usr/local/bin/perl
require 'cgi-lib.pl';
&ReadParse(*input);
if ($input{'nextpage'} eq "two") {
print &PrintHeader,&HtmlTop("Vote for Whom?");
print "<form method=POST>\n";
print "<input type=hidden name=\"nextpage\" value=\"three\">\n";
print "<input type=hidden name=\"name\" value=\"$input{'name'}\">\n";
print "<input type=hidden name=\"state\" value=\"$input{'state'}\">\n";
print "<input type=hidden name=\"party\" value=\"$input{'party'}\">\n";
print "<select name=\"candidate\">\n";
if ($input{'party'} eq "Democrat") {
print "<option>Bill Clinton\n";
print "<option>Donkey\n";
}
else {
print "<option>Bob Dole\n";
print "<option>Elephant\n";
}
print "</select>\n";
print "<p><input type=submit value=\"Vote!\"></p>\n";
print "</form>\n";
print &HtmlBot;
}
elsif ($input{'nextpage'} eq "three") {
print &PrintHeader,&HtmlTop("Thanks for Voting!");
print <<EOM;
<p>Thank you for voting, $input{'name'} from $input{'state'}. You voted for:
<b>$input{'candidate'}</b>. Thank you for participating in our
democracy!</p>
EOM
print &HtmlBot;
}
else {
print &PrintHeader,&HtmlTop("Voting Booth");
print <<EOM;
<form method=POST>
<input type=hidden name="nextpage" value="two">
<p>Name: <input name="name"><br>
State: <input name="state" size=3></p>
<p><select name="party">
<option>Democrat
<option>Republican
</select></p>
<p><input type=submit value="Vote!"></p>
</form>
EOM
print &HtmlBot;
}
Tic-Tac-Toe
In order to demonstrate another
application of state, I wrote a tic-tac-toe game. Tic-tac-toe is a simple
game, and yet it is not trivial to implement as a CGI program. It presents
several challenges, among them, presenting a board, accepting the player's
move, and maintaining state.
My tic-tac-toe game works like
this:
·
It provides a blank board. The user makes the
first move.
·
The CGI stores this move and then checks to see if
the user has won. If the user has won or if there's a stalemate, the game is
over; otherwise, the computer moves. Once again, the CGI checks for a winner
or a stalemate.
·
If no one has won, the CGI generates a new board
based on the state of the previous board.
The program needs some method of
passing the state of the previous board to the next board. The state of the
board is easily expressed as a nine-character string with the first three
characters representing the top row, the second three characters
representing the middle row, and the last three characters representing the
final row . The program only needs this one string, and it is not likely
that a user will need to maintain state over different sessions. Therefore,
using a session file to pass state is overkill.
Passing state via environment
variables or via a hidden input field are both good options for this game.
Determining which method you use depends on how you present the information
and accept user input. You can either display the board using a form or by
using a text board with several references. I programmed tic-tac-toe using
both methods, comparing and contrasting the advantages and disadvantages of
each.
General Functions
Common to programs using both
methods are the functions that actually play the game. I use a
two-dimensional three-by-three array of integers to store the board value.
The player plays X and the computer plays O. An X represents a 1 on the
array, an O a -1, and an empty spot a zero. The function set_board() in
Listing 13.4 converts a state string into values on this two-dimensional
array. Likewise, board2string() (also in Listing 13.4) converts the array
into a state string.
Listing 13.4. The set_board()
and board2string() functions.
void set_board(int
board[3][3],char *state)
{
int i;
for (i = 0; i<9; i++) {
if (state[i] == 'x')
board[i%3][i/3] = 1;
else if (state[i] == 'o')
board[i%3][i/3] = -1;
else
board[i%3][i/3] = 0;
}
}
char *board2string(int board[3][3])
{
char *str = malloc(10);
int i,j;
for (j=0; j<3; j++)
for (i=0; i<3; i++) {
if (board[i][j] == 1)
str[i+j*3] = 'x';
else if (board[i][j] == -1)
str[i+j*3] = 'o';
else
str[i+j*3] = '_';
}
str[9] = '\0';
return str;
}
In order to determine whether
there is a winner on the board, you sum up the rows, columns, and two
diagonals. The sum will be a number ranging from -3 to 3. If the value of
any sum is 3, then the human (X) wins. If the value of any sum is -3, then
the computer (O) wins; otherwise, there is no winner. The function
check_winner() (shown in Listing 13.5) sums each row, column, and diagonal
and returns a 1 if the human wins, a -1 if the computer wins, a 2 if there
is no winner and the board is full (a stalemate), or a 0 otherwise.
Listing 13.5. The check_winner()
function.
int check_winner(int board[3][3])
{
int i,j;
short FOUND = 0;
/* 0 = go on, 1 = human wins, -1 = computer wins, 2 = stalemate */
/* sum up horizontals */
for (j = 0; j<3; j++) {
if (board[0][j]+board[1][j]+board[2][j] == 3)
return 1;
else if (board[0][j]+board[1][j]+board[2][j] == -3)
return -1;
}
/* try verticals */
for (i = 0; i<3; i++) {
if (board[i][0]+board[i][1]+board[i][2] == 3)
return 1;
else if (board[i][0]+board[i][1]+board[i][2] == -3)
return -1;
}
/* now test diagonals */
i = board[0][0]+board[1][1]+board[2][2];
j = board[2][0]+board[1][1]+board[0][2];
if ( (i==3) || (j==3) )
return 1;
else if ( (i==-3) || (j==-3) )
return -1;
for (j = 0; j<3; j++)
for (i = 0; i<3; i++)
if (board[i][j] == 0)
FOUND = 1;
if (FOUND)
return 0;
else
return 2;
}
Finally, a function is needed that
determines the computer's move. I wanted the computer to play a "stupid"
game of tic-tac-toe so the human could easily win, so I have the computer
pick a random empty spot on the board. The function computer_move() (shown
in Listing 13.6) makes the computer's move.
Listing 13.6. The computer_move()
function.
void computer_move(int
board[3][3])
{
int positions[9];
int i,j,move;
int num = 0;
/* we can assume there are empty positions; otherwise, this function
would not have been called */
/* find empty positions */
for (j=0; j<3; j++)
for (i=0; i<3; i++)
if (board[i][j] == 0) {
positions[num] = i+j*3;
num++;
}
/* pick random number from 0 to num-1 */
move = (int) ((double) num*rand()/(RAND_MAX+1.0));
board[positions[move]%3][positions[move]/3] = -1;
}
Passing State Using URLs
I first describe the game by
passing state via URLs. and the HTML that created it is shown in Listing
13.7. Each empty spot is represented by a question mark and is surrounded by
an <a href> tag. Within each <a href> tag is the state of the board if the
user decides to move on that spot. In order to generate all of these states,
the program needs an algorithm to determine all possible states according to
the current board state and the empty positions on the board.
Listing 13.7.
<html> <head>
<title>Your Move</title>
</head>
<body>
<h1>Your Move</h1>
<pre>
X | <a href="/cgi-bin/text-tictactoe?xxo______">?</a> | O
<a href="/cgi-bin/text-tictactoe?x_ox_____">?</a> | <a href=
Â"/cgi-bin/text-tictactoe?x_o_x____">?</a>
| <a href=
Â"/cgi-bin/text-tictactoe?x_o__x___">?</a>
<a href="/cgi-bin/text-tictactoe?x_o___x__">?</a> | <a href=
Â"/cgi-bin/text-tictactoe?x_o____x_">?</a>
| <a href=
Â"/cgi-bin/text-tictactoe?x_o_____x">?</a>
</pre>
<p><a href="/cgi-bin/text-tictactoe">Start new game</a></p>
</body> </html>
|
Note |
|
The text board is not
visually pleasing. You could draw three images, a square with an X, a
square with an O, and an empty square, and use these images inline
instead of the text X, O, and ? to improve the look of this program. |
Clicking any of the question marks
on the board calls text-tictactoe again and tells it how the board should
look. The program then determines whether the human has won; if he or she
hasn't won, then the computer moves. The complete code for text-tictactoe is
shown in Listing 13.8.
Listing 13.8. The text-tictactoe.c
program.
#include <stdio.h>
#include <math.h>
#include "cgi-lib.h"
#include "html-lib.h"
#include "string-lib.h"
void set_board(int board[3][3],char *state)
{
int i;
for (i = 0; i<9; i++) {
if (state[i] == 'x')
board[i%3][i/3] = 1;
else if (state[i] == 'o')
board[i%3][i/3] = -1;
else
board[i%3][i/3] = 0;
}
}
char *board2string(int board[3][3])
{
char *str = malloc(10);
int i,j;
for (j=0; j<3; j++)
for (i=0; i<3; i++) {
if (board[i][j] == 1)
str[i+j*3] = 'x';
else if (board[i][j] == -1)
str[i+j*3] = 'o';
else
str[i+j*3] = '_';
}
str[9] = '\0';
return str;
}
int check_winner(int board[3][3])
{
int i,j;
short FOUND = 0;
/* 0 = go on, 1 = human wins, -1 = computer wins, 2 = stalemate */
/* sum up horizontals */
for (j = 0; j<3; j++) {
if (board[0][j]+board[1][j]+board[2][j] == 3)
return 1;
else if (board[0][j]+board[1][j]+board[2][j] == -3)
return -1;
}
/* try verticals */
for (i = 0; i<3; i++) {
if (board[i][0]+board[i][1]+board[i][2] == 3)
return 1;
else if (board[i][0]+board[i][1]+board[i][2] == -3)
return -1;
}
/* now test diagonals */
i = board[0][0]+board[1][1]+board[2][2];
j = board[2][0]+board[1][1]+board[0][2];
if ( (i==3) || (j==3) )
return 1;
else if ( (i==-3) || (j==-3) )
return -1;
for (j = 0; j<3; j++)
for (i = 0; i<3; i++)
if (board[i][j] == 0)
FOUND = 1;
if (FOUND)
return 0;
else
return 2;
}
void computer_move(int board[3][3])
{
int positions[9];
int i,j,move;
int num = 0;
/* we can assume there are empty positions; otherwise, this function
would not have been called */
/* find empty positions */
for (j=0; j<3; j++)
for (i=0; i<3; i++)
if (board[i][j] == 0) {
positions[num] = i+j*3;
num++;
}
/* pick random number from 0 to num-1 */
move = (int) ((double) num*rand()/(RAND_MAX+1.0));
board[positions[move]%3][positions[move]/3] = -1;
}
void print_board(char *msg,char *state)
{
int i,j;
char pstate[9];
html_begin(msg);
h1(msg);
printf("<pre>\n");
for (j=0; j<3; j++) {
for (i=0; i<3; i++) {
switch (state[i + j*3]) {
case '_':
strcpy(pstate,state);
pstate[i + j*3] = 'x';
printf("<a href=\"/cgi-bin/text-tictactoe?%s\">",pstate);
printf("?</a>");
break;
case 'x':
printf("X");
break;
case 'o':
printf("O");
break;
}
switch(i) {
case 0: case 1:
printf(" | ");
break;
case 2:
printf("\n");
break;
}
}
}
printf("</pre>\n");
printf("<p><a href=\"/cgi-bin/text-tictactoe\">");
printf("Start new game</a></p>\n");
}
void next_move(int board[3][3])
{
int winner;
char state[9];
html_header();
strcpy(state,board2string(board));
if (strcmp(state,"_________")) {
winner = check_winner(board);
if (winner == 1) /* human wins */
print_board("You Win!",state);
else if (winner == 2)
print_board("Stalemate",state);
else if (winner == 0) { /* computer's turn */
computer_move(board);
strcpy(state,board2string(board));
winner = check_winner(board);
if (winner == -1)
print_board("Computer Wins!",state);
else if (winner == 2)
print_board("Stalemate",state);
else
print_board("Your Move",state);
}
}
else
print_board("Your Move",state);
html_end();
}
int main()
{
int board[3][3];
if (QUERY_STRING == NULL)
set_board(board,"_________");
else
set_board(board,QUERY_STRING);
next_move(board);
}
-------------------------------------------------
Passing State Using Forms
The code for text-tictactoe is
fairly clean and straightforward. There doesn't seem to be a good reason at
this point to reimplement tic-tac-toe using <input type=hidden> tags to pass
the state, especially since it requires forms. However, you can create a
nice-looking form that enables the user to click an imagemap of the board,
determines where on the board the user clicked, and plays the game from
there.
The challenge for creating a
textual, forms-based implementation of tic-tac-toe (form-tictactoe) is
presenting the board and accepting user input. I use a form with a submit
button for each empty spot on the board . Each submit button is named
"location" and contains its coordinates as its value. Listing 13.9 contains
the HTML for one board scenario. Notice that the current state of the board
is stored in a hidden field. When the user selects a submit button, the
current state and the user's move is submitted to the CGI program form-tictactoe.
Listing 13.9. The HTML form for
one board scenario.
<html> <head>
<title>Your Move</title>
</head>
<body>
<h1>Your Move</h1>
<form action="/cgi-bin/form-tictactoe" method=POST>
<input type=hidden name="board" value="ooxox___x">
<p>
O
O
X
<br>
O
X
<input type=submit name="location" value="21">
<br>
<input type=submit name="location" value="02">
<input type=submit name="location" value="12">
X
</p>
</form>
</body> </html>
Form-tictactoe differs slightly
from text-tictactoe in that it receives the previous board configuration and
the user's move rather than the new board configuration containing the
user's move. This is only a slight change, and the revised code is shown in
Listing 13.10.
Listing 13.10. form-tictactoe.c.
#include <stdio.h>
#include <math.h>
#include "cgi-lib.h"
#include "html-lib.h"
#include "string-lib.h"
void set_board(int board[3][3],char *state)
{
int i;
for (i = 0; i<9; i++) {
if (state[i] == 'x')
board[i%3][i/3] = 1;
else if (state[i] == 'o')
board[i%3][i/3] = -1;
else
board[i%3][i/3] = 0;
}
}
char *board2string(int board[3][3])
{
char *str = malloc(10);
int i,j;
for (j=0; j<3; j++)
for (i=0; i<3; i++) {
if (board[i][j] == 1)
str[i+j*3] = 'x';
else if (board[i][j] == -1)
str[i+j*3] = 'o';
else
str[i+j*3] = '_';
}
str[9] = '\0';
return str;
}
int check_winner(int board[3][3])
{
int i,j;
short FOUND = 0;
/* 0 = go on, 1 = human wins, -1 = computer wins, 2 = stalemate */
/* sum up horizontals */
for (j = 0; j<3; j++) {
if (board[0][j]+board[1][j]+board[2][j] == 3)
return 1;
else if (board[0][j]+board[1][j]+board[2][j] == -3)
return -1;
}
/* try verticals */
for (i = 0; i<3; i++) {
if (board[i][0]+board[i][1]+board[i][2] == 3)
return 1;
else if (board[i][0]+board[i][1]+board[i][2] == -3)
return -1;
}
/* now test diagonals */
i = board[0][0]+board[1][1]+board[2][2];
j = board[2][0]+board[1][1]+board[0][2];
if ( (i==3) || (j==3) )
return 1;
else if ( (i==-3) || (j==-3) )
return -1;
for (j = 0; j<3; j++)
for (i = 0; i<3; i++)
if (board[i][j] == 0)
FOUND = 1;
if (FOUND)
return 0;
else
return 2;
}
void computer_move(int board[3][3])
{
int positions[9];
int i,j,move;
int num = 0;
/* we can assume there are empty positions; otherwise, this function
would not have been called */
/* find empty positions */
for (j=0; j<3; j++)
for (i=0; i<3; i++)
if (board[i][j] == 0) {
positions[num] = i+j*3;
num++;
}
/* pick random number from 0 to num-1 */
move = (int) ((double) num*rand()/(RAND_MAX+1.0));
board[positions[move]%3][positions[move]/3] = -1;
}
void print_play(char *msg,char *state)
{
int i,j;
html_begin(msg);
h1(msg);
printf("<pre>\n");
for (j=0; j<3; j++) {
for (i=0; i<3; i++) {
switch (state[i + j*3]) {
case '_':
printf(" ");
break;
case 'x':
printf("X");
break;
case 'o':
printf("O");
break;
}
switch(i) {
case 0: case 1:
printf(" | ");
break;
case 2:
printf("\n");
break;
}
}
}
printf("</pre>\n");
printf("<p><a href=\"/cgi-bin/form-tictactoe\">");
printf("Play again?</a></p>\n");
}
void print_move(char *msg,char *state)
{
int i,j;
char xy[3];
html_begin(msg);
h1(msg);
printf("<form action=\"/cgi-bin/form-tictactoe\" method=POST>\n");
printf("<input type=hidden name=\"board\" value=\"%s\">\n",state);
printf("<p>\n");
for (j=0; j<3; j++) {
for (i=0; i<3; i++) {
switch (state[i + j*3]) {
case '_':
sprintf(xy,"%d%d",i,j);
printf("<input type=submit name=\"location\" value=\"%s\">\n",xy);
break;
case 'x':
printf("X\n");
break;
case 'o':
printf("O\n");
break;
}
}
printf("<br>\n");
}
printf("</form>\n");
}
void print_board(int board[3][3], int x, int y)
{
int winner;
char state[9];
html_header();
strcpy(state,board2string(board));
if (x != -1) { /* win? if not, computer moves. */
board[x][y] = 1;
strcpy(state,board2string(board));
winner = check_winner(board);
if (winner == 1) /* human wins */
print_play("You Win!",state);
else if (winner == 2)
print_play("Stalemate",state);
else if (winner == 0) { /* computer's turn */
computer_move(board);
strcpy(state,board2string(board));
winner = check_winner(board);
if (winner == -1)
print_play("Computer Wins!",state);
else if (winner == 2)
print_play("Stalemate",state);
else
print_move("Your Move",state);
}
}
else
print_move("Your Move",state);
html_end();
}
int main()
{
int board[3][3];
char xy[3];
int x,y;
llist coordinates;
if (read_cgi_input(&coordinates)) {
set_board(board,cgi_val(coordinates,"board"));
strcpy(xy,cgi_val(coordinates,"location"));
x = atoi(substr(xy,0,1));
y = atoi(substr(xy,1,1));
}
else {
set_board(board,"_________");
x = -1;
y = -1;
}
print_board(board,x,y);
list_clear(&coordinates);
}
As you can see, form-tictactoe is
slightly more complex than text-tictactoe; therefore, passing state through
the URL seems to be the better strategy in this case. However, you can make
form-tictactoe present the board in an interesting and visually pleasing way
using imagemaps, which you can't do by passing the information using URLs, .
If presentation is important for you, then the form-based approach works
best.
Shopping
Carts
One of the most common
applications on the World Wide Web is the shopping cart application. The
idea is that the user is provided with a shopping cart. The user can browse
through different pages and descriptions on a Web site and add items to his
or her shopping cart. The user should be able to examine the shopping cart
and remove or add items. When the user is finished selecting items, he or
she can order the items.
As with other CGI applications
that require state, you can effectively use all three methods of maintaining
state with shopping cart applications. Normally, however, because of the
space restriction of the URL method, you are limited to either hidden form
fields or a session file. A session file is often ideal in this scenario
because users might want a chance to leave the site temporarily, possibly to
look at other sites or maybe just to take a little break. A session file
allows the user to save state across different sessions, a useful feature in
a shopping cart application. However, the potential for other users to
capture your shopping cart is a dangerous one, and you should take the
proper steps to avoid this possibility.
One important part of all shopping
cart applications is a database on the back end. You need a way to store
descriptions and information about all the items in your store.
Online Course Catalog
The term
shopping cart application
is a general one. Not all shopping cart applications are actually an online
ordering system. As an example of a shopping cart application, I have
designed an online course catalog.
Several schools have made their
course catalogs available on the World Wide Web. Some have added a search
feature that makes it easy to pick courses. Another nice feature would be to
allow students to select courses while browsing the catalog online. This is
equivalent to adding the courses to a shopping cart. After the student is
finished browsing through the catalog, he or she could look at the shopping
cart, compare the classes, resolve classes meeting at conflicting times, and
then easily prioritize his or her class choices. After the student is
finished organizing the shopping cart, he or she could "order" the classes
(submit the course list for registration).
I develop here a very basic online
course catalog system upon which you can easily add more features. Here are
the requirements for this system:
·
All of the courses and course description and
information must be stored in some sort of database.
·
The CGI program lists courses by department. While
browsing through the courses, you can add courses to your basket. A course
cannot be added to your basket more than once.
·
When you are finished choosing your courses, you
can display your choices. The display should show conflicting times.
I assume each course has the
following information:
·
Department
·
Course number
·
Course title
·
Days it meets
·
Time it meets
·
Course description
For simplicity's sake, I make
certain assumptions about the courses. Courses either meet on
Monday-Wednesday-Friday or on Tuesday-Thursday. Each course description is
one paragraph. None of the fields will contain double pipes (||).
I use a file system database
similar to the one used in the video library . Each department is
represented by a top-level directory. Within each directory are files
containing the course information in the following format:
TITLE={}
DAYS={} # MWF || TT
TIME={} # 24 hour format
DESC={}
where the information is contained
within the braces. I allow for the possibility of braces within the fields
in a hexadecimal encoded form. The name of the file is the course number.
The DAYS field contains either MWF for Monday-Wednesday-Friday or TT for
Tuesday-Thursday. The Time is a number between 0 and 24; all of the classes
meet on the hour. and a sample course file is shown in Listing 13.11.
Listing 13.11. A sample course
file called 101, located in the english/ directory.
TITLE={Intro to English}
DAYS={MWF}
TIME={10}
DESC={An introduction to the wonders and nuances of the written English
language. Learn how to read, write, and speak proper English!}
In
Chapter 12, I wrote some Perl code that parses this kind of file. This
code is recycled as the function &parse_file, shown in Listing 13.12. &parse_file
returns a list containing the course title, the days and time the course
meets, and the course description.
Listing 13.12. The &parse_file
function.
sub parse_file {
local($DEPT,$CN) = @_;
local($filename) = $dbasedir.$DEPT."/".$CN;
local($TITLE,$DAYS,$TIME,$DESC);
local($field);
if (-e $filename) {
# read fields of each record
open(RECORD,$filename)
|| &CgiDie("Error","Couldn't Open Record");
$/ = '}';
while ($field = <RECORD>) {
$field =~ s/^[\r\n]//;
if ($field =~ /^TITLE=\{/) {
($TITLE = $field) =~ s/^TITLE=\{//;
$TITLE =~ s/\}//;
$TITLE = &decode($TITLE);
}
elsif ($field =~ /^DAYS=\{/) {
($DAYS = $field) =~ s/^DAYS=\{//;
$DAYS =~ s/\}//;
$DAYS = &decode($DAYS);
}
elsif ($field =~ /^TIME=\{/) {
($TIME = $field) =~ s/^TIME=\{//;
$TIME =~ s/\}//;
$TIME = &decode($TIME);
}
elsif ($field =~ /^DESC=\{/) {
($DESC = $field) =~ s/^DESC=\{//;
$DESC =~ s/\}//;
$DESC = &decode($DESC);
}
}
$/ = '\n';
close(RECORD);
return ($TITLE,$DAYS,$TIME,$DESC);
}
else {
&CgiDie("Error","Record does not exist");
}
}
sub decode {
local($data) = @_;
$data =~ s/%([0-9a-fA-F]{2})/pack("c",hex($1))/ge;
return $data;
}
Using this database as a back end,
my program, called courses, does the following:
·
Given no input, it lists the departments. The
student can select the department whose courses he or she wants to browse.
·
After the student chooses a department, an HTML
form with a detailed description of each course is displayed. At the end of
each description is a submit button that allows the user to choose a course.
There are two links at the bottom of the page: one to go back to the
departmental list and the other to view the shopping cart's current
contents.
·
When the student views the shopping cart, it
displays the courses chosen, sorted by time. Courses that meet at the same
time are displayed under the same time heading.
All of the information is
dynamically created from the database and from previous user input and is
heavily dependent on forms. Therefore, it only seems natural to use hidden
input fields as the way to pass state.
|
Note |
|
As stated earlier, using a
session file might actually be more practical for this shopping cart
application. Implementing a shopping cart using a session file is left
as an exercise to the reader. |
As advised earlier, I begin
designing this application by first displaying all of the basic forms. There
are three possible forms shown in Listings 13.13, 13.14, and 13.15. The
first form compiles a list of departments from the database and displays
that list. It contains a state variable "form" that says which form to
display next. In this case, it says to display the "list" form-the second
form, which displays a detailed list of the courses in the selected
department.
Listing 13.13. The first form:
a list of departments.
<html>
<head>
<title>Which Department?</title>
</head>
<body>
<h1>Which Department?</h1>
<form method=POST>
<input type=hidden name="form" value="list">
<select name="department">
<option>english
<option>government
<option>history
<option>math
<option>physics
</select>
<p><input type=submit value="Choose Department"></p>
</form>
</body>
</html>
This second form contains both the
name of the department selected and the state "form" in hidden fields. Note
that the value of "form" is once again "list," or the second form. If a
student selects a course by pressing the submit button, courses redisplay
the same list, except without the option of selecting that course again. At
the bottom of this form there are two options: either to go back to the list
of departments or to view the courses selected (the third form).
Listing 13.14. Second form: a
detailed list of courses.
<html>
<head>
<title>english</title>
</head>
<body>
<h1>english</h1>
<hr>
<form method=POST>
<input type=hidden name="department" value="english">
<input type=hidden name="form" value="list">
<h2>Intro to English</h2>
<h3>english 101</h3>
<p><i>MWF</i>, <b>10</b></p>
<p>An introduction to the wonders and nuances of the written English
language. Learn how to read, write, and speak proper English!</p>
<p><input type=submit name="courses" value="english 101"></p>
<hr>
<h2>English as a Second Language</h2>
<h3>english 105</h3>
<p><i>MWF</i>, <b>10</b></p>
<p>Learn how to speak English fluently in 21 days, or your money back!</p>
<p><input type=submit name="courses" value="english 105"></p>
<hr>
<h2>Shakespeare</h2>
<h3>english 210</h3>
<p><i>MWF</i>, <b>11</b></p>
<p>In this course, we read and discuss the greatest of Shakespeare's
works, including Macbeth, Romeo and Juliet, and A Midsummer's Night
Dream.</p>
<p><input type=submit name="courses" value="english 210"></p>
<hr>
<p><input type=submit name="do" value="Show Schedule"><br>
<input type=submit name="do" value="Choose Department"></p>
</form>
</body>
</html>
The third form does not really
need to be a form at all for this implementation of courses, because there
are no options on this form. However, since you might want to develop this
program further, I display this HTML document as a form that contains the
courses you have chosen as hidden fields. You can add such functionality as
removing courses or submitting the courses for registration.
Listing 13.15. The third form:
the student's chosen courses.
<html>
<head>
<title>Classes Chosen</title>
</head>
<body>
<h1>Classes Chosen</h1>
<form action=POST>
<input type=hidden name="courses" value="english 101">
<input type=hidden name="courses" value="english 105">
<input type=hidden name="courses" value="english 210">
<h2>Monday, Wednesday, Friday</h2>
<hr>
<h3>10</h3>
<h4>english 101: Intro to English</h4>
<p>An introduction to the wonders and nuances of the written English
language. Learn how to read, write, and speak proper English!</p>
<hr>
<h4>english 105: English as a Second Language</h4>
<p>Learn how to speak English fluently in 21 days, or your money back!</p>
<hr>
<h3>11</h3>
<h4>english 210: Shakespeare</h4>
<p>In this course, we read and discuss the greatest of Shakespeare's
works, including Macbeth, Romeo and Juliet, and A Midsummer's Night
Dream.</p>
<hr>
<h2>Tuesday, Thursday</h2>
<hr>
</form>
</body>
</html>
There are two form fields I use to
maintain state. The first is field "form," which tells courses which form to
display next. The second is field "courses," which contains the courses you
have selected in the form "Department CourseNumber."
The complete source code for
courses is shown in Listing 13.16.
Listing 13.16. The courses
application.
#!/usr/local/bin/perl
require 'cgi-lib.pl';
$dbasedir = '/home/eekim/Web/CGI/courses/';
$indexfile = 'index';
# $QUERY_STRING = $ENV{'QUERY_STRING'};
$num_input = &ReadParse(*input);
if (!$input{'form'} || ($input{'do'} eq "Choose Department")) {
@departments = &get_dir($dbasedir);
print &PrintHeader,&HtmlTop("Which Department?");
print "<form method=POST>\n";
&next_form("list");
&print_state;
print "<select name=\"department\">\n";
foreach $dept (@departments) {
print "<option>$dept\n";
}
print "</select>\n";
print "<p><input type=submit value=\"Choose Department\"></p>\n";
print "</form>\n";
print &HtmlBot;
}
elsif ($input{'do'} eq "Show Schedule") {
print &PrintHeader,&HtmlTop("Classes Chosen");
print "<form action=POST>\n";
@chosen = &print_state;
# damn, wish Perl4 had struct
# %mwf|%tt{$TIME} = $DEPT||$CN||$TITLE||$DESC{}$DEPT||$CN||$TITLE||$DESC
undef %mwf;
undef %tt;
foreach $class (@chosen) {
($DEPT,$CN) = split(/ /,$class);
($TITLE,$DAYS,$TIME,$DESC) = parse_file($DEPT,$CN);
if ($DAYS eq "MWF") {
$mwf{$TIME} .= "{}" unless (!$mwf{$TIME});
$mwf{$TIME} .= "$DEPT||$CN||$TITLE||$DESC";
}
else {
$tt{$TIME} .= "{}" unless (!$mwf{$TIME});
$tt{$TIME} .= "$DEPT||$CN||$TITLE||$DESC";
}
}
print "<h2>Monday, Wednesday, Friday</h2>\n";
print "<hr>\n";
foreach $time (sort keys(%mwf)) {
print "<h3>$time</h3>\n";
foreach $course (split("{}",$mwf{$time})) {
($DEPT,$CN,$TITLE,$DESC) = split(/\|\|/,$course);
print "<h4>$DEPT $CN: $TITLE</h4>\n";
print "<p>$DESC</p>\n";
print "<hr>\n";
}
}
print "<h2>Tuesday, Thursday</h2>\n";
print "<hr>\n";
foreach $time (sort keys(%tt)) {
print "<h3>$time</h3>\n";
foreach $course (split("{}",$tt{$time})) {
($DEPT,$CN,$TITLE,$DESC) = split(/\|\|/,$course);
print "<h4>$DEPT $CN: $TITLE</h4>\n";
print "<p>$DESC</p>\n";
print "<hr>\n";
}
}
print "</form>\n";
print &HtmlBot;
}
elsif ($input{'form'} eq "list") {
$dept = $input{'department'};
@courses = &get_dir($dbasedir.$dept);
print &PrintHeader,&HtmlTop($dept);
print "<hr>\n";
print "<form method=POST>\n";
print "<input type=hidden name=\"department\" value=\"$dept\">\n"
unless (!$dept);
&next_form("list"); # this needs to be changed
@chosen = &print_state;
foreach $cn (@courses) {
&print_file($dept,$cn,@chosen);
}
print "<p><input type=submit name=\"do\" value=\"Show Schedule\"><br>\n";
print "<input type=submit name=\"do\" value=\"Choose Department\"></p>\n";
print "</form>\n";
print &HtmlBot;
}
sub get_dir {
local($dir) = @_;
local(@directories);
opendir(DIR,$dir) || &CgiDie("Error","Can't read directory");
@directories = grep(!/^\./,readdir(DIR));
closedir(DIR);
return sort(@directories);
}
sub next_form {
local($form) = @_;
print "<input type=hidden name=\"form\" value=\"$form\">\n\n";
}
sub print_state {
local($course,@chosen);
foreach $course (split("\0",$input{'courses'})) {
print "<input type=hidden name=\"courses\" value=\"$course\">\n";
push(@chosen,$course);
}
print "\n";
return @chosen;
}
sub print_file {
local($DEPT,$CN,@chosen) = @_;
local($TITLE,$DAYS,$TIME,$DESC) = &parse_file($DEPT,$CN);
print "<h2>$TITLE</h2>\n";
print "<h3>$DEPT $CN</h3>\n";
print "<p><i>$DAYS</i>, <b>$TIME</b></p>\n";
print "<p>$DESC</p>\n";
print "<p><input type=submit name=\"courses\" value=\"$DEPT $CN\"></p>\n"
unless (grep(/^$DEPT $CN$/,@chosen));
print "<hr>\n";
}
sub parse_file {
local($DEPT,$CN) = @_;
local($filename) = $dbasedir.$DEPT."/".$CN;
local($TITLE,$DAYS,$TIME,$DESC);
local($field);
if (-e $filename) {
# read fields of each record
open(RECORD,$filename)
|| &CgiDie("Error","Couldn't Open Record");
$/ = '}';
while ($field = <RECORD>) {
$field =~ s/^[\r\n]//;
if ($field =~ /^TITLE=\{/) {
($TITLE = $field) =~ s/^TITLE=\{//;
$TITLE =~ s/\}//;
$TITLE = &decode($TITLE);
}
elsif ($field =~ /^DAYS=\{/) {
($DAYS = $field) =~ s/^DAYS=\{//;
$DAYS =~ s/\}//;
$DAYS = &decode($DAYS);
}
elsif ($field =~ /^TIME=\{/) {
($TIME = $field) =~ s/^TIME=\{//;
$TIME =~ s/\}//;
$TIME = &decode($TIME);
}
elsif ($field =~ /^DESC=\{/) {
($DESC = $field) =~ s/^DESC=\{//;
$DESC =~ s/\}//;
$DESC = &decode($DESC);
}
}
$/ = '\n';
close(RECORD);
return ($TITLE,$DAYS,$TIME,$DESC);
}
else {
&CgiDie("Error","Record does not exist");
}
}
sub decode {
local($data) = @_;
$data =~ s/%([0-9a-fA-F]{2})/pack("c",hex($1))/ge;
return $data;
}
Summary
There are three methods of
maintaining state using CGI: using environment variables, hidden HTML form
fields, and session files. Which method you use depends largely on need,
including how much information you need to maintain across state and how you
want to present the information.
Two important types of
applications that require knowledge of previous states are multipart forms
and shopping cart applications. Both have a variety of applications, and
both can use all three methods of maintaining state to their advantage. |