1# $Id: CookBook.pm,v 1.1.1.1 2003/08/02 23:39:33 takezoe Exp $ 2 3package CGI::Session::CookBook; 4 5use vars ('$VERSION'); 6 7($VERSION) = '$Revision: 1.1.1.1 $' =~ m/Revision:\s*(\S+)/; 8 91; 10 11__END__; 12 13=pod 14 15=head1 NAME 16 17CookBook - tutorial on session management in cgi applications 18 19=head1 NOTE 20 21This document is under construction. 22 23=head1 DESCRIPTION 24 25C<CGI::Session::CookBook> is a tutorial that accompanies B<CGI::Session> 26distribution. It shows the usage of the library in web applications and 27demonstrates practical solutions for certain problems. We do not recommend you 28to read this tutorial unless you're familiar with L<CGI::Session|CGI::Session> 29and it's syntax. 30 31=head1 CONVENTIONS 32 33To avoid unnecessary redundancy, in all the examples that follow we assume 34the 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 42Although we are using default B<DSN> in our examples, you feel free to 43use any configuration you please. 44 45After initializing the session, we should "mark" the user with that ID. 46We use HTTP Cookies to do it: 47 48 $cookie = $cgi->cookie(CGISESSID => $session->id ); 49 print $cgi->header(-cookie=>$cookie); 50 51The first line is creating a cookie using B<CGI.pm>'s C<cookie()> 52method. The second line is sending the cookie to the user's browser 53using B<CGI.pm>'s C<header()> method. 54 55After the above confessions, we can move to some examples with a less 56guilty conscious. 57 58=head1 STORING THE USER'S NAME 59 60=head2 PROBLEM 61 62We have a form in our site that asks for user's name and email address. 63We want to store the data so that we can greet the user when he/she 64visits the site next time ( possibly after several days or even weeks ). 65 66=head2 SOLUTION 67 68Although quite simple and straight forward it seems, variations of this 69example are used in more robust session managing tricks. 70 71Assuming the name of the form input fields are called "first_name" and 72"email" respectively, we can first retrieve this information from the 73cgi parameter. Using B<CGI.pm> this can be achieved in the following 74way: 75 76 $first_name = $cgi->param("first_name"); 77 $email = $cgi->param("email"); 78 79After having the above two values from the form handy, we can now save 80them in the session like: 81 82 $session->param(first_name, $first_name); 83 $session->param(email, $email); 84 85If the above 4-line solution seems long for you (it does to me), you can 86achieve it with a single line of code: 87 88 $session->save_param($cgi, ["first_name", "email"]); 89 90The above syntax will get "first_name" and "email" parameters from the 91B<CGI.pm> and saves them to the B<CGI::Session> object.Now some other 92time 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 97and it does surprise him ( if not scare :) ) 98 99=head1 REMEMBER THE REFERER 100 101=head2 PROBLEM 102 103You run an outrourcing service, and people get refered to your program 104from other sites. After finishing the process, which might take several 105click-throughs, you need to provide them with a link which takes them to 106a site where they came from. In other words, after 10 clicks through 107your pages you need to recall the referered link, which takes the user 108to your site. 109 110=head2 SOLUTION 111 112This solution is similar to the previous one, but instead of getting the 113data from the submitted form, you get it from HTTP_REFERER environmental 114variable, which holds the link to the refered page. But you should be 115cautious, because the click on your own page to the same application 116generates a referal as well, in this case with your own link. So you 117need to watchout for that by saving the link only if it doesn't already 118exist. This approach is suitable for the application which ALWAYS get 119accessed by clicking links and posting forms, but NOT by typing in the 120url. Good examples would be voting polls, shopping carts among many 121others. 122 123 $ENV{HTTP_REFERER} or die "Illegal use"; 124 125 unless ( $session->param("referer") ) { 126 $session->param("referer", $ENV{HTTP_REFERER}); 127 } 128 129In 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 131saved, in which case there would be no need to override it. It also 132means, if the referer was not saved previously, it's most likely the 133first visit to the page, and the HTTP_REFERER holds the link to the link 134we're interested in, not our own. 135 136When 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 145You have an online store with about a dozen categories and thousands of 146items in each category. When a visitor is surfing the site, you want to 147display 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 149more usable and a lot friendlier 150 151=head2 SOLUTION 152 153The solution might vary on the way you implement the application. Here 154we'll show an example of the user's browsing history, where it shows 155just visited links and the pages' titles. For obvious reasons we build 156the array of the link=>title relationship. If you have a dynamicly 157generated content, you might have a slicker way of doing it. Despite the 158fact your implementation might be different, this example shows how to 159store 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 186What we want you to notice is the $history, which is a reference to an 187array, elements of which consist of references to anonymous hashes. This 188example illustrates that one can safely store complex data structures, 189including objects, in the session and they can be re-created for you the 190way they were once stored. 191 192Displaying 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 203If you use B<HTML::Template>, to access the above history in your 204templates simply C<associate> the $session object with that of 205B<HTML::Template>: 206 207 $template = new HTML::Template(filename=>"some.tmpl", 208associate=>$session ); 209 210Now in your "some.tmpl" template you can access the above history like 211so: 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 228and this will print the list in nicely formated table. For more 229information on associating an object with the B<HTML::Template> refer to 230L<HTML::Template manual|HTML::Template> 231 232=head1 SHOPPING CART 233 234=head2 PROBLEM 235 236You have a site that lists the available products off the database. You 237need an application that would enable users' to "collect" items for 238checkout, in other words, to put into a virtual shopping cart. When they 239are done, they can proceed to checkout. 240 241=head2 SOLUTION 242 243Again, the exact implementation of the site will depend on the 244implementation of this solution. This example is pretty much similar to 245the way we implemented the browing history in the previous example. But 246instead of saving the links of the pages, we simply save the ProductID 247as the arrayref in the session parameter called, say, "CART". In the 248folloiwng example we tried to represent the imaginary database in the 249form of a hash. 250 251Each item in the listing will have a url to the shopping cart. The url 252will be in the following format: 253 254 http://ultracgis.com/cart.cgi?cmd=add;itemID=1001 255 256C<cmd> CGI parameter is a run mode for the application, in this 257particular example it's "add", which tells the application that an item 258is about to be added. C<itemID> tells the application which item should 259be added. You might as well go with the item title, instead of numbers, 260but most of the time in dynamicly generated sites you prefer itemIDs 261over their titles, since titles tend to be not consistent (it's from 262experience): 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 295The above is the skeleton of the application. Now we start writing the 296functions (subroutines) associated with each run-mode. We'll start with 297C<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 323As you see, things are quite straight-forward this time as well. We're 324accepting three arguments, getting the itemID from the C<itemID> CGI 325parameter, retrieving contents of the current cart from the "CART" 326session parameter, updating the contents with the information we know 327about 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 329anything doesn't make sence to you, STOP! Read it over! 330 331Here are the contents for C<display_cart()>, which simply gets the 332shoping 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 356yet</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 370A more professional approach would be to take the HTML outside the 371program code by using B<HTML::Template>, in which case the above 372C<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 384And 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 404A slight problem in the above template: TOTAL_PRICE doesn't exist. To 405fix this problem we need to introduce a slight modification to our 406C<add_item()>, where we also save the precalculated total price in the 407"total_price" session parameter. Try it yourself. 408 409If you've been following the examples, you shouldn't discover anything 410in the above code either. Let's move to C<remove_item()>. That's what 411the 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 435C<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 446You want to create an area in the part of your site/application where 447only restricted users should have access to. 448 449=head2 SOLUTION 450 451I have encountered literally dozens of different implementations of this 452by other programmers, none of them perfect. Key properties of such an 453application are reliability, security and no doubt, user-friendliness. 454Consider this receipt not just as a CGI::Session implementation, but 455also a receipt on handling login/authentication routines transparently. 456Your users will love you for it. 457 458So first, let's build the logic, only then we'll start coding. Before 459going any further, we need to agree upon a username/password fields that 460we'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 462watching out for those two fields at the very start of the program to 463detect if the user submitted a login form or not. Some people tend to 464setup a dedicated run-mode like "_cmd=login" which will be handled 465seperately, but later you'll see why this is not a good idea. 466 467If those two parameters are present in our CGI object, we will go ahead 468and try to load the user's profile from the database and set a special 469session flag "~logged-in" to a true value. If those parameters are 470present, but if the login/password pairs do not match with the ones in 471the database, we leave "~logged-in" untouched, but increment another 472flag "~login-trials" to one. So here is an init() function (for 473initializer) 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 502Syntax for _load_profile() totally depends on where your user profiles 503are stored. I normally store them in MySQL tables, but suppose you're 504storing them in flat files in the following format: 505 506 username password email 507 508Your _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 533Now regardless of what run mode user is in, you just call the above 534C<init()> method somewhere in the beginning of your program, and if the 535user is logged in properly, you're guaranteed that "~logged-in" session 536flag would be set to true and the user's profile information will be 537available 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 555In the above example we're using exit() to stop the further processing. 556If you require mod_perl compatibility, you will want some other, more 557graceful way. 558 559To access the user's profile data without accessing the database again, 560you simply do: 561 562 my $profile = $session->param("~profile"); 563 print "Hello $profile->{username}, I know it's you. Confess!"; 564 565and the user will be terrified :-). 566 567But here is a trick. Suppose, a user clicked on the link with the 568following query_string: "profile.cgi?_cmd=edit", but he/she is not 569logged in. If you're performing the above init() function, the user will 570see a login_page(). What happens after they submit the form with proper 571username/password? Ideally you would want the user to be taken directly 572to "?_cmd=edit" page, since that's the link they clicked before being 573prompted to login, rather than some other say "?_cmd=view" page. To 574deal with this very important usabilit feature, you need to include a 575hiidden field in your login form similar to: 576 577 <INPUT TYPE="hidden" NAME="_cmd" VALUE="$cmd" /> 578 579Since I prefer using HTML::Template, that's what I can find in my login 580form most of the time: 581 582 <input type="hidden" name="_cmd" value="<tmpl_var _cmd>"> 583 584The above _cmd slot will be filled in properly by just associating $cgi 585object with HTML::Template. 586 587Implementing a "sign out" functionality is even more straight forward. 588Since the application is only checking for "~logged-in" session flag, we 589simply 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 596You can choose to clear() "~profile" as well, but wouldn't you want to 597have an ability to greet the user with his/her username or fill out his 598username in the login form next time? This might be a question of 599beliefs. But we believe it's the question of usability. You may also 600choose to delete() the session... agh, let's not argue what is better 601and what is not. As long as you're happy, that's what counts :-). Enjoy! 602 603=head1 SUGGESTIONS AND CORRECTIONS 604 605We tried to put together some simple examples of CGI::Session usage. 606There're litterally hundreds of different exciting tricks one can 607perform with proper session management. If you have a problem, and 608believe CGI::Session is a right tool but don't know how to implement it, 609or, if you want to see some other examples of your choice in this Cook 610Book, just drop us an email, and we'll be happy to work on them as soon 611as this evil time permits us. 612 613Send your questions, requests and corrections to CGI::Session mailing 614list, 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 626L<CGI::Session|CGI::Session> - CGI::Session manual 627 628=item * 629 630L<CGI::Session::Tutorial|CGI::Session::Tutorial> - extended CGI::Session manual 631 632=item * 633 634L<CGI::Session::CookBook|CGI::Session::CookBook> - practical solutions for real life problems 635 636=item * 637 638B<RFC 2965> - "HTTP State Management Mechanism" found at ftp://ftp.isi.edu/in-notes/rfc2965.txt 639 640=item * 641 642L<CGI|CGI> - standard CGI library 643 644=item * 645 646L<Apache::Session|Apache::Session> - another fine alternative to CGI::Session 647 648=back 649 650=cut 651