File Coverage

blib/lib/CAM/Session.pm
Criterion Covered Total %
statement 31 171 18.1
branch 6 48 12.5
condition 4 24 16.6
subroutine 8 30 26.6
pod 18 18 100.0
total 67 291 23.0


line stmt bran cond sub pod time code
1             package CAM::Session;
2              
3             =head1 NAME
4              
5             CAM::Session - DBI and cookie CGI session state maintenance
6              
7             =head1 LICENSE
8              
9             Copyright 2005 Clotho Advanced Media, Inc.,
10              
11             This library is free software; you can redistribute it and/or modify it
12             under the same terms as Perl itself.
13              
14             =head1 COMPARISON
15              
16             CGI::Session is a better module than this one, but this one is a
17             little easier to use. If you are starting from scratch, use
18             CGI::Session. If you are using CAM::App, then we recommend this
19             module for session management since CAM::App takes care of all of the
20             details for you.
21              
22             See README for more detail.
23              
24             =head1 SYNOPSIS
25              
26             use CAM::Session;
27             use DBI;
28             my $dbh = DBI->connect(...);
29             CAM::Session->setDBH($dbh);
30            
31             my $session = new CAM::Session();
32             $session->printCookie();
33            
34             $session->set("username", $username);
35             ...
36             $session->get("username", $username);
37             $session->delete("username");
38              
39             To periodically clean up the session table, run a script like the
40             following as a daily scheduled task:
41              
42             use CAM::Session;
43             use DBI;
44             my $dbh = DBI->connect(...) || die "no dbh";
45             CAM::Session->setDBH($dbh);
46             CAM::Session->setExpiration(24*60*60); # older than one day
47             CAM::Session->clean();
48              
49             =head1 DESCRIPTION
50              
51             CAM::Session interacts with the CGI program, the database and the
52             visitor's cookie to create a storage space for persistent data.
53              
54             =cut
55              
56             #----------------
57              
58             require 5.005_62;
59 1     1   22915 use strict;
  1         2  
  1         32  
60 1     1   46 use warnings;
  1         2  
  1         30  
61 1     1   5 use Carp;
  1         2  
  1         80  
62 1     1   835 use CGI::Cookie;
  1         11396  
  1         31  
63 1     1   2022 use CGI;
  1         9458  
  1         7  
64 1     1   2361 use DBI;
  1         26033  
  1         2304  
