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