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