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