08ef36680c5c60423aed9662c37e05cd8bbafd20
[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         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 storage and client side state required to maintain session data.
534
535 =head1 SYNOPSIS
536
537     # To get sessions to "just work", all you need to do is use these plugins:
538
539     use Catalyst qw/
540       Session
541       Session::Store::FastMmap
542       Session::State::Cookie
543       /;
544
545     # you can replace Store::FastMmap with Store::File - both have sensible
546     # default configurations (see their docs for details)
547
548     # more complicated backends are available for other scenarios (DBI storage,
549     # etc)
550
551
552     # after you've loaded the plugins you can save session data
553     # For example, if you are writing a shopping cart, it could be implemented
554     # like this:
555
556     sub add_item : Local {
557         my ( $self, $c ) = @_;
558
559         my $item_id = $c->req->param("item");
560
561         # $c->session is a hash ref, a bit like $c->stash
562         # the difference is that it' preserved across requests
563
564         push @{ $c->session->{items} }, $item_id;
565
566         $c->forward("MyView");
567     }
568
569     sub display_items : Local {
570         my ( $self, $c ) = @_;
571
572         # values in $c->session are restored
573         $c->stash->{items_to_display} =
574           [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
575
576         $c->forward("MyView");
577     }
578
579 =head1 DESCRIPTION
580
581 The Session plugin is the base of two related parts of functionality required
582 for session management in web applications.
583
584 The first part, the State, is getting the browser to repeat back a session key,
585 so that the web application can identify the client and logically string
586 several requests together into a session.
587
588 The second part, the Store, deals with the actual storage of information about
589 the client. This data is stored so that the it may be revived for every request
590 made by the same client.
591
592 This plugin links the two pieces together.
593
594 =head1 RECOMENDED BACKENDS
595
596 =over 4
597
598 =item Session::State::Cookie
599
600 The only really sane way to do state is using cookies.
601
602 =item Session::Store::File
603
604 A portable backend, based on Cache::File.
605
606 =item Session::Store::FastMmap
607
608 A fast and flexible backend, based on Cache::FastMmap.
609
610 =back
611
612 =head1 METHODS
613
614 =over 4
615
616 =item sessionid
617
618 An accessor for the session ID value.
619
620 =item session
621
622 Returns a hash reference that might contain unserialized values from previous
623 requests in the same session, and whose modified value will be saved for future
624 requests.
625
626 This method will automatically create a new session and session ID if none
627 exists.
628
629 =item session_expires
630
631 =item session_expires $reset
632
633 This method returns the time when the current session will expire, or 0 if
634 there is no current session. If there is a session and it already expired, it
635 will delete the session and return 0 as well.
636
637 If the C<$reset> parameter is true, and there is a session ID the expiry time
638 will be reset to the current time plus the time to live (see
639 L</CONFIGURATION>). This is used when creating a new session.
640
641 =item flash
642
643 This is like Ruby on Rails' flash data structure. Think of it as a stash that
644 lasts for longer than one request, letting you redirect instead of forward.
645
646 The flash data will be cleaned up only on requests on which actually use
647 $c->flash (thus allowing multiple redirections), and the policy is to delete
648 all the keys which haven't changed since the flash data was loaded at the end
649 of every request.
650
651     sub moose : Local {
652         my ( $self, $c ) = @_;
653
654         $c->flash->{beans} = 10;
655         $c->response->redirect( $c->uri_for("foo") );
656     }
657
658     sub foo : Local {
659         my ( $self, $c ) = @_;
660
661         my $value = $c->flash->{beans};
662
663         # ...
664
665         $c->response->redirect( $c->uri_for("bar") );
666     }
667
668     sub bar : Local {
669         my ( $self, $c ) = @_;
670
671         if ( exists $c->flash->{beans} ) { # false
672         
673         }
674     }
675
676 =item clear_flash
677
678 Zap all the keys in the flash regardless of their current state.
679
680 =item keep_flash @keys
681
682 If you want to keep a flash key for the next request too, even if it hasn't
683 changed, call C<keep_flash> and pass in the keys as arguments.
684
685 =item delete_session REASON
686
687 This method is used to invalidate a session. It takes an optional parameter
688 which will be saved in C<session_delete_reason> if provided.
689
690 =item session_delete_reason
691
692 This accessor contains a string with the reason a session was deleted. Possible
693 values include:
694
695 =over 4
696
697 =item *
698
699 C<address mismatch>
700
701 =item *
702
703 C<session expired>
704
705 =back
706
707 =item session_expire_key $key, $ttl
708
709 Mark a key to expire at a certain time (only useful when shorter than the
710 expiry time for the whole session).
711
712 For example:
713
714     __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
715
716     # later
717
718     $c->session_expire_key( __user => 3600 );
719
720 Will make the session data survive, but the user will still be logged out after
721 an hour.
722
723 Note that these values are not auto extended.
724
725 =back
726
727 =head1 INTERNAL METHODS
728
729 =over 4
730
731 =item setup
732
733 This method is extended to also make calls to
734 C<check_session_plugin_requirements> and C<setup_session>.
735
736 =item check_session_plugin_requirements
737
738 This method ensures that a State and a Store plugin are also in use by the
739 application.
740
741 =item setup_session
742
743 This method populates C<< $c->config->{session} >> with the default values
744 listed in L</CONFIGURATION>.
745
746 =item prepare_action
747
748 This methoid is extended.
749
750 It's only effect is if the (off by default) C<flash_to_stash> configuration
751 parameter is on - then it will copy the contents of the flash to the stash at
752 prepare time.
753
754 =item finalize_headers
755
756 This method is extended and will extend the expiry time before sending
757 the response.
758
759 =item finalize
760
761 This method is extended and will call finalize_session after the other
762 finalizes run.  Here we persist the session data if a session exists.
763
764 =item initialize_session_data
765
766 This method will initialize the internal structure of the session, and is
767 called by the C<session> method if appropriate.
768
769 =item create_session_id
770
771 Creates a new session id using C<generate_session_id> if there is no session ID
772 yet.
773
774 =item validate_session_id SID
775
776 Make sure a session ID is of the right format.
777
778 This currently ensures that the session ID string is any amount of case
779 insensitive hexadecimal characters.
780
781 =item generate_session_id
782
783 This method will return a string that can be used as a session ID. It is
784 supposed to be a reasonably random string with enough bits to prevent
785 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
786 MD5 or SHA-256, depending on the availibility of these modules.
787
788 =item session_hash_seed
789
790 This method is actually rather internal to generate_session_id, but should be
791 overridable in case you want to provide more random data.
792
793 Currently it returns a concatenated string which contains:
794
795 =over 4
796
797 =item * A counter
798
799 =item * The current time
800
801 =item * One value from C<rand>.
802
803 =item * The stringified value of a newly allocated hash reference
804
805 =item * The stringified value of the Catalyst context object
806
807 =back
808
809 In the hopes that those combined values are entropic enough for most uses. If
810 this is not the case you can replace C<session_hash_seed> with e.g.
811
812     sub session_hash_seed {
813         open my $fh, "<", "/dev/random";
814         read $fh, my $bytes, 20;
815         close $fh;
816         return $bytes;
817     }
818
819 Or even more directly, replace C<generate_session_id>:
820
821     sub generate_session_id {
822         open my $fh, "<", "/dev/random";
823         read $fh, my $bytes, 20;
824         close $fh;
825         return unpack("H*", $bytes);
826     }
827
828 Also have a look at L<Crypt::Random> and the various openssl bindings - these
829 modules provide APIs for cryptographically secure random data.
830
831 =item finalize_session
832
833 Clean up the session during C<finalize>.
834
835 This clears the various accessors after saving to the store.
836
837 =item dump_these
838
839 See L<Catalyst/dump_these> - ammends the session data structure to the list of
840 dumped objects if session ID is defined.
841
842
843 =item calculate_extended_session_expires
844
845 =item calculate_initial_session_expires
846
847 =item create_session_id_if_needed
848
849 =item delete_session_id
850
851 =item extend_session_expires
852
853 =item extend_session_id
854
855 =item get_session_id
856
857 =item reset_session_expires
858
859 =item session_is_valid
860
861 =item set_session_id
862
863 =back
864
865 =head1 USING SESSIONS DURING PREPARE
866
867 The earliest point in time at which you may use the session data is after
868 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
869
870 State plugins must set $c->session ID before C<prepare_action>, and during
871 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
872 the store.
873
874     sub prepare_action {
875         my $c = shift;
876
877         # don't touch $c->session yet!
878
879         $c->NEXT::prepare_action( @_ );
880
881         $c->session;  # this is OK
882         $c->sessionid; # this is also OK
883     }
884
885 =head1 CONFIGURATION
886
887     $c->config->{session} = {
888         expires => 1234,
889     };
890
891 All configuation parameters are provided in a hash reference under the
892 C<session> key in the configuration hash.
893
894 =over 4
895
896 =item expires
897
898 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
899 hours).
900
901 =item verify_address
902
903 When true, C<<$c->request->address>> will be checked at prepare time. If it is
904 not the same as the address that initiated the session, the session is deleted.
905
906 Defaults to false.
907
908 =item flash_to_stash
909
910 This option makes it easier to have actions behave the same whether they were
911 forwarded to or redirected to. On prepare time it copies the contents of
912 C<flash> (if any) to the stash.
913
914 =back
915
916 =head1 SPECIAL KEYS
917
918 The hash reference returned by C<< $c->session >> contains several keys which
919 are automatically set:
920
921 =over 4
922
923 =item __expires
924
925 This key no longer exists. Use C<session_expires> instead.
926
927 =item __updated
928
929 The last time a session was saved to the store.
930
931 =item __created
932
933 The time when the session was first created.
934
935 =item __address
936
937 The value of C<< $c->request->address >> at the time the session was created.
938 This value is only populated if C<verify_address> is true in the configuration.
939
940 =back
941
942 =head1 CAVEATS
943
944 =head2 Round the Robin Proxies
945
946 C<verify_address> could make your site inaccessible to users who are behind
947 load balanced proxies. Some ISPs may give a different IP to each request by the
948 same client due to this type of proxying. If addresses are verified these
949 users' sessions cannot persist.
950
951 To let these users access your site you can either disable address verification
952 as a whole, or provide a checkbox in the login dialog that tells the server
953 that it's OK for the address of the client to change. When the server sees that
954 this box is checked it should delete the C<__address> sepcial key from the
955 session hash when the hash is first created.
956
957 =head2 Race Conditions
958
959 In this day and age where cleaning detergents and dutch football (not the
960 american kind) teams roam the plains in great numbers, requests may happen
961 simultaneously. This means that there is some risk of session data being
962 overwritten, like this:
963
964 =over 4
965
966 =item 1.
967
968 request a starts, request b starts, with the same session id
969
970 =item 2.
971
972 session data is loaded in request a
973
974 =item 3.
975
976 session data is loaded in request b
977
978 =item 4.
979
980 session data is changed in request a
981
982 =item 5.
983
984 request a finishes, session data is updated and written to store
985
986 =item 6.
987
988 request b finishes, session data is updated and written to store, overwriting
989 changes by request a
990
991 =back
992
993 If this is a concern in your application, a soon to be developed locking
994 solution is the only safe way to go. This will have a bigger overhead.
995
996 For applications where any given user is only making one request at a time this
997 plugin should be safe enough.
998
999 =head1 AUTHORS
1000
1001 Andy Grundman
1002
1003 Christian Hansen
1004
1005 Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
1006
1007 Sebastian Riedel
1008
1009 And countless other contributers from #catalyst. Thanks guys!
1010
1011 =head1 COPYRIGHT & LICENSE
1012
1013     Copyright (c) 2005 the aforementioned authors. All rights
1014     reserved. This program is free software; you can redistribute
1015     it and/or modify it under the same terms as Perl itself.
1016
1017 =cut
1018
1019