Perl and CGI part I Cookies, Query Strings, and Post variables.... OH My!!!! If you're just starting out in perl and trying to figure out how to handle all that cgi stuff you have a number of options. You can use a CPAN module (CGI, CGI-Lite, FCGI...) or you can use a do it yourself solution. The CPAN Modules have the benefit of handling everything for you with easy(supposedly) to use methods. On the other hand if you don't need all the features of a CPAN Module a do it yourself solution may be less confusing and have a smaller codebase. In this article I will walk you through making a custom module for your CGI to handle the basics of a CGI application. This article assumes familiarity with basic Perl Syntax and Modules. All those Cookies, Query Strings and Post Variables are made possible in the HTTP world by something called HTTP headers. The HTTP header tells the application (browser, Server, rss reader.....) making the page request useful information for displaying the page. Some of the Header is built automatically by the HTTP server(apache, IIS....) or application making a request. However, if you want to include cookies, or retrieve query strings, post variables and cookies values on the server side, you have to be able to retrieve the values from those headers. Now in my case I needed to be able to set and read cookies and retrieve Query strings and Post variables. I didn't need to be able to write out html automatically or output anything else other than that header. Rather than try to figure out how to use the CPAN modules for just those tasks and nothing else I decided to stretch myself a little and write my own. I like learning new things and it turned out not to be any more difficult than wading through the documentation for the CPAN modules would have been. The first thing to remember when woking on a CGI Module is the last line of the HTTP Header. The last line you say? Why do we start on the last line? The reason is that any header regardless of whether it has anything else in it must have this last line. Additionally this line must be last because, when the browser sees it, it stops processing the header and assumes everything after it is part of the page itself. What is this magical line: "Content-type: text/html\n\n" This line tells the browser application what mime type the page is. In this case it is html text. I could just as easily have set it to text/xml, text/rss, or any other mime type I cared to, including my own custom mime types. For more information about mime types you can look here. For our custom module we are are going to use an object to represent the header. What we want, is to be able to add cookies to the header. We also want to leave it open to possibly adding other things to the header later. When our header is created we will retrieve the string by using a method. The first thing our Header object needs is a constructor method, which we will call new_header, in the interest of readable code.

