version bump
[catagits/Catalyst-Plugin-Session.git] / lib / Catalyst / Plugin / Session.pm
1 #!/usr/bin/perl
2
3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
5
6 use strict;
7 use warnings;
8
9 use NEXT;
10 use Catalyst::Exception ();
11 use Digest              ();
12 use overload            ();
13 use Object::Signature   ();
14 use Carp;
15
16 our $VERSION = "0.19";
17
18 my @session_data_accessors; # used in delete_session
19 BEGIN {
20     __PACKAGE__->mk_accessors(
21         "_session_delete_reason",
22         @session_data_accessors = qw/
23           _sessionid
24           _session
25           _session_expires
26           _extended_session_expires
27           _session_data_sig
28           _flash
29           _flash_keep_keys
30           _flash_key_hashes
31           _tried_loading_session_id
32           _tried_loading_session_data
33           _tried_loading_session_expires
34           _tried_loading_flash_data
35           /
36     );
37 }
38
39 sub setup {
40     my $c = shift;
41
42     $c->NEXT::setup(@_);
43
44     $c->check_session_plugin_requirements;
45     $c->setup_session;
46
47     return $c;
48 }
49
50 sub check_session_plugin_requirements {
51     my $c = shift;
52
53     unless ( $c->isa("Catalyst::Plugin::Session::State")
54         && $c->isa("Catalyst::Plugin::Session::Store") )
55     {
56         my $err =
57           (     "The Session plugin requires both Session::State "
58               . "and Session::Store plugins to be used as well." );
59
60         $c->log->fatal($err);
61         Catalyst::Exception->throw($err);
62     }
63 }
64
65 sub setup_session {
66     my $c = shift;
67
68     my $cfg = ( $c->config->{session} ||= {} );
69
70     %$cfg = (
71         expires        => 7200,
72         verify_address => 0,
73         %$cfg,
74     );
75
76     $c->NEXT::setup_session();
77 }
78
79 sub prepare_action {
80     my $c = shift;
81
82     if (    $c->config->{session}{flash_to_stash}
83         and $c->sessionid
84         and my $flash_data = $c->flash )
85     {
86         @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
87     }
88
89     $c->NEXT::prepare_action(@_);
90 }
91
92 sub finalize_headers {
93     my $c = shift;
94
95     # fix cookie before we send headers
96     $c->_save_session_expires;
97
98     return $c->NEXT::finalize_headers(@_);
99 }
100
101 sub finalize {
102     my $c = shift;
103     my $ret = $c->NEXT::finalize(@_);
104
105     # then finish the rest
106     $c->finalize_session;
107     return $ret;
108 }
109
110 sub finalize_session {
111     my $c = shift;
112
113     $c->NEXT::finalize_session;
114
115     $c->_save_session_id;
116     $c->_save_session;
117     $c->_save_flash;
118
119     $c->_clear_session_instance_data;
120 }
121
122 sub _save_session_id {
123     my $c = shift;
124
125     # we already called set when allocating
126     # no need to tell the state plugins anything new
127 }
128
129 sub _save_session_expires {
130     my $c = shift;
131
132     if ( defined($c->_session_expires) ) {
133         my $expires = $c->session_expires; # force extension
134
135         my $sid = $c->sessionid;
136         $c->store_session_data( "expires:$sid" => $expires );
137     }
138 }
139
140 sub _save_session {
141     my $c = shift;
142
143     if ( my $session_data = $c->_session ) {
144
145         no warnings 'uninitialized';
146         if ( Object::Signature::signature($session_data) ne
147             $c->_session_data_sig )
148         {
149             $session_data->{__updated} = time();
150             my $sid = $c->sessionid;
151             $c->store_session_data( "session:$sid" => $session_data );
152         }
153     }
154 }
155
156 sub _save_flash {
157     my $c = shift;
158
159     if ( my $flash_data = $c->_flash ) {
160
161         my $hashes = $c->_flash_key_hashes || {};
162         my $keep = $c->_flash_keep_keys || {};
163         foreach my $key ( keys %$hashes ) {
164             if ( !exists $keep->{$key} and Object::Signature::signature( \$flash_data->{$key} ) eq $hashes->{$key} ) {
165                 delete $flash_data->{$key};
166             }
167         }
168         
169         my $sid = $c->sessionid;
170
171         if (%$flash_data) {
172             $c->store_session_data( "flash:$sid", $flash_data );
173         }
174         else {
175             $c->delete_session_data("flash:$sid");
176         }
177     }
178 }
179
180 sub _load_session_expires {
181     my $c = shift;
182     return $c->_session_expires if $c->_tried_loading_session_expires;
183     $c->_tried_loading_session_expires(1);
184
185     if ( my $sid = $c->sessionid ) {
186         my $expires = $c->get_session_data("expires:$sid") || 0;
187
188         if ( $expires >= time() ) {
189             $c->_session_expires( $expires );
190             return $expires;
191         } else {
192             $c->delete_session( "session expired" );
193             return 0;
194         }
195     }
196
197     return;
198 }
199
200 sub _load_session {
201     my $c = shift;
202     return $c->_session if $c->_tried_loading_session_data;
203     $c->_tried_loading_session_data(1);
204
205     if ( my $sid = $c->sessionid ) {
206         if ( $c->_load_session_expires ) {    # > 0
207
208             my $session_data = $c->get_session_data("session:$sid") || return;
209             $c->_session($session_data);
210
211             no warnings 'uninitialized';    # ne __address
212             if (   $c->config->{session}{verify_address}
213                 && $session_data->{__address} ne $c->request->address )
214             {
215                 $c->log->warn(
216                         "Deleting session $sid due to address mismatch ("
217                       . $session_data->{__address} . " != "
218                       . $c->request->address . ")"
219                 );
220                 $c->delete_session("address mismatch");
221                 return;
222             }
223
224             $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
225             $c->_session_data_sig( Object::Signature::signature($session_data) ) if $session_data;
226             $c->_expire_session_keys;
227
228             return $session_data;
229         }
230     }
231
232     return;
233 }
234
235 sub _load_flash {
236     my $c = shift;
237     return $c->_flash if $c->_tried_loading_flash_data;
238     $c->_tried_loading_flash_data(1);
239
240     if ( my $sid = $c->sessionid ) {
241         if ( my $flash_data = $c->_flash
242             || $c->_flash( $c->get_session_data("flash:$sid") ) )
243         {
244             $c->_flash_key_hashes({ map { $_ => Object::Signature::signature( \$flash_data->{$_} ) } keys %$flash_data });
245             
246             return $flash_data;
247         }
248     }
249
250     return;
251 }
252
253 sub _expire_session_keys {
254     my ( $c, $data ) = @_;
255
256     my $now = time;
257
258     my $expire_times = ( $data || $c->_session || {} )->{__expire_keys} || {};
259     foreach my $key ( grep { $expire_times->{$_} < $now } keys %$expire_times ) {
260         delete $c->_session->{$key};
261         delete $expire_times->{$key};
262     }
263 }
264
265 sub _clear_session_instance_data {
266     my $c = shift;
267     $c->$_(undef) for @session_data_accessors;
268     $c->NEXT::_clear_session_instance_data; # allow other plugins to hook in on this
269 }
270
271 sub delete_session {
272     my ( $c, $msg ) = @_;
273
274     $c->log->debug("Deleting session" . ( defined($msg) ? "($msg)" : '(no reason given)') ) if $c->debug;
275
276     # delete the session data
277     if ( my $sid = $c->sessionid ) {
278         $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
279         $c->delete_session_id($sid);
280     }
281
282     # reset the values in the context object
283     # see the BEGIN block
284     $c->_clear_session_instance_data;
285
286     $c->_session_delete_reason($msg);
287 }
288
289 sub session_delete_reason {
290     my $c = shift;
291
292     $c->session_is_valid; # check that it was loaded
293
294     $c->_session_delete_reason(@_);
295 }
296
297 sub session_expires {
298     my $c = shift;
299
300     if ( defined( my $expires = $c->_extended_session_expires ) ) {
301         return $expires;
302     } elsif ( defined( $expires = $c->_load_session_expires ) ) {
303         return $c->extend_session_expires( $expires );
304     } else {
305         return 0;
306     }
307 }
308
309 sub extend_session_expires {
310     my ( $c, $expires ) = @_;
311     $c->_extended_session_expires( my $updated = $c->calculate_extended_session_expires( $expires ) );
312     $c->extend_session_id( $c->sessionid, $updated );
313     return $updated;
314 }
315
316 sub calculate_initial_session_expires {
317     my $c = shift;
318     return ( time() + $c->config->{session}{expires} );
319 }
320
321 sub calculate_extended_session_expires {
322     my ( $c, $prev ) = @_;
323     $c->calculate_initial_session_expires;
324 }
325
326 sub reset_session_expires {
327     my ( $c, $sid ) = @_;
328     
329     my $exp = $c->calculate_initial_session_expires;
330     $c->_session_expires( $exp );
331     $c->_extended_session_expires( $exp );
332     $exp;
333 }
334
335 sub sessionid {
336     my $c = shift;
337     
338     return $c->_sessionid || $c->_load_sessionid;
339 }
340
341 sub _load_sessionid {
342     my $c = shift;
343     return if $c->_tried_loading_session_id;
344     $c->_tried_loading_session_id(1);
345
346     if ( defined( my $sid = $c->get_session_id ) ) {
347         if ( $c->validate_session_id($sid) ) {
348             # temporarily set the inner key, so that validation will work
349             $c->_sessionid($sid);
350             return $sid;
351         } else {
352             my $err = "Tried to set invalid session ID '$sid'";
353             $c->log->error($err);
354             Catalyst::Exception->throw($err);
355         }
356     }
357
358     return;
359 }
360
361 sub session_is_valid {
362     my $c = shift;
363
364     # force a check for expiry, but also __address, etc
365     if ( $c->_load_session ) {
366         return 1;
367     } else {
368         return;
369     }
370 }
371
372 sub validate_session_id {
373     my ( $c, $sid ) = @_;
374
375     $sid and $sid =~ /^[a-f\d]+$/i;
376 }
377
378 sub session {
379     my $c = shift;
380
381     $c->_session || $c->_load_session || do {
382         $c->create_session_id_if_needed;
383         $c->initialize_session_data;
384     };
385 }
386
387 sub keep_flash {
388     my ( $c, @keys ) = @_;
389     my $href = $c->_flash_keep_keys || $c->_flash_keep_keys({});
390     (@{$href}{@keys}) = ((undef) x @keys);
391 }
392
393 sub _flash_data { 
394     my $c = shift;
395     $c->_flash || $c->_load_flash || do {
396         $c->create_session_id_if_needed;
397         $c->_flash( {} );
398     };
399 }
400
401 sub _set_flash {
402     my $c = shift;
403     if (@_) {
404         my $items = @_ > 1 ? {@_} : $_[0];
405         croak('flash takes a hash or hashref') unless ref $items;
406         @{ $c->_flash }{ keys %$items } = values %$items;
407     }
408 }
409
410 sub flash {
411     my $c = shift;
412     $c->_flash_data;
413     $c->_set_flash(@_);
414     return $c->_flash;
415 }
416
417 sub clear_flash {
418     my $c = shift;
419     
420     #$c->delete_session_data("flash:" . $c->sessionid); # should this be in here? or delayed till finalization?
421     $c->_flash_key_hashes({});
422     $c->_flash_keep_keys({});
423     $c->_flash({});
424 }
425
426 sub session_expire_key {
427     my ( $c, %keys ) = @_;
428
429     my $now = time;
430     @{ $c->session->{__expire_keys} }{ keys %keys } =
431       map { $now + $_ } values %keys;
432 }
433
434 sub initialize_session_data {
435     my $c = shift;
436
437     my $now = time;
438
439     return $c->_session(
440         {
441             __created => $now,
442             __updated => $now,
443
444             (
445                 $c->config->{session}{verify_address}
446                 ? ( __address => $c->request->address )
447                 : ()
448             ),
449         }
450     );
451 }
452
453 sub generate_session_id {
454     my $c = shift;
455
456     my $digest = $c->_find_digest();
457     $digest->add( $c->session_hash_seed() );
458     return $digest->hexdigest;
459 }
460
461 sub create_session_id_if_needed {
462     my $c = shift;
463     $c->create_session_id unless $c->sessionid;
464 }
465
466 sub create_session_id {
467     my $c = shift;
468     
469     my $sid = $c->generate_session_id;
470
471     $c->log->debug(qq/Created session "$sid"/) if $c->debug;
472
473     $c->_sessionid($sid);
474     $c->reset_session_expires;
475     $c->set_session_id($sid);
476
477     return $sid;
478 }
479
480 my $counter;
481
482 sub session_hash_seed {
483     my $c = shift;
484
485     return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
486 }
487
488 my $usable;
489
490 sub _find_digest () {
491     unless ($usable) {
492         foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
493             if ( eval { Digest->new($alg) } ) {
494                 $usable = $alg;
495                 last;
496             }
497         }
498         Catalyst::Exception->throw(
499                 "Could not find a suitable Digest module. Please install "
500               . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
501           unless $usable;
502     }
503
504     return Digest->new($usable);
505 }
506
507 sub dump_these {
508     my $c = shift;
509
510     (
511         $c->NEXT::dump_these(),
512
513         $c->sessionid
514         ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
515         : ()
516     );
517 }
518
519
520 sub get_session_id { shift->NEXT::get_session_id(@_) }
521 sub set_session_id { shift->NEXT::set_session_id(@_) }
522 sub delete_session_id { shift->NEXT::delete_session_id(@_) }
523 sub extend_session_id { shift->NEXT::extend_session_id(@_) }
524
525 __PACKAGE__;
526
527 __END__
528
529 =pod
530
531 =head1 NAME
532
533 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
534 storage and client side state required to maintain session data.
535
536 =head1 SYNOPSIS
537
538     # To get sessions to "just work", all you need to do is use these plugins:
539
540     use Catalyst qw/
541       Session
542       Session::Store::FastMmap
543       Session::State::Cookie
544       /;
545
546         # you can replace Store::FastMmap with Store::File - both have sensible
547         # default configurations (see their docs for details)
548
549         # more complicated backends are available for other scenarios (DBI storage,
550         # etc)
551
552
553     # after you've loaded the plugins you can save session data
554     # For example, if you are writing a shopping cart, it could be implemented
555     # like this:
556
557     sub add_item : Local {
558         my ( $self, $c ) = @_;
559
560         my $item_id = $c->req->param("item");
561
562         # $c->session is a hash ref, a bit like $c->stash
563         # the difference is that it' preserved across requests
564
565         push @{ $c->session->{items} }, $item_id;
566
567         $c->forward("MyView");
568     }
569
570     sub display_items : Local {
571         my ( $self, $c ) = @_;
572
573         # values in $c->session are restored
574         $c->stash->{items_to_display} =
575           [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
576
577         $c->forward("MyView");
578     }
579
580 =head1 DESCRIPTION
581
582 The Session plugin is the base of two related parts of functionality required
583 for session management in web applications.
584
585 The first part, the State, is getting the browser to repeat back a session key,
586 so that the web application can identify the client and logically string
587 several requests together into a session.
588
589 The second part, the Store, deals with the actual storage of information about
590 the client. This data is stored so that the it may be revived for every request
591 made by the same client.
592
593 This plugin links the two pieces together.
594
595 =head1 RECOMENDED BACKENDS
596
597 =over 4
598
599 =item Session::State::Cookie
600
601 The only really sane way to do state is using cookies.
602
603 =item Session::Store::File
604
605 A portable backend, based on Cache::File.
606
607 =item Session::Store::FastMmap
608
609 A fast and flexible backend, based on Cache::FastMmap.
610
611 =back
612
613 =head1 METHODS
614
615 =over 4
616
617 =item sessionid
618
619 An accessor for the session ID value.
620
621 =item session
622
623 Returns a hash reference that might contain unserialized values from previous
624 requests in the same session, and whose modified value will be saved for future
625 requests.
626
627 This method will automatically create a new session and session ID if none
628 exists.
629
630 =item session_expires
631
632 =item session_expires $reset
633
634 This method returns the time when the current session will expire, or 0 if
635 there is no current session. If there is a session and it already expired, it
636 will delete the session and return 0 as well.
637
638 If the C<$reset> parameter is true, and there is a session ID the expiry time
639 will be reset to the current time plus the time to live (see
640 L</CONFIGURATION>). This is used when creating a new session.
641
642 =item flash
643
644 This is like Ruby on Rails' flash data structure. Think of it as a stash that
645 lasts for longer than one request, letting you redirect instead of forward.
646
647 The flash data will be cleaned up only on requests on which actually use
648 $c->flash (thus allowing multiple redirections), and the policy is to delete
649 all the keys which haven't changed since the flash data was loaded at the end
650 of every request.
651
652     sub moose : Local {
653         my ( $self, $c ) = @_;
654
655         $c->flash->{beans} = 10;
656         $c->response->redirect( $c->uri_for("foo") );
657     }
658
659     sub foo : Local {
660         my ( $self, $c ) = @_;
661
662         my $value = $c->flash->{beans};
663
664         # ...
665
666         $c->response->redirect( $c->uri_for("bar") );
667     }
668
669     sub bar : Local {
670         my ( $self, $c ) = @_;
671
672         if ( exists $c->flash->{beans} ) { # false
673         
674         }
675     }
676
677 =item clear_flash
678
679 Zap all the keys in the flash regardless of their current state.
680
681 =item keep_flash @keys
682
683 If you wawnt to keep a flash key for the next request too, even if it hasn't
684 changed, call C<keep_flash> and pass in the keys as arguments.
685
686 =item delete_session REASON
687
688 This method is used to invalidate a session. It takes an optional parameter
689 which will be saved in C<session_delete_reason> if provided.
690
691 =item session_delete_reason
692
693 This accessor contains a string with the reason a session was deleted. Possible
694 values include:
695
696 =over 4
697
698 =item *
699
700 C<address mismatch>
701
702 =item *
703
704 C<session expired>
705
706 =back
707
708 =item session_expire_key $key, $ttl
709
710 Mark a key to expire at a certain time (only useful when shorter than the
711 expiry time for the whole session).
712
713 For example:
714
715     __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
716
717     # later
718
719     $c->session_expire_key( __user => 3600 );
720
721 Will make the session data survive, but the user will still be logged out after
722 an hour.
723
724 Note that these values are not auto extended.
725
726 =back
727
728 =head1 INTERNAL METHODS
729
730 =over 4
731
732 =item setup
733
734 This method is extended to also make calls to
735 C<check_session_plugin_requirements> and C<setup_session>.
736
737 =item check_session_plugin_requirements
738
739 This method ensures that a State and a Store plugin are also in use by the
740 application.
741
742 =item setup_session
743
744 This method populates C<< $c->config->{session} >> with the default values
745 listed in L</CONFIGURATION>.
746
747 =item prepare_action
748
749 This methoid is extended.
750
751 It's only effect is if the (off by default) C<flash_to_stash> configuration
752 parameter is on - then it will copy the contents of the flash to the stash at
753 prepare time.
754
755 =item finalize_headers
756
757 This method is extended and will extend the expiry time before sending
758 the response.
759
760 =item finalize
761
762 This method is extended and will call finalize_session after the other
763 finalizes run.  Here we persist the session data if a session exists.
764
765 =item initialize_session_data
766
767 This method will initialize the internal structure of the session, and is
768 called by the C<session> method if appropriate.
769
770 =item create_session_id
771
772 Creates a new session id using C<generate_session_id> if there is no session ID
773 yet.
774
775 =item validate_session_id SID
776
777 Make sure a session ID is of the right format.
778
779 This currently ensures that the session ID string is any amount of case
780 insensitive hexadecimal characters.
781
782 =item generate_session_id
783
784 This method will return a string that can be used as a session ID. It is
785 supposed to be a reasonably random string with enough bits to prevent
786 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
787 MD5 or SHA-256, depending on the availibility of these modules.
788
789 =item session_hash_seed
790
791 This method is actually rather internal to generate_session_id, but should be
792 overridable in case you want to provide more random data.
793
794 Currently it returns a concatenated string which contains:
795
796 =over 4
797
798 =item *
799
800 A counter
801
802 =item *
803
804 The current time
805
806 =item *
807
808 One value from C<rand>.
809
810 =item *
811
812 The stringified value of a newly allocated hash reference
813
814 =item *
815
816 The stringified value of the Catalyst context object
817
818 =back
819
820 In the hopes that those combined values are entropic enough for most uses. If
821 this is not the case you can replace C<session_hash_seed> with e.g.
822
823     sub session_hash_seed {
824         open my $fh, "<", "/dev/random";
825         read $fh, my $bytes, 20;
826         close $fh;
827         return $bytes;
828     }
829
830 Or even more directly, replace C<generate_session_id>:
831
832     sub generate_session_id {
833         open my $fh, "<", "/dev/random";
834         read $fh, my $bytes, 20;
835         close $fh;
836         return unpack("H*", $bytes);
837     }
838
839 Also have a look at L<Crypt::Random> and the various openssl bindings - these
840 modules provide APIs for cryptographically secure random data.
841
842 =item finalize_session
843
844 Clean up the session during C<finalize>.
845
846 This clears the various accessors after saving to the store.
847
848 =item dump_these
849
850 See L<Catalyst/dump_these> - ammends the session data structure to the list of
851 dumped objects if session ID is defined.
852
853
854 =item calculate_extended_session_expires
855
856 =item calculate_initial_session_expires
857
858 =item create_session_id_if_needed
859
860 =item delete_session_id
861
862 =item extend_session_expires
863
864 =item extend_session_id
865
866 =item get_session_id
867
868 =item reset_session_expires
869
870 =item session_is_valid
871
872 =item set_session_id
873
874 =back
875
876 =head1 USING SESSIONS DURING PREPARE
877
878 The earliest point in time at which you may use the session data is after
879 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
880
881 State plugins must set $c->session ID before C<prepare_action>, and during
882 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
883 the store.
884
885         sub prepare_action {
886                 my $c = shift;
887
888                 # don't touch $c->session yet!
889
890                 $c->NEXT::prepare_action( @_ );
891
892                 $c->session;  # this is OK
893                 $c->sessionid; # this is also OK
894         }
895
896 =head1 CONFIGURATION
897
898     $c->config->{session} = {
899         expires => 1234,
900     };
901
902 All configuation parameters are provided in a hash reference under the
903 C<session> key in the configuration hash.
904
905 =over 4
906
907 =item expires
908
909 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
910 hours).
911
912 =item verify_address
913
914 When true, C<<$c->request->address>> will be checked at prepare time. If it is
915 not the same as the address that initiated the session, the session is deleted.
916
917 Defaults to false.
918
919 =item flash_to_stash
920
921 This option makes it easier to have actions behave the same whether they were
922 forwarded to or redirected to. On prepare time it copies the contents of
923 C<flash> (if any) to the stash.
924
925 =back
926
927 =head1 SPECIAL KEYS
928
929 The hash reference returned by C<< $c->session >> contains several keys which
930 are automatically set:
931
932 =over 4
933
934 =item __expires
935
936 This key no longer exists. Use C<session_expires> instead.
937
938 =item __updated
939
940 The last time a session was saved to the store.
941
942 =item __created
943
944 The time when the session was first created.
945
946 =item __address
947
948 The value of C<< $c->request->address >> at the time the session was created.
949 This value is only populated if C<verify_address> is true in the configuration.
950
951 =back
952
953 =head1 CAVEATS
954
955 =head2 Round the Robin Proxies
956
957 C<verify_address> could make your site inaccessible to users who are behind
958 load balanced proxies. Some ISPs may give a different IP to each request by the
959 same client due to this type of proxying. If addresses are verified these
960 users' sessions cannot persist.
961
962 To let these users access your site you can either disable address verification
963 as a whole, or provide a checkbox in the login dialog that tells the server
964 that it's OK for the address of the client to change. When the server sees that
965 this box is checked it should delete the C<__address> sepcial key from the
966 session hash when the hash is first created.
967
968 =head2 Race Conditions
969
970 In this day and age where cleaning detergents and dutch football (not the
971 american kind) teams roam the plains in great numbers, requests may happen
972 simultaneously. This means that there is some risk of session data being
973 overwritten, like this:
974
975 =over 4
976
977 =item 1.
978
979 request a starts, request b starts, with the same session id
980
981 =item 2.
982
983 session data is loaded in request a
984
985 =item 3.
986
987 session data is loaded in request b
988
989 =item 4.
990
991 session data is changed in request a
992
993 =item 5.
994
995 request a finishes, session data is updated and written to store
996
997 =item 6.
998
999 request b finishes, session data is updated and written to store, overwriting
1000 changes by request a
1001
1002 =back
1003
1004 If this is a concern in your application, a soon to be developed locking
1005 solution is the only safe way to go. This will have a bigger overhead.
1006
1007 For applications where any given user is only making one request at a time this
1008 plugin should be safe enough.
1009
1010 =head1 AUTHORS
1011
1012 =over 4
1013
1014 =item Andy Grundman
1015
1016 =item Christian Hansen
1017
1018 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
1019
1020 =item Sebastian Riedel
1021
1022 =back
1023
1024 And countless other contributers from #catalyst. Thanks guys!
1025
1026 =head1 COPYRIGHT & LICENSE
1027
1028         Copyright (c) 2005 the aforementioned authors. All rights
1029         reserved. This program is free software; you can redistribute
1030         it and/or modify it under the same terms as Perl itself.
1031
1032 =cut
1033
1034