File Coverage

blib/lib/CGI/Scribe.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package CGI::Scribe;
2              
3             $SESSION_LENGTH = 16;
4             $CGI::Scribe::DEBUG = 0;
5              
6 1     1   690 use strict;
  1         2  
  1         41  
7 1     1   6 use vars qw( $VERSION $AUTOLOAD $SESSION_LENGTH $SRAND );
  1         2  
  1         83  
8 1     1   5 use Carp;
  1         6  
  1         119  
9 1     1   1171 use Storable qw( freeze thaw );
  1         3777  
  1         75  
10 1     1   1502 use MD5;
  0            
  0            
11             use CGI::Cookie;
12              
13             require 5.004; # Depends on srand() supplying good seed
14              
15             $VERSION = '0.03';
16             $SRAND = 0;
17              
18             sub new {
19             my $class = shift;
20             my($session, $attr) = @_;
21              
22             $class = ref $class || $class;
23             bless my $self = {}, $class;
24            
25             $self->initialize;
26              
27             if ($session) {
28             croak "invalid session id $session" unless $session =~ /^[-A-Za-z0-9]+$/;
29             $self->{session} = $session;
30             }
31              
32             @$self{ keys %$attr } = values %$attr if $attr;
33              
34             $self->_fetch_cookie if ref $self->{cookie};
35             $self->_new_session unless $self->{session};
36              
37             $self;
38             }
39              
40             sub initialize {
41             my $self = shift;
42              
43             $self->{session} = undef;
44             $self->{cookie} = undef;
45             $self->{secret} = 'eaven-hay and-ay e-thay earth-ay';
46             $self->{session_length} = $SESSION_LENGTH;
47             $self->{is_new} = 0;
48             $self->{_fetched} = 0;
49             $self->{_dirty} = 0;
50             $self->{_data} = {};
51             $self->{_cookie_data} = {};
52             $self->{debug} = $CGI::Scribe::DEBUG;
53              
54             $self->{autoload} = {
55             session_length => 1,
56             session => 1,
57             secret => 1,
58             debug => 1,
59             is_new => 1,
60             };
61             }
62              
63             sub version { $VERSION }
64              
65             sub _debug {
66             my $self = shift;
67             my($msg, $level) = @_;
68              
69             $level ||= 1;
70             $msg = "[$self->{session}] $msg" if $self->{session};
71             warn ref $self, " $msg\n" if $self->{debug} >= $level;
72             }
73              
74             sub _fetch_cookie {
75             my $self = shift;
76              
77             my($mac, $session, $frozen) = $self->{cookie}->value;
78             return undef unless $mac and $session;
79             # If the session is defined but doesn't match the cookie, then ignore cookie
80             return undef if $self->{session} and $self->{session} ne $session;
81              
82             # Check the Message Authentication Code (MAC)
83             my $mac_check = MD5->hexhash($self->{secret} .
84             MD5->hexhash(join '', $self->{secret}, $session, $frozen));
85             return undef unless $mac eq $mac_check;
86              
87             # Thaw the session data
88             $self->{session} = $session;
89             # Convert the hex data to binary
90             my $thawed = eval { thaw( pack 'H*', $frozen ) };
91             croak "error thawing session in cookie: $@" if $@ or ref $thawed ne 'HASH';
92              
93             foreach my $key (keys %$thawed) {
94             $self->{_cookie_data}{$key} = 1; # keep track of what was in cookie
95             $self->{_data}{ $key } = $thawed->{ $key };
96             }
97              
98             if ($self->{debug}) {
99             $self->_debug('fetched from cookie', 1);
100             foreach my $key (keys %$thawed) {
101             $self->_debug("cookie data: $key=$thawed->{$key}", 2);
102             }
103             }
104              
105             1;
106             }
107              
108             sub _new_session {
109             my $self = shift;
110             my $seed = shift;
111              
112             # Perl 5.004 and later automatically call srand() with a "good"
113             # seed, if it hasn't been called already. However, it seems to
114             # happen at compile-time such that child processes generate
115             # identical sequences. We've added a flag so that we make sure to
116             # call srand() on the first invocation of this method.
117             srand unless $SRAND++;
118              
119             $self->{session} = join '-',
120             substr(MD5->hexhash($self->{secret} . rand() . $seed),
121             0, $self->{session_length}), time;
122              
123             $self->_debug('generated', 1) if $self->{debug};
124              
125             $self->{_fetched} = 1;
126             $self->{_dirty} = 0;
127             $self->{is_new} = 1;
128             $self->{session};
129             }
130              
131             sub _fetch {
132             my $self = shift;
133              
134             $self->{_fetched} = 1;
135             $self->_debug('fetched from server', 1) if $self->{debug};
136             }
137              
138             sub _store {
139             my $self = shift;
140              
141             $self->{_dirty} = 0;
142             $self->_fetch unless $self->{_fetched};
143             $self->_debug('stored on server', 1) if $self->{debug};
144             }
145              
146             sub clear {
147             my $self = shift;
148              
149             $self->{_data} = {};
150             $self->{_dirty} = 1;
151             $self->{_fetched} = 1;
152             $self->_debug('cleared', 1) if $self->{debug};
153             }
154              
155             sub param {
156             my $self = shift;
157              
158             # If no arguments, return list of keys
159             unless(@_) {
160             $self->_fetch unless $self->{_fetched};
161             return keys %{ $self->{_data} };
162             }
163              
164             # If just one parameter named is supplied, return the value
165             if (@_ == 1) {
166             return $self->{_data}{ $_[0] }
167             if exists $self->{_data}{ $_[0] } or $self->{_fetched};
168             $self->_fetch;
169             return $self->{_data}{ $_[0] };
170             }
171              
172             # If more than one parameter, they want to set value(s)
173             $self->{_dirty} = 1;
174             $self->_fetch unless $self->{_fetched};
175             my %hash = @_;
176             @{ $self->{_data} }{ keys %hash } = values %hash;
177              
178             1;
179             }
180              
181             sub exists {
182             my $self = shift;
183              
184             return undef unless @_;
185             return exists $self->{_data}{ $_[0] }
186             if exists $self->{_data}{ $_[0] } or $self->{_fetched};
187             $self->_fetch;
188             exists $self->{_data}{ $_[0] };
189             }
190              
191             sub delete_param {
192             my $self = shift;
193              
194             return unless @_;
195              
196             # We must make sure we've fetched, to protect ourselves from reading
197             # the deleted value(s) later
198             $self->_fetch unless $self->{_fetched};
199             $self->{_dirty} = 1;
200              
201             foreach my $param (@_) {
202             delete $self->{_data}{$param};
203             }
204             }
205              
206             sub cookie {
207             my $self = shift;
208              
209             # If a hash reference is passed, it specifies all of the param names
210             # to place in the cookie. If a list is passed, we include those
211             # names *along with* whatever was previously stored in the cookie.
212             my %params = map { $_ => 1 }
213             (ref $_[0] eq 'ARRAY'
214             ? @{ $_[0] }
215             : keys %{ $self->{_cookie_data} }, @_);
216             my @params = grep { $self->exists( $_ ) } keys %params;
217            
218             unless ($self->{cookie}) {
219             $self->{cookie} = new CGI::Cookie( -name => 'session' );
220             }
221              
222             my $frozen = eval {
223             freeze( { map { $_ => $self->param($_) } @params } );
224             };
225             croak $@ if $@;
226             $frozen = unpack 'H*', $frozen;
227             my $mac = MD5->hexhash($self->{secret} .
228             MD5->hexhash(join '', $self->{secret}, $self->{session},
229             $frozen));
230             $self->{cookie}->value([ $mac, $self->{session}, $frozen ]);
231             $self->{cookie};
232             }
233              
234             sub DESTROY {
235             my $self = shift;
236              
237             $self->_store if $self->{_dirty};
238             }
239              
240             sub AUTOLOAD {
241             my $self = shift;
242             my $type = ref($self) || croak "autoload: $self is not an object";
243             my $name = $AUTOLOAD;
244              
245             $name =~ s/.*://;
246             return if $name eq 'DESTROY';
247             croak "unknown autoload name '$name'" unless exists $self->{autoload}{$name};
248             return (@_ ? $self->{$name} = shift : $self->{$name});
249             }
250              
251             sub TIEHASH { shift()->new( @_ ) }
252             sub STORE { shift()->param( @_ ) }
253             sub DELETE { shift()->delete_param( @_ ) }
254             sub CLEAR { shift()->clear( @_ ) }
255             sub EXISTS { shift()->exists( @_ ) }
256              
257             sub FETCH {
258             my $self = shift;
259             my $key = shift;
260            
261             return $self->{session} if $key eq '_session';
262             $self->param( $key );
263             }
264              
265             sub FIRSTKEY {
266             my $self = shift;
267              
268             $self->_fetch unless $self->{_fetched};
269             my $reset = keys %{ $self->{_data} };
270             return each %{ $self->{_data} };
271             }
272              
273             sub NEXTKEY { each %{ shift()->{_data} } }
274              
275              
276             1;
277              
278             __END__