package pk_cgi; require Exporter; use strict; our @ISA = qw(Exporter); our @EXPORT = qw(cgi_request new_header get_cookie_list get_cookie); sub new_header { my $proto = shift; my $class = ref($proto) || $proto; my $header = "Content-type: text/html\n\n"; return bless($header, $class); }
What we did here is create a string with that all important last line in the header. This header is actually completely viable now. we could output it and it would be perfectly acceptable to the requesting application. It does not however have any cookies defined in it. Those will be added later if we should want them. We will use an object method for those. As you can see the object creation is almost absurdly simple. Just a string blessed into the object. Should you wish, you could also add arguments to set the mime type to something else. Now, what about those cookies? How do we handle those? Cookies are handled by lines in the header like this one "Set-Cookie: cookiename=cookievalue\n". That is a basic cookie header. you could also add some optional parameters: "Set-Cookie: cookiename=cookievalue; path=value; expires=value; domain=value\n" Our method needs to build the Set-Cookie line based on parameters and then prepend the line onto our header object. That prepend is very important, remember. because the line already in the header has to stay last. We will use a hash to pass the cookie's name and value pair into the method. If we have any of the optional parameters to set we can store those in the hash also.
sub add_cookie { my $self = shift; my $Cookie = shift; my $String = "Set-Cookie: " . $$Cookie{name} . "="; $String .= "$$Cookie{value}"; if (exists ($$Cookie{path})) { ## set cookies path $String .= "; path="; $String .= $$Cookie{path}; } if (exists ($$Cookie{expires})) { ## set cookies expiration $String .= "; expires="; $String .= $$Cookie{expires}; } if (exists ($$Cookie{domain})) { ## set cookies domain $String .= "; domain="; $String .= $$Cookie{expires}; } $$self = $String . "\n" . $$self; }
Again the method is absurdly simple. using the values from the hash to build the header line and prepending it to the header object's string. what if we want multiple values in our cookies though? We could just set a whole bunch of cookies for each name=value pair we needed, but for some applications this would quickly get unwieldy to use. What we need is multivalue cookies. Happily such a thing is possible. We just have to work out a way to separate a cookie's value string into sub name=value pairs. To do this we need a separator for each pair and one to separate each name from the value. These separators are arbitrary, but you probably want to use something that isn't likely to occur in your names or values. Also the ; and = is not a good idea since it is used elswhere in the HTTP header as a separator. I chose the ":" and the "," to act as separators. The Colon separates names from values and the Comma separates name:value pairs. Once we have our separators we need to change our object method so it can recognize if we are setting multivalue or single value cookies and act accordingly. We still use the hash to pass the values, but this time if its a multivalue cookie the value key of the hash stores a reference to another hash holding the name=value pairs for our multivalue cookie. We need to test for the presence of this hash and generate our Set-Cookie line accordingly. Here is our new object method.
sub add_cookie { my $self = shift; my $Cookie = shift; my $String = "Set-Cookie: " . $$Cookie{name} . "="; if (ref($$Cookie{value}) eq "HASH") { #print "its a hash"; my $CookieValue = $$Cookie{value}; foreach my $key (sort(keys(%$CookieValue))) { $String = $String . $key . ":" . $$CookieValue{$key} . ","; } #print $String; } else { #print "its not a hash"; #print $$Cookie{value}; $String .= "$$Cookie{name}:$$Cookie{value}, "; } if (exists ($$Cookie{path})) { ## set cookies path $String .= "; path="; $String .= $$Cookie{path}; } if (exists ($$Cookie{expires})) { ## set cookies expiration $String .= "; expires="; $String .= $$Cookie{expires}; } if (exists ($$Cookie{domain})) { ## set cookies domain $String .= "; domain="; $String .= $$Cookie{expires}; } $$self = $String . "\n" . $$self; #print $$self; }
We changed several things in order to facilitate retrieval of our cookies later with the new multivalue cookie format. Since, while retrieving our cookies, we don't know whether the cookie is a multivalue cookie or not storing the single value cookie in the same format will make it easier on us during retrieval. The new method checks for a hash reference in $$Cookie{value} if there is a hash reference then it stores the multiple values in that hash. If there isn't a hash then it stores the single value in the same format. We can now handle setting cookies in our CGI module. All we have left is retrieving the cookies. There are two ways we might want to retrieve the cookies we've set in our application: Retrieving a list of all the cookies sent, or retrieving a cookie by name. Retrieving by name is probably the most useful of the two so lets start with that one. First we need to pull the list of cookies out of the header. then we need to locate the cookie we want to find and finally we need to return that cookies value or list of name=value pairs. Since we had to foresight to store single values in the same format as multivalues we made it a little easier on ourselves. We can treat single value cookies the same as multivalue cookies. We will pass the cookie id in as a string. When a browser application sends cookies to the server they get stored in perl's %ENV hash under the HTTP_COOKIE key. If the key doesn't exist then there were no cookies. So lets get started on that method. We pull the list of cookies out of the header like so:
sub get_cookie { my $CookieId = shift; my %CookieVars; if (exists $ENV{'HTTP_COOKIE'}) { my @buffer = split(/;/,$ENV{'HTTP_COOKIE'}); } else { $CookieVars{Status} = 0; return 0; } }
A return value of 0 means no cookies were found. The cookies are stored in a buffer array after being split on the ";". The next thing we need to do is extract the name value pairs of each of the cookies and return the cookie we are looking for.
foreach my $i (@buffer) { #print $i; (my $Name, my $Value) = split(/=/,$i); if ($CookieId eq $Name) { my @buffer2 = split(/,/, $Value); foreach my $y (@buffer2) { (my $CVar, my $CVal) = split(/:/, $y); $CookieVars{$CVar} = $CVal; #print "$CVar = $CVal"; } $CookieVars{Status} = 1; return %CookieVars; } }
After storing the cookies in the buffer we step through the buffer and split the cookie on the equal sign storing the name and the value. Then using an if statement we test to see if the name is the same as the cookie ID. When we locate the cookie we want we store its name:value pairs in a hash and and return said hash. It doesn't matter whether the cookie had multiple values or not it still returns a hash. Lastly if we couldn't find the cookie we were looking for we set the status field to 0 in the hash and return the hash. When we retrieve a cookie we can check this status field for a value of 0 to see if the cookie existed. A value of 1 means the cookie did exist. Here is the complete method:
sub get_cookie { my $CookieId = shift; my %CookieVars; if (exists $ENV{'HTTP_COOKIE'}) { my @buffer = split(/;/,$ENV{'HTTP_COOKIE'}); foreach my $i (@buffer) { #print $i; (my $Name, my $Value) = split(/=/,$i); if ($CookieId eq $Name) { my @buffer2 = split(/,/, $Value); foreach my $y (@buffer2) { (my $CVar, my $CVal) = split(/:/, $y); $CookieVars{$CVar} = $CVal; #print "$CVar = $CVal"; } $CookieVars{Status} = 1; return %CookieVars; } } } else { $CookieVars{Status} = 0; return %CookieVars; } }
Retrieving a list of cookies is actually much easier to do. Here is the code for the method, I'll leave it as an exercise for the reader to interpret it.
sub get_cookie_list { my @buffer = split(/;/,$ENV{'HTTP_COOKIE'}); my %cookies; foreach my $i (@buffer) { (my $Name, my $Value) = split(/=/,$i); my @CookieValues = split(/,/, $Value); my %CookieVars; foreach my $j (@CookieValues) { (my $CookieVariable, my $CookieValue) = split(/:/, $j); $CookieVars{$CookieVariable} = $CookieValue; } $cookies{$Name} = %CookieVars; } return %cookies; }
Using the module is also an easy matter. Simply create a new header object using the constructor method. Add your cookies using the appropriate object methods and output the header before printing any thing else on your page. A simple script using it is shown below.
use pk_cgi; my $header = pk_cgi->new_header; $new_cookie = {name => "name", value => "some value"}; $header->add_cookie($new_cookie); print $header->get_header; %cookie = pk_cgi::get_cookie("name"); print "< ?xml version='1.0' encoding='UTF-8'?> < !DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'> "; print " ";
In a later article I will address retrieving get and post variables from forms. Additional Reading: * http://www.perldoc.com/ * http://www.cgi101.com/class/ * http://www.ltsw.se/knbase/internet/mime.htp You may also be interested in these tutorials and articles = "Mod_Perl 2.0 how-to's"