65              
66             our @ISA = qw();
67             our $VERSION = '1.03';
68              
69             # global settings, can be overridden for the whole class or for
70             # individual instances.
71             our $global_expiration = 24*60*60; # one day, in seconds
72             our $global_dbh = undef;
73             our $global_dbTablename = "session";
74             our $global_cookieName = "session";
75             our $global_keylength = 16;
76              
77             our $colname_key = "session_key";
78             our $colname_time = "session_time";
79             our $colname_data = "session_data";
80              
81             #----------------
82              
83             =head1 FUNCTIONS
84              
85             =over 4
86              
87             =cut
88              
89             #----------------
90              
91             =item new
92              
93             =item new DBIHANDLE
94              
95             Create a new session object, retrieving the session ID from the
96             cookie, if any. If the database handle is not set here, it must have
97             been set previously via the setDBH() class method.
98              
99             =cut
100              
101             sub new
102             {
103 3     3 1 42 my $pkg = shift;
104 3         4 my $dbh = shift; # optional
105              
106 3   66     30 my $self = bless({
107             data => {},
108             expiration => $global_expiration,
109             dbTablename => $global_dbTablename,
110             cookieName => $global_cookieName,
111             dbh => $dbh || $global_dbh,
112             needsSave => 0,
113             }, $pkg);
114              
115 3 100       14 if (!$self->{dbh})
116             {
117 1         192 &carp("No database connection has been specified. Please use ".$pkg."::setDBH()");
118 1         9 return undef;
119             }
120 2 50 66     12 if (!ref($self->{dbh}) || ref($self->{dbh}) !~ /^(DBI|DBD)\b/)
121             {
122 2 100       160 my $type = ref($self->{dbh}) ? ref($self->{dbh}) : "scalar";
123 2         228 &carp("The DBH object is not a valid DBI/DBD connection: $type");
124 2         42 return undef;
125             }
126              
127 0         0 my %cookies = CGI::Cookie->fetch();
128 0 0       0 if (exists $cookies{$self->{cookieName}})
129             {
130             # existing session
131 0         0 $self->{id} = $cookies{$self->{cookieName}}->value;
132 0 0       0 if (!$self->loadSessionData())
133             {
134 0         0 $self->_newSession();
135             }
136             }
137             else
138             {
139 0         0 $self->_newSession();
140             }
141              
142 0         0 return $self;
143             }
144             #----------------
145              
146             =item DESTROY
147              
148             Saves the session data on object destruction, if needed.
149              
150             =cut
151              
152             sub DESTROY
153             {
154 3     3   6 my $self = shift;
155 3 50       11 if ($self->{needsSave})
156             {
157 0         0 $self->saveSessionData();
158             }
159 3         17 return $self;
160             }
161             #----------------
162              
163             =item getID
164              
165             =cut
166              
167             sub getID
168             {
169 0     0 1   my $self = shift;
170 0           return $self->{id};
171             }
172             #----------------
173              
174             =item getCookie
175              
176             Return a cookie that indicates this session. Any arguments are passed
177             to CGI::Cookie::new(). Use this, for example, with
178              
179             print CGI->header(-cookie => $session->getCookie);
180              
181             =cut
182              
183             sub getCookie
184             {
185 0     0 1   my $self = shift;
186              
187 0           my $id = $self->getID();
188 0           my $cookie = CGI::Cookie->new(-name => $self->{cookieName},
189             -value => $id,
190             -path => "/",
191             @_);
192 0           return $cookie;
193             }
194             #----------------
195              
196             =item printCookie
197              
198             Outputs a cookie that indicates this session. Use this just before
199             "print CGI->header()", for example.
200              
201             =cut
202              
203             sub printCookie
204             {
205 0     0 1   my $self = shift;
206              
207 0           my $cookie = $self->getCookie(@_);
208 0           print "Set-Cookie: $cookie\n";
209             }
210             #----------------
211              
212             =item getAll
213              
214             Retrieve a hash of all of the session data.
215              
216             =cut
217              
218             sub getAll
219             {
220 0     0 1   my $self = shift;
221              
222 0 0         if (wantarray)
223             {
224 0           return (%{$self->{data}});
  0            
225             }
226             else
227             {
228 0           return (scalar keys %{$self->{data}});
  0            
229             }
230             }
231             #----------------
232              
233             =item get FIELDNAME
234              
235             Retrieve a field from the session storage.
236              
237             =cut
238              
239             sub get
240             {
241 0     0 1   my $self = shift;
242 0           my $fieldName = shift;
243              
244 0 0         return undef if (!defined $fieldName);
245 0           return $self->{data}->{$fieldName};
246             }
247             #----------------
248              
249             =item set FIELDNAME, VALUE, FIELDNAME, VALUE, ...
250              
251             Record a field in the session storage. If autoSave is on (it is
252             by default) this value is immediately recorded in the database.
253              
254             =cut
255              
256             sub set
257             {
258 0     0 1   my $self = shift;
259              
260 0           while (@_ > 0)
261             {
262 0           my $fieldName = shift;
263 0           my $value = shift;
264              
265 0 0         return undef if (!defined $fieldName);
266              
267 0           $self->{data}->{$fieldName} = $value;
268             }
269 0           $self->{needsSave} = 1;
270 0           return $self;
271             }
272             #----------------
273              
274             =item delete FIELDNAME, FIELDNAME, ...
275              
276             Remove one or more fields from the session storage. If autoSave is on
277             (it is by default) this change is immediately recorded in the
278             database.
279              
280             =cut
281              
282             sub delete
283             {
284 0     0 1   my $self = shift;
285              
286 0           foreach my $fieldName (@_)
287             {
288 0           delete $self->{data}->{$fieldName};
289             }
290 0           $self->{needsSave} = 1;
291 0           return $self;
292             }
293             #----------------
294              
295             =item clear
296              
297             Calls delete() on every field in the session storage.
298              
299             =cut
300              
301             sub clear
302             {
303 0     0 1   my $self = shift;
304 0           return $self->delete(keys %{$self->{data}});
  0            
305             }
306             #----------------
307              
308             =item loadSessionData
309              
310             Retrieve the session data from storage. This function is called by
311             new() so it is only needed if you need to reload the data for some
312             reason.
313              
314             Returns a boolean indicating the success or failure of the load
315             operation.
316              
317             =cut
318              
319             sub loadSessionData
320             {
321 0     0 1   my $self = shift;
322              
323 0           my $id = $self->getID();
324 0 0         return undef if (!$id);
325 0           my $dbrow = $self->_getSession($id);
326 0 0         return undef if (!$dbrow);
327            
328 0           $self->{data} = $self->_explode($dbrow->{$colname_data});
329 0 0         if (!$self->{data})
330             {
331 0           $self->{data} = {};
332 0           return undef;
333             }
334 0           $self->{needsSave} = 0;
335 0           return $self;
336             }
337             #----------------
338              
339             =item saveSessionData
340              
341             Write the session data to permanent storage. This function is called
342             by the set() method. so it is only needed if you have turned off the
343             autoSave feature.
344              
345             Returns a boolean indicating the success or failure of the save
346             operation.
347              
348             =cut
349              
350             sub saveSessionData
351             {
352 0     0 1   my $self = shift;
353              
354 0           my $id = $self->getID();
355 0 0         return undef if (!$id);
356 0           my $data = $self->_implode($self->{data});
357 0 0         $data = "" if (!defined $data);
358 0           my $dbh = $self->{dbh};
359 0           my $result = $dbh->do("update $$self{dbTablename} set " .
360             "$colname_data=" . $dbh->quote($data) . "," .
361             "$colname_time=now() " .
362             "where $colname_key='$id'");
363              
364 0 0 0       return undef if ((!$result) || $result == 0);
365 0           return $self;
366             }
367             #----------------
368              
369             =item isNewSession
370              
371             Returns true if this session was newly created (as opposed to a repeat
372             visitor)
373              
374             =cut
375              
376             sub isNewSession
377             {
378 0     0 1   my $self = shift;
379 0           return $self->{newsession};
380             }
381             #----------------
382              
383             # PRIVATE FUNCTION
384             sub _newSession
385             {
386 0     0     my $self = shift;
387              
388 0           $self->{id} = undef;
389              
390 0           my $dbh = $self->{dbh};
391 0           my $tries = 0;
392             # Loop until we get an unused ID, but give up if it takes too long
393 0           while ($tries++ < 20)
394             {
395 0           my $id = $self->_newID();
396 0           my $sth = $dbh->prepare("select count(*) from $$self{dbTablename} " .
397             "where $colname_key=?");
398 0           $sth->execute($id);
399 0           my ($matches) = $sth->fetchrow_array();
400 0           $sth->finish();
401              
402 0 0         if ($matches == 0)
403             {
404 0           $dbh->do("insert into $$self{dbTablename} set " .
405             "$colname_key='$id',$colname_time=now()");
406 0           $self->{id} = $id;
407 0           $self->{newsession} = 1;
408 0           last;
409             }
410             }
411 0           return $self;
412             }
413              
414             # PRIVATE FUNCTION
415             sub _getSession
416             {
417 0     0     my $self = shift;
418 0           my $id = shift;
419              
420 0 0         return undef if (!$id);
421              
422 0           my $dbh = $self->{dbh};
423 0 0         my $sth = $dbh->prepare("select *" .
424             (defined $self->{expiration} ?
425             ",date_add(now(), interval -$$self{expiration} second) as expires "
426             : "") .
427             "from $$self{dbTablename} " .
428             "where $colname_key=?");
429 0           $sth->execute($id);
430 0           my $row = $sth->fetchrow_hashref();
431 0           $sth->finish();
432              
433 0 0         return undef if (!$row);
434              
435 0 0         if (defined $self->{expiration})
436             {
437 0           $row->{$colname_time} =~ s/\D//g;
438 0           $row->{expires} =~ s/\D//g;
439            
440 0 0         if ($row->{$colname_time} lt $row->{expires})
441             {
442 0           $dbh->do("delete from $$self{dbTablename} " .
443             "where $colname_key=" . $dbh->quote($self->{cachekey}));
444 0           return undef;
445             }
446             }
447              
448 0           return $row;
449             }
450             #----------------
451              
452             =item setDBH DBI_HANDLE
453              
454             Set the global database handle for this package. Use like this:
455              
456             CAM::Session->setDBH($dbh);
457              
458             =cut
459              
460             sub setDBH
461             {
462 0     0 1   my $pkg = shift; # unused
463 0           my $val = shift;
464 0           $global_dbh = $val;
465             }
466             #----------------
467              
468             =item setExpiration SECONDS
469              
470             Set the duration for the session content. If the session is older
471             than the specified time, a new session will be created. The default
472             expiration is unlimited (set solely by the visitor's cookie
473             expiration). This is a class method
474              
475             Use like this:
476              
477             CAM::Session->setExpiration($seconds);
478              
479             =cut
480              
481             sub setExpiration
482             {
483 0     0 1   my $pkg = shift; # unused
484 0           my $val = shift;
485 0           $global_expiration = $val;
486             }
487             #----------------
488              
489             =item setTableName NAME
490              
491             Set the name of the database table that is used for the session
492             storage. This is a class method.
493              
494             Use like this:
495              
496             CAM::Session->setTableName($name);
497              
498             =cut
499              
500             sub setTableName
501             {
502 0     0 1   my $pkg = shift; # unused
503 0           my $val = shift;
504 0           $global_dbTablename = $val;
505             }
506             #----------------
507              
508             =item setCookieName NAME
509              
510             Set the name of the cookie that is used for the recording the session.
511             This is a class method.
512              
513             Use like this:
514              
515             CAM::Session->setCookieName($name);
516              
517             =cut
518              
519             sub setCookieName
520             {
521 0     0 1   my $pkg = shift; # unused
522 0           my $val = shift;
523 0           $global_cookieName = $val;
524             }
525             #----------------
526              
527              
528             # PRIVATE FUNCTION
529             sub _implode
530             {
531 0     0     my $self = shift;
532 0           my $H_data = shift;
533              
534             # Treat the hash like an array. The keys and values are treated
535             # identically.
536 0           my @escaped = (%$H_data);
537 0           foreach (@escaped)
538             {
539 0 0         $_ = "" if (!defined $_);
540 0           $_ = CGI::escape($_);
541             }
542 0           return join(",", @escaped);
543             }
544              
545             # PRIVATE FUNCTION
546             sub _explode
547             {
548 0     0     my $self = shift;
549 0           my $implosion = shift;
550              
551 0 0         $implosion = "" if (!defined $implosion);
552              
553             # The split limit of -1 prevents trailing blank fields from being omitted
554 0           my @fields = split /,/, $implosion, -1;
555 0 0         if (@fields %2 != 0)
556             {
557 0           &carp("not an even number of fields in imploded data");
558 0           return undef;
559             }
560 0           foreach (@fields)
561             {
562 0           $_ = CGI::unescape($_);
563             }
564 0           return {@fields};
565             }
566              
567             # PRIVATE FUNCTION
568             sub _newID
569             {
570 0     0     my $self = shift;
571              
572 0           require Digest::MD5;
573             # Copied from CGI::Session::ID::MD5
574 0           my $md5 = Digest::MD5->new();
575 0           $md5->add($$ , time() , rand(9999) );
576 0           return substr($md5->hexdigest(), 0, $global_keylength);
577             }
578             #----------------
579              
580             =item setup
581              
582             =item setup DBIHANDLE, TABLENAME
583              
584             Create a database table for storing sessions. This is not
585             intended to be called often, if ever. This is a class method.
586              
587             =cut
588              
589             sub setup
590             {
591 0     0 1   my $pkg = shift; # unused
592 0   0       my $dbh = shift || $global_dbh;
593 0   0       my $tablename = shift || $global_dbTablename;
594              
595 0           $dbh->do("create table if not exists $tablename (" .
596             "$colname_key char($global_keylength) primary key not null," .
597             "$colname_time timestamp," .
598             "$colname_data mediumtext)");
599             }
600             #----------------
601              
602             =item clean
603              
604             =item clean DBIHANDLE, TABLENAME, SECONDS
605              
606             Cleans out all records older than the specified number of seconds.
607             This is a class method.
608              
609             =cut
610              
611             sub clean
612             {
613 0     0 1   my $pkg = shift; # unused
614 0   0       my $dbh = shift || $global_dbh;
615 0   0       my $tablename = shift || $global_dbTablename;
616 0   0       my $seconds = shift || $global_expiration;
617              
618 0           return $dbh->do("delete from $tablename " .
619             "where $colname_time < " .
620             "date_add(now(),interval -$seconds second)");
621             }
622              
623              
624             1;
625             __END__