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