CGI Scripts and Cookies

Lincoln Stein

In the last installment of this column I promised to talk about MiniSvr, a central part of the CGI::* modules and an easy way to maintain state within a CGI script. Unfortunately I've made several changes to my system in the last few weeks (upgrading to Perl 5.003 and Apache 1.1b4), and MiniSvr broke. At press time, I still haven't figured out what's gone wrong. So today I'm going to talk about a sweeter subject, cookies.

What's a cookie? The folks at Netscape came up with the idea for Navigator 1.1. It's just a name=value pair, much like the named parameters used in the CGI query string. When a Web server or CGI script wants to save some state information, it creates a cookie or two and sends them to the browser inside the HTTP header. The browser keeps track of all the cookies sent to it by a particular server and stores them in an on-disk database so that the cookies persist even when the browser is closed and reopened later. The next time the browser connects to a Web site, it searches its database for all cookies that belong to that server and transmits them back to the server within the HTTP header.

Cookies can be permanent or set to expire after a number of hours or days. They can be made site-wide, so that the cookie is available to every URL on your site, or restricted to a partial URL path. You can also set a flag in the cookie so that it's only transmitted over the Internet when the browser and server are communicating by a secure protocol such as SSL. You can even create promiscuous cookies that are sent to every server in a particular Internet domain.

The idea is simple but powerful. If a CGI script needs to save a small amount of state information, such as the user's preferred background color, it can be stored directly in a cookie. If lots of information needs to be stored, you can keep the information in a database on the server's side and use the cookie to record a session key or user ID. Other browsers have begun to adopt cookies (notably Microsoft in its Internet Explorer), and cookies are on their way to becoming a part of the HTTP standard.

So how do you create a cookie? If you use the library it's a piece of cake:

0  #!/usr/bin/perl  
2  use CGI qw(:standard);   
4  $cookie1 = cookie(-name  => 'regular', 
5                    -value => 'chocolate chip');   
6  $cookie2 = cookie(-name  => 'high fiber', 
7                    -value => 'oatmeal raisin');   
8  print header(-cookie => [$cookie1, $cookie2]); 

Line 2 loads the CGI library and imports the :standard set of function calls using a syntax that's new in library versions 2.21 and higher. This syntax allows you to call all of the CGI object's methods without explicitly creating a CGI instance - a default CGI object is created for you behind the scenes. Lines 4 through 7 create two new cookies using the CGI cookie() method. The last step is to incorporate the cookies into the document's HTTP header. We do this in line 8 by printing out the results of the header() method, passing it the -cookie parameter along with an array reference containing the two cookies.

When we run this script from the command line, the result is:

   Set-cookie: regular=chocolate%20chip 
   Set-cookie: high%20fiber=oatmeal%20raisin 
   Content-type: text/html  

As you can see, translates spaces into %20's, as the Netscape cookie specification prohibits whitespace and certain other characters, such as the semicolon. (It also places an upper limit of a few kilobytes on the size of a cookie, so don't try to store the text of Hamlet in one.) When the browser sees these two cookies it squirrels them away and returns them to your script the next time it needs a document from your server.

To retrieve the value of a cookie sent to you by the browser, use cookie() without a -value parameter:

0  #!/usr/bin/perl   
2  use CGI qw(:standard);   
4  $regular    = cookie('regular');   
5  $high_fiber = cookie('high fiber');   
7  print header(-type => 'text/plain'),   
8      "The regular cookie is $regular.\n",   
9      "The high fiber cookie is $high_fiber.";  

In this example, lines 4 and 5 retrieve the two cookies by name. Lines 7 through 9 print out an HTTP header (containing no cookie this time), and two lines of text. The output of this script, when viewed in a browser, would be

	The regular cookie is chocolate chip. 
	The high fiber cookie is oatmeal raisin.  

The cookie() method is fairly flexible. You can save entire arrays as cookies by giving the -value parameter an array reference:

	$c = cookie(-name => 'specials', 
           -value => ['oatmeal',
                      'chocolate chip','alfalfa']);  

