home *** CD-ROM | disk | FTP | other *** search
/ PC Professionell 2004 December / PCpro_2004_12.ISO / files / webserver / xampp / xampp-perl-addon-1.4.9-installer.exe / CookBook.pm < prev    next >
Encoding:
Perl POD Document  |  2003-03-14  |  22.2 KB  |  651 lines

  1. # $Id: CookBook.pm,v 3.1.4.1 2003/03/14 13:17:38 sherzodr Exp $
  2.  
  3. package CGI::Session::CookBook;
  4.  
  5. use vars ('$VERSION');
  6.  
  7. ($VERSION) = '$Revision: 3.1.4.1 $' =~ m/Revision:\s*(\S+)/;
  8.  
  9. 1;
  10.  
  11. __END__;
  12.  
  13. =pod
  14.  
  15. =head1 NAME
  16.  
  17. CookBook - tutorial on session management in cgi applications
  18.  
  19. =head1 NOTE
  20.  
  21. This document is under construction. 
  22.  
  23. =head1 DESCRIPTION
  24.  
  25. C<CGI::Session::CookBook> is a tutorial that accompanies B<CGI::Session> 
  26. distribution. It shows the usage of the library in web applications and 
  27. demonstrates practical solutions for certain problems. We do not recommend you 
  28. to read this tutorial unless you're familiar with L<CGI::Session|CGI::Session> 
  29. and it's syntax.
  30.  
  31. =head1 CONVENTIONS
  32.  
  33. To avoid unnecessary redundancy, in all the examples that follow we assume
  34. the following session and cgi objects:
  35.  
  36.     use CGI::Session;
  37.     use CGI;
  38.  
  39.     my $cgi = new CGI;    
  40.     my $session = new CGI::Session(undef, $cgi, {Directory=>'/tmp'});    
  41.  
  42. Although we are using default B<DSN> in our examples, you feel free to 
  43. use any configuration you please.
  44.  
  45. After initializing the session, we should "mark" the user with that ID.
  46. We use HTTP Cookies to do it:
  47.  
  48.     $cookie = $cgi->cookie(CGISESSID => $session->id );
  49.     print $cgi->header(-cookie=>$cookie);
  50.  
  51. The first line is creating a cookie using B<CGI.pm>'s C<cookie()> 
  52. method. The second line is sending the cookie to the user's browser 
  53. using B<CGI.pm>'s C<header()> method.
  54.  
  55. After the above confessions, we can move to some examples with a less 
  56. guilty conscious.
  57.  
  58. =head1 STORING THE USER'S NAME
  59.  
  60. =head2 PROBLEM
  61.  
  62. We have a form in our site that asks for user's name and email address. 
  63. We want to store the data so that we can greet the user when he/she 
  64. visits the site next time ( possibly after several days or even weeks ).
  65.  
  66. =head2 SOLUTION
  67.  
  68. Although quite simple and straight forward it seems, variations of this 
  69. example are used in more robust session managing tricks.
  70.  
  71. Assuming the name of the form input fields are called "first_name" and 
  72. "email" respectively, we can first retrieve this information from the 
  73. cgi parameter. Using B<CGI.pm> this can be achieved in the following 
  74. way:
  75.  
  76.     $first_name = $cgi->param("first_name");
  77.     $email  = $cgi->param("email");
  78.  
  79. After having the above two values from the form handy, we can now save 
  80. them in the session like:
  81.  
  82.     $session->param(first_name, $first_name);
  83.     $session->param(email, $email);
  84.  
  85. If the above 4-line solution seems long for you (it does to me), you can 
  86. achieve it with a single line of code:
  87.  
  88.     $session->save_param($cgi, ["first_name", "email"]);
  89.  
  90. The above syntax will get "first_name" and "email" parameters from the 
  91. B<CGI.pm> and saves them to the B<CGI::Session> object.Now some other 
  92. time or even in some other place we can simply say
  93.  
  94.     $name = $session->param("first_name");
  95.     print "$name, I know it's you. Confess!";
  96.  
  97. and it does surprise him ( if not scare :) )
  98.  
  99. =head1 REMEMBER THE REFERER
  100.  
  101. =head2 PROBLEM
  102.  
  103. You run an outrourcing service, and people get refered to your program 
  104. from other sites. After finishing the process, which might take several 
  105. click-throughs, you need to provide them with a link which takes them to 
  106. a site where they came from. In other words, after 10 clicks through 
  107. your pages you need to recall the referered link, which takes the user 
  108. to your site.
  109.  
  110. =head2 SOLUTION
  111.  
  112. This solution is similar to the previous one, but instead of getting the 
  113. data from the submitted form, you get it from HTTP_REFERER environmental 
  114. variable, which holds the link to the refered page. But you should be 
  115. cautious, because the click on your own page to the same application 
  116. generates a referal as well, in this case with your own link. So you 
  117. need to watchout for that by saving the link only if it doesn't already 
  118. exist. This approach is suitable for the application which ALWAYS get 
  119. accessed by clicking links and posting forms, but NOT by typing in the 
  120. url. Good examples would be voting polls, shopping carts among many 
  121. others.
  122.  
  123.     $ENV{HTTP_REFERER} or die "Illegal use";
  124.  
  125.     unless ( $session->param("referer") ) {
  126.         $session->param("referer", $ENV{HTTP_REFERER});
  127.     }
  128.  
  129. In the above code, we simply save the referer in the session under the 
  130. "referer" parameter. Note, that we first check if it was previously 
  131. saved, in which case there would be no need to override it. It also 
  132. means, if the referer was not saved previously, it's most likely the 
  133. first visit to the page, and the HTTP_REFERER holds the link to the link 
  134. we're interested in, not our own.
  135.  
  136. When we need to present the link back to the refered site, we just do:
  137.  
  138.     $href = $session->param("referer");
  139.     print qq~<a href="$href">go back</a>~;
  140.  
  141. =head1 BROWSING HISTORY
  142.  
  143. =head2 PROBLEM
  144.  
  145. You have an online store with about a dozen categories and thousands of 
  146. items in each category. When a visitor is surfing the site, you want to 
  147. display the last 10-20 visited pages/items on the left menu of the site 
  148. ( for examples of this refer to Amazon.com ). This will make the site 
  149. more usable and a lot friendlier
  150.  
  151. =head2 SOLUTION
  152.  
  153. The solution might vary on the way you implement the application. Here 
  154. we'll show an example of the user's browsing history, where it shows 
  155. just visited links and the pages' titles. For obvious reasons we build 
  156. the array of the link=>title relationship. If you have a dynamicly 
  157. generated content, you might have a slicker way of doing it. Despite the 
  158. fact your implementation might be different, this example shows how to 
  159. store a complex data structure in the session parameter. It's a blast!
  160.  
  161.     %pages = (
  162.         "Home"      => "http://www.ultracgis.com",
  163.         "About us"  => "http://www.ultracgis.com/about",
  164.         "Contact"   => "http://www.ultracgis.com/contact",
  165.         "Products"  => "http://www.ultracgis.com/products",
  166.         "Services"  => "http://www.ultracgis.com/services",
  167.         "Portfolio" => "http://www.ultracgis.com/pfolio",
  168.         # ...
  169.     );
  170.  
  171.     # Get a url of the page loaded
  172.     $link = $ENV{REQUEST_URI} or die "Errr. What the hack?!";
  173.  
  174.     # get the previously saved arrayref from the session parameter
  175.     # named "HISTORY"
  176.     $history = $session->param("HISTORY") || [];
  177.  
  178.     # push()ing a hashref to the arrayref
  179.     push (@{$history}, {title => $pages{ $link  },
  180.                         link  => $link          });
  181.  
  182.     # storing the modified history back in the session
  183.     $session->param( "HISTORY", $history );
  184.  
  185.  
  186. What we want you to notice is the $history, which is a reference to an 
  187. array, elements of which consist of references to anonymous hashes. This 
  188. example illustrates that one can safely store complex data structures, 
  189. including objects, in the session and they can be re-created for you the 
  190. way they were once stored.
  191.  
  192. Displaying the browsing history should be even more straight-forward:
  193.  
  194.     # we first get the history information from the session
  195.     $history = $session->param("HISTORY") || [];
  196.  
  197.     print qq~<div>Your recently viewed pages</div>~;
  198.  
  199.     for $page ( @{ $history } ) {
  200.         print qq~<a href="$page->{link}">$page->{title}</a><br>~;
  201.     }
  202.  
  203. If you use B<HTML::Template>, to access the above history in your 
  204. templates simply C<associate> the $session object with that of 
  205. B<HTML::Template>:
  206.  
  207.     $template = new HTML::Template(filename=>"some.tmpl", 
  208. associate=>$session );
  209.  
  210. Now in your "some.tmpl" template you can access the above history like 
  211. so:
  212.  
  213.     <!-- left menu starts -->
  214.     <table width="170">
  215.         <tr>
  216.             <th> last visited pages </th>
  217.         </tr>
  218.         <TMPL_LOOP NAME=HISTORY>
  219.         <tr>
  220.             <td>
  221.             <a href="<TMPL_VAR NAME=LINK>"> <TMPL_VAR NAME=TITLE> </a>
  222.             </td>
  223.         </tr>
  224.         </TMPL_LOOP>
  225.     </table>
  226.     <!-- left menu ends -->
  227.  
  228. and this will print the list in nicely formated table. For more 
  229. information on associating an object with the B<HTML::Template> refer to 
  230. L<HTML::Template manual|HTML::Template>
  231.  
  232. =head1 SHOPPING CART
  233.  
  234. =head2 PROBLEM
  235.  
  236. You have a site that lists the available products off the database. You 
  237. need an application that would enable users' to "collect" items for 
  238. checkout, in other words, to put into a virtual shopping cart. When they 
  239. are done, they can proceed to checkout.
  240.  
  241. =head2 SOLUTION
  242.  
  243. Again, the exact implementation of the site will depend on the 
  244. implementation of this solution. This example is pretty much similar to 
  245. the way we implemented the browing history in the previous example. But 
  246. instead of saving the links of the pages, we simply save the ProductID 
  247. as the arrayref in the session parameter called, say, "CART". In the 
  248. folloiwng example we tried to represent the imaginary database in the 
  249. form of a hash.
  250.  
  251. Each item in the listing will have a url to the shopping cart. The url 
  252. will be in the following format:
  253.  
  254.     http://ultracgis.com/cart.cgi?cmd=add;itemID=1001
  255.  
  256. C<cmd> CGI parameter is a run mode for the application, in this 
  257. particular example it's "add", which tells the application that an item 
  258. is about to be added. C<itemID> tells the application which item should 
  259. be added. You might as well go with the item title, instead of numbers, 
  260. but most of the time in dynamicly generated sites you prefer itemIDs 
  261. over their titles, since titles tend to be not consistent (it's from 
  262. experience):
  263.  
  264.     # Imaginary database in the form of a hash
  265.     %products = (
  266.         1001 =>    [ "usr/bin/perl t-shirt",    14.99],
  267.         1002 =>    [ "just perl t-shirt",       14.99],
  268.         1003 =>    [ "shebang hat",             15.99],
  269.         1004 =>    [ "linux mug",               19.99],
  270.         # on and on it goes....
  271.     );
  272.  
  273.     # getting the run mode for the state. If doesn't exist,
  274.     # defaults to "display", which shows the cart's content
  275.     $cmd = $cgi->param("cmd") || "display";
  276.  
  277.     if ( $cmd eq "display" ) {
  278.         print display_cart($cgi, $session);
  279.  
  280.     } elsif ( $cmd eq "add" ) {
  281.         print add_item($cgi, $session, \%products,);
  282.  
  283.     } elsif ( $cmd eq "remove") {
  284.         print remove_item($cgi, $session);
  285.  
  286.     } elsif ( $cmd eq "clear" ) {
  287.         print clear_cart($cgi, $session);
  288.  
  289.     } else {
  290.         print display_cart($cgi, $session);
  291.  
  292.     }
  293.  
  294.  
  295. The above is the skeleton of the application. Now we start writing the 
  296. functions (subroutines) associated with each run-mode. We'll start with 
  297. C<add_item()>:
  298.  
  299.     sub add_item {
  300.         my ($cgi, $session, $products) = @_;
  301.  
  302.         # getting the itemID to be put into the cart
  303.         my $itemID = $cgi->param("itemID") or die "No item specified";
  304.  
  305.         # getting the current cart's contents:
  306.         my $cart = $session->param("CART") || [];
  307.  
  308.         # adding the selected item
  309.         push @{ $cart }, {
  310.             itemID => $itemID,
  311.             name   => $products->{$itemID}->[0],
  312.             price  => $products->{$itemID}->[1],
  313.         };
  314.  
  315.         # now store the updated cart back into the session
  316.         $session->param( "CART", $cart );
  317.  
  318.         # show the contents of the cart
  319.         return display_cart($cgi, $session);
  320.     }
  321.  
  322.  
  323. As you see, things are quite straight-forward this time as well. We're 
  324. accepting three arguments, getting the itemID from the C<itemID> CGI 
  325. parameter, retrieving contents of the current cart from the "CART" 
  326. session parameter, updating the contents with the information we know 
  327. about the item with the C<itemID>, and storing the modifed $cart back to 
  328. "CART" session parameter. When done, we simply display the cart. If 
  329. anything doesn't make sence to you, STOP! Read it over!
  330.  
  331. Here are the contents for C<display_cart()>, which simply gets the 
  332. shoping cart's contents from the session parameter and generates a list:
  333.  
  334.     sub display_cart {
  335.         my ($cgi, $session) = @_;
  336.  
  337.         # getting the cart's contents
  338.         my $cart = $session->param("CART") || [];
  339.         my $total_price = 0;
  340.         my $RV = q~<table><tr><th>Title</th><th>Price</th></tr>~;
  341.  
  342.         if ( $cart ) {
  343.             for my $product ( @{$cart} ) {
  344.                 $total_price += $product->{price};
  345.                 $RV = qq~
  346.                     <tr>
  347.                         <td>$product->{name}</td>
  348.                         <td>$product->{price}</td>
  349.                     </tr>~;
  350.             }
  351.  
  352.         } else {
  353.             $RV = qq~
  354.                 <tr>
  355.                     <td colspan="2">There are no items in your cart 
  356. yet</td>
  357.                 </tr>~;
  358.         }
  359.  
  360.         $RV = qq~
  361.             <tr>
  362.                 <td><b>Total Price:</b></td>
  363.                 <td><b>$total_price></b></td>
  364.             </tr></table>~;
  365.  
  366.         return $RV;
  367.     }
  368.  
  369.  
  370. A more professional approach would be to take the HTML outside the 
  371. program code by using B<HTML::Template>, in which case the above 
  372. C<display_cart()> will look like:
  373.  
  374.     sub display_cart {
  375.         my ($cgi, $session) = @_;
  376.  
  377.         my $template = new HTML::Template(filename=>"cart.tmpl",
  378.                                           associate=>$session,
  379.                                           die_on_bad_params=>0);
  380.         return $template->output();
  381.  
  382.     }
  383.  
  384. And respective portion of the html template would be something like:
  385.  
  386.     <!-- shopping cart starts -->
  387.     <table>
  388.         <tr>
  389.             <th>Title</th><th>Price</th>
  390.         </tr>
  391.         <TMPL_LOOP NAME=CART>
  392.         <tr>
  393.             <td> <TMPL_VAR NAME=NAME> </td>
  394.             <td> <TMPL_VAR NAME=PRICE> </td>
  395.         </tr>
  396.         </TMPL_LOOP>
  397.         <tr>
  398.             <td><b>Total Price:</b></td>
  399.             <td><b> <TMPL_VAR NAME=TOTAL_PRICE> </td></td>
  400.         </tr>
  401.     </table>
  402.     <!-- shopping cart ends -->
  403.  
  404. A slight problem in the above template: TOTAL_PRICE doesn't exist. To 
  405. fix this problem we need to introduce a slight modification to our 
  406. C<add_item()>, where we also save the precalculated total price in the 
  407. "total_price" session parameter. Try it yourself.
  408.  
  409. If you've been following the examples, you shouldn't discover anything 
  410. in the above code either. Let's move to C<remove_item()>. That's what 
  411. the link for removing an item from the shopping cart will look like:
  412.  
  413.     http://ultracgis.com/cart.cgi?cmd=remove;itemID=1001
  414.  
  415.     sub remove_item {
  416.         my ($cgi, $session) = @_;
  417.  
  418.         # getting the itemID from the CGI parameter
  419.         my $itemID = $cgi->param("itemID") or return undef;
  420.  
  421.         # getting the cart data from the session
  422.         my $cart = $session->param("CART") or return undef;
  423.  
  424.         my $idx = 0;
  425.         for my $product ( @{$cart} ) {
  426.             $product->{itemID} == $itemID or next;
  427.             splice( @{$cart}, $idx++, 1);
  428.         }
  429.  
  430.         $session->param("CART", $cart);
  431.  
  432.         return display_cart($cgi, $session);
  433.     }
  434.  
  435. C<clear_cart()> will get even shorter
  436.  
  437.     sub clear_cart {
  438.         my ($cgi, $session) = @_;
  439.         $session->clear(["CART"]);
  440.     }
  441.  
  442. =head1 MEMBERS AREA
  443.  
  444. =head2 PROBLEM
  445.  
  446. You want to create an area in the part of your site/application where 
  447. only restricted users should have access to.
  448.  
  449. =head2 SOLUTION
  450.  
  451. I have encountered literally dozens of different implementations of this 
  452. by other programmers, none of them perfect. Key properties of such an 
  453. application are reliability, security and no doubt, user-friendliness. 
  454. Consider this receipt not just as a CGI::Session implementation, but 
  455. also a receipt on handling login/authentication routines transparently. 
  456. Your users will love you for it.
  457.  
  458. So first, let's build the logic, only then we'll start coding. Before 
  459. going any further, we need to agree upon a username/password fields that 
  460. we'll be using for our login form. Let's choose "lg_name" and 
  461. "lg_password" respectively. Now, in our application, we'll always be 
  462. watching out for those two fields at the very start of the program to 
  463. detect if the user submitted a login form or not. Some people tend to 
  464. setup a dedicated run-mode like "_cmd=login" which will be handled 
  465. seperately, but later you'll see why this is not a good idea.
  466.  
  467. If those two parameters are present in our CGI object, we will go ahead 
  468. and try to load the user's profile from the database and set a special 
  469. session flag "~logged-in" to a true value. If those parameters are 
  470. present, but if the login/password pairs do not match with the ones in 
  471. the database, we leave "~logged-in" untouched, but increment another 
  472. flag "~login-trials" to one. So here is an init() function (for 
  473. initializer) which should be called at the top of the program:
  474.  
  475.     sub init {
  476.         my ($session, $cgi) = @_; # receive two args
  477.  
  478.         if ( $session->param("~logged-in") ) {
  479.             return 1;  # if logged in, don't bother going further
  480.         }
  481.  
  482.         my $lg_name = $cgi->param("lg_name") or return;
  483.         my $lg_psswd=$cgi->param("lg_password") or return;
  484.  
  485.         # if we came this far, user did submit the login form
  486.         # so let's try to load his/her profile if name/psswds match
  487.         if ( my $profile = _load_profile($lg_name, $lg_psswd) ) {
  488.             $session->param("~profile", $profile);
  489.             $session->param("~logged-in", 1);
  490.             $session->clear(["~login-trials"]);
  491.             return 1;
  492.  
  493.         }
  494.  
  495.         # if we came this far, the login/psswds do not match
  496.         # the entries in the database
  497.         my $trials = $session->param("~login-trials") || 0;
  498.         return $session->param("~login-trials", ++$trials);
  499.     }
  500.  
  501.  
  502. Syntax for _load_profile() totally depends on where your user profiles 
  503. are stored. I normally store them in MySQL tables, but suppose you're 
  504. storing them in flat files in the following format:
  505.  
  506.     username    password    email
  507.  
  508. Your _load_profile() would look like:
  509.  
  510.     sub _load_profile {
  511.         my ($lg_name, $lg_psswd) = @_;
  512.  
  513.         local $/ = "\n";
  514.         unless (sysopen(PROFILE, "profiles.txt", O_RDONLY) ) {
  515.             die "Couldn't open profiles.txt: $!");
  516.         }
  517.         while ( <PROFILES> ) {
  518.             /^(\n|#)/ and next;
  519.             chomp;
  520.             my ($n, $p, $e) = split "\s+";
  521.             if ( ($n eq $lg_name) && ($p eq $lg_psswd) ) {
  522.                 my $p_mask = "x" . length($p);
  523.                 return {username=>$n, password=>$p_mask, email=>$e};
  524.  
  525.             }
  526.         }
  527.         close(PROFILE);
  528.  
  529.         return undef;
  530.     }
  531.  
  532.  
  533. Now regardless of what run mode user is in, you just call the above 
  534. C<init()> method somewhere in the beginning of your program, and if the 
  535. user is logged in properly, you're guaranteed that "~logged-in" session 
  536. flag would be set to true and the user's profile information will be 
  537. available to you all the time from the "~profile" session parameter:
  538.  
  539.     init($cgi, $session);
  540.  
  541.     if ( $session->param("~login-trials") >= 3 ) {
  542.         print error("You failed 3 times in a row.\n" .
  543.                     "Your session is blocked. Please contact us with ".
  544.                     "the details of your action");
  545.         exit(0);
  546.  
  547.     }
  548.  
  549.     unless ( $session->param("~logged-in") ) {
  550.         print login_page($cgi, $session);
  551.         exit(0);
  552.  
  553.     }
  554.  
  555. In the above example we're using exit() to stop the further processing. 
  556. If you require mod_perl compatibility, you will want some other, more 
  557. graceful way.
  558.  
  559. To access the user's profile data without accessing the database again, 
  560. you simply do:
  561.  
  562.     my $profile = $session->param("~profile");
  563.     print "Hello $profile->{username}, I know it's you. Confess!";
  564.  
  565. and the user will be terrified :-).
  566.  
  567. But here is a trick. Suppose, a user clicked on the link with the 
  568. following query_string: "profile.cgi?_cmd=edit", but he/she is not 
  569. logged in. If you're performing the above init() function, the user will 
  570. see a login_page(). What happens after they submit the form with proper 
  571. username/password? Ideally you would want the user to be taken directly 
  572. to "?_cmd=edit" page, since that's the link they clicked before being 
  573. prompted to login,  rather than some other say "?_cmd=view" page. To 
  574. deal with this very important usabilit feature, you need to include a 
  575. hiidden field in your login form similar to:
  576.  
  577.     <INPUT TYPE="hidden" NAME="_cmd" VALUE="$cmd" />
  578.  
  579. Since I prefer using HTML::Template, that's what I can find in my login 
  580. form most of the time:
  581.  
  582.     <input type="hidden" name="_cmd" value="<tmpl_var _cmd>">
  583.  
  584. The above _cmd slot will be filled in properly by just associating $cgi 
  585. object with HTML::Template.
  586.  
  587. Implementing a "sign out" functionality is even more straight forward. 
  588. Since the application is only checking for "~logged-in" session flag, we 
  589. simply clear the flag when a user click on say "?_cmd=logout" link:
  590.  
  591.     if ( $cmd eq "logout" ) {
  592.         $session->clear(["~logged-in"]);
  593.  
  594.     }
  595.  
  596. You can choose to clear() "~profile" as well, but wouldn't you want to 
  597. have an ability to greet the user with his/her username or fill out his 
  598. username in the login form next time? This might be a question of 
  599. beliefs. But we believe it's the question of usability. You may also 
  600. choose to delete() the session... agh, let's not argue what is better 
  601. and what is not. As long as you're happy, that's what counts :-). Enjoy!
  602.  
  603. =head1 SUGGESTIONS AND CORRECTIONS
  604.  
  605. We tried to put together some simple examples of CGI::Session usage. 
  606. There're litterally hundreds of different exciting tricks one can 
  607. perform with proper session management. If you have a problem, and 
  608. believe CGI::Session is a right tool but don't know how to implement it, 
  609. or, if you want to see some other examples of your choice in this Cook 
  610. Book, just drop us an email, and we'll be happy to work on them as soon 
  611. as this evil time permits us.
  612.  
  613. Send your questions, requests and corrections to CGI::Session mailing 
  614. list, Cgi-session@ultracgis.com.
  615.  
  616. =head1 AUTHOR
  617.  
  618.     Sherzod Ruzmetov <sherzodr@cpan.org>
  619.  
  620. =head1 SEE ALSO
  621.  
  622. =over 4
  623.  
  624. =item *
  625.  
  626. L<CGI::Session|CGI::Session> - CGI::Session manual
  627.  
  628. =item *
  629.  
  630. L<CGI::Session::Tutorial|CGI::Session::Tutorial> - extended CGI::Session manual
  631.  
  632. =item *
  633.  
  634. L<CGI::Session::CookBook|CGI::Session::CookBook> - practical solutions for real life problems
  635.  
  636. =item *
  637.  
  638. B<RFC 2965> - "HTTP State Management Mechanism" found at ftp://ftp.isi.edu/in-notes/rfc2965.txt
  639.  
  640. =item *
  641.  
  642. L<CGI|CGI> - standard CGI library
  643.  
  644. =item *
  645.  
  646. L<Apache::Session|Apache::Session> - another fine alternative to CGI::Session
  647.  
  648. =back
  649.  
  650. =cut
  651.