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