Or you can save and restore whole associative arrays:

$c = cookie(-name => 'prices', 
           -value => {      'oatmeal' => '$0.50',
                     'chocolate_chip' => '$1.25',
                            'alfalfa' => 'free'});

Later you can recover the two cookies this way:

	@specials = cookie('specials'); 
	%prices   = cookie('prices'); 

By default, browsers will remember cookies that only until they exit, and will only send the cookie out to scripts with a URL path that's similar to the script that generated it. If you want them to remember the cookie for a longer period of time, pass an -expires parameter to cookie() containing the cookie's shelf life. To change the URL path over which the cookie is valid, pass its value in -path:

	$c = cookie(-name => 'regular', 
           -value => 'oatmeal raisin', 
            -path => '/cgi-bin/bakery', 
         -expires => '+3d'); 

This cookie will expire in three days' time ('+3d'). Other cookie() parameters allow you to adjust the domain names and URL paths that trigger the browser to send a cookie, and to turn on cookie secure mode. The -path parameter shown here tells the browser to send the cookie to every program in /cgi-bin/bakery.

Figure 1: Fill Out Form

Figure 1: Fill Out Form

The next page shows a CGI script called configure.cgi. When you call this script's URL you're presented with the fill-out form shown above. You can change the page's background color, the text size and color, and even customize it with your name. The next time you visit this page (even if you've closed the browser and come back to the page weeks later), it remembers all of these values and builds a page based on them.

This script recognizes four CGI parameters used to change the configuration:

Usually these parameters are sent to the script via the fill out form that it generates, but you could set them from within a URL this way:


Let's walk through the code. Line 2 imports the CGI library, bringing in both the standard method calls and a number of methods that generate HTML3-specific tags. Next we define a set of background colors and sizes. The choice of colors may seem capricious, but it's not. These are the background colors defined by the newly-released HTML3.2 standard, and they're based on the original colors used by the IBM VGA graphics display.

Line 9 is where we recover the user's previous preferences, if any. We use the cookie() method to fetch a cookie named "preferences," and store its value in a like-named associative array.

In lines 12 through 14, we fetch the CGI parameters named text, background, name, and size. If any of them are set, it indicates that the user wants to change the corresponding value saved in the browser's cookie. We store changed parameters in the %preferences associative array, replacing the original values.

00  #!/usr/bin/perl   
02  use CGI qw(:standard :html3);   
04  # Some constants to use in our form.   
05  @colors = qw/aqua black blue fuchsia gray green lime maroon navy 
              olive purple red silver teal white yellow/;   
06  @sizes = ("<default>", 1..7);   
08  # recover the "preferences" cookie.  
09  %preferences = cookie('preferences');  
11  # If the user wants to change the background color or her name, 
    # they will appear among our CGI parameters.  
12  foreach ('text', 'background', 'name', 'size') {  
13      $preferences{$_} = param($_) || $preferences{$_};  
14  }  
16  # Set some defaults  
17  $preferences{background} = $preferences{background} || 'silver';  
18  $preferences{text} = $preferences{text} || 'black';  
20  # Refresh the cookie so that it doesn't expire.  
21  $the_cookie = cookie( -name => 'preferences', 
22                        -value=> \%preferences, 
23                        -path => '/', 
24                     -expires => '+30d');  
25  print header(-cookie => $the_cookie);  
27  # Adjust the title to incorporate the user's name, if provided.  
28  $title = $preferences{name} ? "Welcome back, $preferences{name}!" : 
                                  "Customizable Page";  
30  # Create the HTML page.  We use several of the HTML 3.2 
    # extended tags to control the background color
31  # and the font size. It's safe to use these features because 
    # cookies don't work anywhere else anyway.  
32  print start_html(-title => $title, 
33                 -bgcolor => $preferences{background}, 
34                    -text => $preferences{text});  
36  print basefont({SIZE=>$preferences{size}}) 
                if $preferences{size} > 0;  
