Lemur zaprasza
Chapter 13 Multipart Forms and Maintaining State CONTENTS 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 In Chapter 8, "Client/Server Issues," 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. This is discussed in detail in Chapter 14, "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 (discussed in Chapter 14) 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") { "<option>Bill Clinton\n"; "<option>Donkey\n"; } else { "<option>Bob Dole\n"; "<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 (see Figure 13.4). 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. Figure 13.4 : Tic-tac-toc state information. 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. A typical board is shown in Figure 13.5 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. Figure 13.5: A board created by text-tictactoe Listing 13.7. The HTML that created the board shown in Figure 13.5. <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. I create a version of the tic-tac-toe program that does this in Chapter 15, "Imagemaps." Before writing a forms- and image-based CGI game, it would be nice to write a purely textual form-based application and use it as the foundation for the more advanced, better looking game. 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 (as shown in Figure 13.6). 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. Figure 13.6 : A form-based tic-tac-toe game. 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, as I show in Chapter 15. 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. For a detailed discussion of databases and CGI, see Chapter 12, "Databases." 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 example in Chapter 12. 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. A sample database tree is shown in Figure 13.7 and a sample course file is shown in Listing 13.11. Figure 13.7 : Sample database directory tree. 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); "<h4>$DEPT $CN: $TITLE</h4>\n"; "<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); "<h4>$DEPT $CN: $TITLE</h4>\n"; "<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. |