38  print h1($title),<<END;  
39  You can change the appearance of this page by submitting
40  the fill-out form below.  If you return to this page any time 
41  within 30 days, your preferences will be restored.  
42  END  
43  ;  
44  # Create the form  
45  print hr, 
46      start_form, 
48      "Your first name: ", 
49      textfield(-name    => 'name', 
50                -default => $preferences{name}, 
51                -size    => 30), br, 
52      table( 
53        TR( 
54           td("Preferred"), 
55           td("Page color:"), 
56           td(popup_menu(   -name => 'background', 
57                          -values => \@colors, 
58                         -default => $preferences{background})
59             )
60          ),  
61        TR( 
62           td(''), 
63           td("Text color:"), 
64           td(popup_menu(   -name => 'text', 
65                          -values => \@colors, 
66                         -default => $preferences{text})
67             )
68          ),  
69        TR( 
70           td(''), 
71           td("Font size:"), 
72           td(popup_menu(   -name => 'size', 
73                          -values => \@sizes, 
74                         -default => $preferences{size})
75             )
76          )
77      ), 
78      submit(-label => 'Set preferences'), 
79      end_form, 
80      hr;  
82  print a({HREF=>"/"}, 'Go to the home page'); 

To try out this script online, point your browser at:

Line 17 and 18 set the text and background colors to reasonable defaults if they can't be found in either the cookie or the CGI script parameters.

Lines 21 through 25 generate the page's HTTP header. First we use the cookie() method to create the cookie containing the user's preferences. We set the expiration date for the cookie for 30 days in the future so that the cookie will be removed from the browser's database if the user doesn't return to this page within that time. We also set the optional -path parameter to /. This makes the cookie valid over our entire site so that it's available to every URL the browser fetches. Although we don't take advantage of this yet, it's useful if we later decide that these preferences should have a site-wide effect. Lastly we emit the HTTP header with the -cookie parameter set.

In lines 30 to 36 we begin the HTML page. True to the intent of making it personalizable, we base the page title on the user's name. If it's set, the title and level 1 header both become "Welcome back <name>!" Otherwise, the title becomes an impersonal "Customizable page." Line 32 calls the start_html() method to create the top part of the HTML page. It sets the title, the background color and the text color based on the values in the %preferences array. Line 36 sets the text size by calling the basefont() method. This simply generates a <BASEFONT> HTML tag with an appropriate SIZE attribute.

Lines 38 and up generate the content of the page. There's a brief introduction to the page, followed by the fill-out form used to change the settings. All the HTML is generated using "shortcuts," in which tags are generated by like-named method calls. For example, the hr() method generates the HTML tag <HR>. As shown in the first column in this series, we start the fill-out form with a call to start_form(), create the various form elements with calls to textfield(), popup_menu(), and submit(), and close the form with end_form().

When I first wrote this script, the popup menus and popup menus in the form didn't line up well. Because all the elements were slightly different widths, everything was crooked. To fix this problem, I used the common trick of placing the form elements inside an invisible HTML3 table. Assigning each element to its own cell forces the fields to line up. You can see how I did this in lines 52 through 77, where I define a table using a set of shortcuts. An outer call to table() generates the surrounding <TABLE> and </TABLE> tags. Within this are a series of TR() methods, each of which generates a <TR> tag. (In order to avoid conflict with Perl's built-in tr/// operator, this is one instance where uses uppercase rather than lowercase shortcut names.) Within each TR() call, in turn, there are several td() calls that generate the <TD> ("table data") cells of the HTML table.

Fortunately my text editor auto-indents nicely, making it easy to see the HTML structure.

On a real site, of course, you'd want the user's preferences to affect all pages, not just one. This isn't a major undertaking; many modern Web servers now allow you to designate a script that preprocesses all files of a certain type. You can create a variation on the script shown here that takes an HTML document and inserts the appropriate <BASEFONT> and <BODY> tags based on the cookie preferences. Now just configure the server to pass all HTML documents through this script and you're set.

Cringe, Microsoft, cringe!