Removed useless parameter
[catagits/Catalyst-Plugin-Session.git] / lib / Catalyst / Plugin / Session.pm
1 #!/usr/bin/perl
2
3 package Catalyst::Plugin::Session;
4
5 use Moose;
6 with 'MooseX::Emulate::Class::Accessor::Fast';
7 use MRO::Compat;
8 use Catalyst::Exception ();
9 use Digest              ();
10 use overload            ();
11 use Object::Signature   ();
12 use Carp;
13
14 use namespace::clean -except => 'meta';
15
16 our $VERSION = '0.37';
17 $VERSION = eval $VERSION;
18
19 my @session_data_accessors; # used in delete_session
20
21 __PACKAGE__->mk_accessors(
22         "_session_delete_reason",
23         @session_data_accessors = qw/
24           _sessionid
25           _session
26           _session_expires
27           _extended_session_expires
28           _session_data_sig
29           _flash
30           _flash_keep_keys
31           _flash_key_hashes
32           _tried_loading_session_id
33           _tried_loading_session_data
34           _tried_loading_session_expires
35           _tried_loading_flash_data
36           /
37 );
38
39 sub _session_plugin_config {
40     my $c = shift;
41     # FIXME - Start warning once all the state/store modules have also been updated.
42     #$c->log->warn("Deprecated 'session' config key used, please use the key 'Plugin::Session' instead")
43     #    if exists $c->config->{session}
44     #$c->config->{'Plugin::Session'} ||= delete($c->config->{session}) || {};
45     $c->config->{'Plugin::Session'} ||= $c->config->{session} || {};
46 }
47
48 sub setup {
49     my $c = shift;
50
51     $c->maybe::next::method(@_);
52
53     $c->check_session_plugin_requirements;
54     $c->setup_session;
55
56     return $c;
57 }
58
59 sub check_session_plugin_requirements {
60     my $c = shift;
61
62     unless ( $c->isa("Catalyst::Plugin::Session::State")
63         && $c->isa("Catalyst::Plugin::Session::Store") )
64     {
65         my $err =
66           (     "The Session plugin requires both Session::State "
67               . "and Session::Store plugins to be used as well." );
68
69         $c->log->fatal($err);
70         Catalyst::Exception->throw($err);
71     }
72 }
73
74 sub setup_session {
75     my $c = shift;
76
77     my $cfg = $c->_session_plugin_config;
78
79     %$cfg = (
80         expires        => 7200,
81         verify_address => 0,
82         verify_user_agent => 0,
83         expiry_threshold => 0,
84         %$cfg,
85     );
86
87     $c->maybe::next::method();
88 }
89
90 sub prepare_action {
91     my $c = shift;
92
93     $c->maybe::next::method(@_);
94
95     if (    $c->_session_plugin_config->{flash_to_stash}
96         and $c->sessionid
97         and my $flash_data = $c->flash )
98     {
99         @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
100     }
101 }
102
103 sub finalize_headers {
104     my $c = shift;
105
106     # fix cookie before we send headers
107     $c->_save_session_expires;
108
109     # Force extension of session_expires before finalizing headers, so a pos
110     # up to date. First call to session_expires will extend the expiry, subs
111     # just return the previously extended value.
112     $c->session_expires;
113
114     return $c->maybe::next::method(@_);
115 }
116
117 sub finalize_body {
118     my $c = shift;
119
120     # We have to finalize our session *before* $c->engine->finalize_xxx is called,
121     # because we do not want to send the HTTP response before the session is stored/committed to
122     # the session database (or whatever Session::Store you use).
123     $c->finalize_session;
124
125     return $c->maybe::next::method(@_);
126 }
127
128 sub finalize_session {
129     my $c = shift;
130
131     $c->maybe::next::method(@_);
132
133     $c->_save_session_id;
134     $c->_save_session;
135     $c->_save_flash;
136
137     $c->_clear_session_instance_data;
138 }
139
140 sub _save_session_id {
141     my $c = shift;
142
143     # we already called set when allocating
144     # no need to tell the state plugins anything new
145 }
146
147 sub _save_session_expires {
148     my $c = shift;
149
150     if ( defined($c->_session_expires) ) {
151
152         my $threshold = $c->_session_plugin_config->{expiry_threshold} || 0;
153
154         if ( my $sid = $c->sessionid ) {
155             my $cutoff = ( $c->get_session_data("expires:$sid") || 0 ) - $threshold;
156
157             if (!$threshold || $cutoff <= time) {
158                 my $expires = $c->session_expires; # force extension
159                 $c->store_session_data( "expires:$sid" => $expires );
160
161             }
162
163         }
164
165     }
166 }
167
168 sub _save_session {
169     my $c = shift;
170
171     if ( my $session_data = $c->_session ) {
172
173         no warnings 'uninitialized';
174         if ( Object::Signature::signature($session_data) ne
175             $c->_session_data_sig )
176         {
177             $session_data->{__updated} = time();
178             my $sid = $c->sessionid;
179             $c->store_session_data( "session:$sid" => $session_data );
180         }
181     }
182 }
183
184 sub _save_flash {
185     my $c = shift;
186
187     if ( my $flash_data = $c->_flash ) {
188
189         my $hashes = $c->_flash_key_hashes || {};
190         my $keep = $c->_flash_keep_keys || {};
191         foreach my $key ( keys %$hashes ) {
192             if ( !exists $keep->{$key} and Object::Signature::signature( \$flash_data->{$key} ) eq $hashes->{$key} ) {
193                 delete $flash_data->{$key};
194             }
195         }
196
197         my $sid = $c->sessionid;
198
199         my $session_data = $c->_session;
200         if (%$flash_data) {
201             $session_data->{__flash} = $flash_data;
202         }
203         else {
204             delete $session_data->{__flash};
205         }
206         $c->_session($session_data);
207         $c->_save_session;
208     }
209 }
210
211 sub _load_session_expires {
212     my $c = shift;
213     return $c->_session_expires if $c->_tried_loading_session_expires;
214     $c->_tried_loading_session_expires(1);
215
216     if ( my $sid = $c->sessionid ) {
217         my $expires = $c->get_session_data("expires:$sid") || 0;
218
219         if ( $expires >= time() ) {
220             $c->_session_expires( $expires );
221             return $expires;
222         } else {
223             $c->delete_session( "session expired" );
224             return 0;
225         }
226     }
227
228     return;
229 }
230
231 sub _load_session {
232     my $c = shift;
233     return $c->_session if $c->_tried_loading_session_data;
234     $c->_tried_loading_session_data(1);
235
236     if ( my $sid = $c->sessionid ) {
237         if ( $c->_load_session_expires ) {    # > 0
238
239             my $session_data = $c->get_session_data("session:$sid") || return;
240             $c->_session($session_data);
241
242             no warnings 'uninitialized';    # ne __address
243             if (   $c->_session_plugin_config->{verify_address}
244                 && exists $session_data->{__address}
245                 && $session_data->{__address} ne $c->request->address )
246             {
247                 $c->log->warn(
248                         "Deleting session $sid due to address mismatch ("
249                       . $session_data->{__address} . " != "
250                       . $c->request->address . ")"
251                 );
252                 $c->delete_session("address mismatch");
253                 return;
254             }
255             if (   $c->_session_plugin_config->{verify_user_agent}
256                 && $session_data->{__user_agent} ne $c->request->user_agent )
257             {
258                 $c->log->warn(
259                         "Deleting session $sid due to user agent mismatch ("
260                       . $session_data->{__user_agent} . " != "
261                       . $c->request->user_agent . ")"
262                 );
263                 $c->delete_session("user agent mismatch");
264                 return;
265             }
266
267             $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
268             $c->_session_data_sig( Object::Signature::signature($session_data) ) if $session_data;
269             $c->_expire_session_keys;
270
271             return $session_data;
272         }
273     }
274
275     return;
276 }
277
278 sub _load_flash {
279     my $c = shift;
280     return $c->_flash if $c->_tried_loading_flash_data;
281     $c->_tried_loading_flash_data(1);
282
283     if ( my $sid = $c->sessionid ) {
284
285         my $session_data = $c->session;
286         $c->_flash($session_data->{__flash});
287
288         if ( my $flash_data = $c->_flash )
289         {
290             $c->_flash_key_hashes({ map { $_ => Object::Signature::signature( \$flash_data->{$_} ) } keys %$flash_data });
291
292             return $flash_data;
293         }
294     }
295
296     return;
297 }
298
299 sub _expire_session_keys {
300     my ( $c, $data ) = @_;
301
302     my $now = time;
303
304     my $expire_times = ( $data || $c->_session || {} )->{__expire_keys} || {};
305     foreach my $key ( grep { $expire_times->{$_} < $now } keys %$expire_times ) {
306         delete $c->_session->{$key};
307         delete $expire_times->{$key};
308     }
309 }
310
311 sub _clear_session_instance_data {
312     my $c = shift;
313     $c->$_(undef) for @session_data_accessors;
314     $c->maybe::next::method(@_); # allow other plugins to hook in on this
315 }
316
317 sub change_session_id {
318     my $c = shift;
319
320     my $sessiondata = $c->session;
321     my $oldsid = $c->sessionid;
322     my $newsid = $c->create_session_id;
323
324     if ($oldsid) {
325         $c->log->debug(qq/change_sessid: deleting session data from "$oldsid"/) if $c->debug;
326         $c->delete_session_data("${_}:${oldsid}") for qw/session expires flash/;
327     }
328
329     $c->log->debug(qq/change_sessid: storing session data to "$newsid"/) if $c->debug;
330     $c->store_session_data( "session:$newsid" => $sessiondata );
331
332     return $newsid;
333 }
334
335 sub delete_session {
336     my ( $c, $msg ) = @_;
337
338     $c->log->debug("Deleting session" . ( defined($msg) ? "($msg)" : '(no reason given)') ) if $c->debug;
339
340     # delete the session data
341     if ( my $sid = $c->sessionid ) {
342         $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
343         $c->delete_session_id($sid);
344     }
345
346     # reset the values in the context object
347     # see the BEGIN block
348     $c->_clear_session_instance_data;
349
350     $c->_session_delete_reason($msg);
351 }
352
353 sub session_delete_reason {
354     my $c = shift;
355
356     $c->session_is_valid; # check that it was loaded
357
358     $c->_session_delete_reason(@_);
359 }
360
361 sub session_expires {
362     my $c = shift;
363
364     if ( defined( my $expires = $c->_extended_session_expires ) ) {
365         return $expires;
366     } elsif ( defined( $expires = $c->_load_session_expires ) ) {
367         return $c->extend_session_expires( $expires );
368     } else {
369         return 0;
370     }
371 }
372
373 sub extend_session_expires {
374     my ( $c, $expires ) = @_;
375     $c->_extended_session_expires( my $updated = $c->calculate_initial_session_expires() );
376     $c->extend_session_id( $c->sessionid, $updated );
377     return $updated;
378 }
379
380 sub change_session_expires {
381     my ( $c, $expires ) = @_;
382
383     $expires ||= 0;
384     my $sid = $c->sessionid;
385     my $time_exp = time() + $expires;
386     $c->store_session_data( "expires:$sid" => $time_exp );
387 }
388
389 sub initial_session_expires {
390     my $c = shift;
391     return ( time() + $c->_session_plugin_config->{expires} );
392 }
393
394 sub calculate_initial_session_expires {
395     my $c = shift;
396
397     my $initial_expires = $c->initial_session_expires;
398     my $stored_session_expires = 0;
399     if ( my $sid = $c->sessionid ) {
400         $stored_session_expires = $c->get_session_data("expires:$sid") || 0;
401     }
402     return ( $initial_expires > $stored_session_expires ) ? $initial_expires : $stored_session_expires;
403 }
404
405 sub calculate_extended_session_expires {
406     my ( $c, $prev ) = @_;
407     return ( time() + $prev );
408 }
409
410 sub reset_session_expires {
411     my ( $c, $sid ) = @_;
412
413     my $exp = $c->calculate_initial_session_expires;
414     $c->_session_expires( $exp );
415     #
416     # since we're setting _session_expires directly, make load_session_expires
417     # actually use that value.
418     #
419     $c->_tried_loading_session_expires(1);
420     $c->_extended_session_expires( $exp );
421     $exp;
422 }
423
424 sub sessionid {
425     my $c = shift;
426
427     return $c->_sessionid || $c->_load_sessionid;
428 }
429
430 sub _load_sessionid {
431     my $c = shift;
432     return if $c->_tried_loading_session_id;
433     $c->_tried_loading_session_id(1);
434
435     if ( defined( my $sid = $c->get_session_id ) ) {
436         if ( $c->validate_session_id($sid) ) {
437             # temporarily set the inner key, so that validation will work
438             $c->_sessionid($sid);
439             return $sid;
440         } else {
441             my $err = "Tried to set invalid session ID '$sid'";
442             $c->log->error($err);
443             Catalyst::Exception->throw($err);
444         }
445     }
446
447     return;
448 }
449
450 sub session_is_valid {
451     my $c = shift;
452
453     # force a check for expiry, but also __address, etc
454     if ( $c->_load_session ) {
455         return 1;
456     } else {
457         return;
458     }
459 }
460
461 sub validate_session_id {
462     my ( $c, $sid ) = @_;
463
464     $sid and $sid =~ /^[a-f\d]+$/i;
465 }
466
467 sub session {
468     my $c = shift;
469
470     my $session = $c->_session || $c->_load_session || do {
471         $c->create_session_id_if_needed;
472         $c->initialize_session_data;
473     };
474
475     if (@_) {
476       my $new_values = @_ > 1 ? { @_ } : $_[0];
477       croak('session takes a hash or hashref') unless ref $new_values;
478
479       for my $key (keys %$new_values) {
480         $session->{$key} = $new_values->{$key};
481       }
482     }
483
484     $session;
485 }
486
487 sub keep_flash {
488     my ( $c, @keys ) = @_;
489     my $href = $c->_flash_keep_keys || $c->_flash_keep_keys({});
490     (@{$href}{@keys}) = ((undef) x @keys);
491 }
492
493 sub _flash_data {
494     my $c = shift;
495     $c->_flash || $c->_load_flash || do {
496         $c->create_session_id_if_needed;
497         $c->_flash( {} );
498     };
499 }
500
501 sub _set_flash {
502     my $c = shift;
503     if (@_) {
504         my $items = @_ > 1 ? {@_} : $_[0];
505         croak('flash takes a hash or hashref') unless ref $items;
506         @{ $c->_flash }{ keys %$items } = values %$items;
507     }
508 }
509
510 sub flash {
511     my $c = shift;
512     $c->_flash_data;
513     $c->_set_flash(@_);
514     return $c->_flash;
515 }
516
517 sub clear_flash {
518     my $c = shift;
519
520     #$c->delete_session_data("flash:" . $c->sessionid); # should this be in here? or delayed till finalization?
521     $c->_flash_key_hashes({});
522     $c->_flash_keep_keys({});
523     $c->_flash({});
524 }
525
526 sub session_expire_key {
527     my ( $c, %keys ) = @_;
528
529     my $now = time;
530     @{ $c->session->{__expire_keys} }{ keys %keys } =
531       map { $now + $_ } values %keys;
532 }
533
534 sub initialize_session_data {
535     my $c = shift;
536
537     my $now = time;
538
539     return $c->_session(
540         {
541             __created => $now,
542             __updated => $now,
543
544             (
545                 $c->_session_plugin_config->{verify_address}
546                 ? ( __address => $c->request->address||'' )
547                 : ()
548             ),
549             (
550                 $c->_session_plugin_config->{verify_user_agent}
551                 ? ( __user_agent => $c->request->user_agent||'' )
552                 : ()
553             ),
554         }
555     );
556 }
557
558 sub generate_session_id {
559     my $c = shift;
560
561     my $digest = $c->_find_digest();
562     $digest->add( $c->session_hash_seed() );
563     return $digest->hexdigest;
564 }
565
566 sub create_session_id_if_needed {
567     my $c = shift;
568     $c->create_session_id unless $c->sessionid;
569 }
570
571 sub create_session_id {
572     my $c = shift;
573
574     my $sid = $c->generate_session_id;
575
576     $c->log->debug(qq/Created session "$sid"/) if $c->debug;
577
578     $c->_sessionid($sid);
579     $c->reset_session_expires;
580     $c->set_session_id($sid);
581
582     return $sid;
583 }
584
585 my $counter;
586
587 sub session_hash_seed {
588     my $c = shift;
589
590     return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
591 }
592
593 my $usable;
594
595 sub _find_digest () {
596     unless ($usable) {
597         foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
598             if ( eval { Digest->new($alg) } ) {
599                 $usable = $alg;
600                 last;
601             }
602         }
603         Catalyst::Exception->throw(
604                 "Could not find a suitable Digest module. Please install "
605               . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
606           unless $usable;
607     }
608
609     return Digest->new($usable);
610 }
611
612 sub dump_these {
613     my $c = shift;
614
615     (
616         $c->maybe::next::method(),
617
618         $c->_sessionid
619         ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
620         : ()
621     );
622 }
623
624
625 sub get_session_id { shift->maybe::next::method(@_) }
626 sub set_session_id { shift->maybe::next::method(@_) }
627 sub delete_session_id { shift->maybe::next::method(@_) }
628 sub extend_session_id { shift->maybe::next::method(@_) }
629
630 __PACKAGE__;
631
632 __END__
633
634 =pod
635
636 =head1 NAME
637
638 Catalyst::Plugin::Session - Generic Session plugin - ties together server side storage and client side state required to maintain session data.
639
640 =head1 SYNOPSIS
641
642     # To get sessions to "just work", all you need to do is use these plugins:
643
644     use Catalyst qw/
645       Session
646       Session::Store::FastMmap
647       Session::State::Cookie
648       /;
649
650     # you can replace Store::FastMmap with Store::File - both have sensible
651     # default configurations (see their docs for details)
652
653     # more complicated backends are available for other scenarios (DBI storage,
654     # etc)
655
656
657     # after you've loaded the plugins you can save session data
658     # For example, if you are writing a shopping cart, it could be implemented
659     # like this:
660
661     sub add_item : Local {
662         my ( $self, $c ) = @_;
663
664         my $item_id = $c->req->param("item");
665
666         # $c->session is a hash ref, a bit like $c->stash
667         # the difference is that it' preserved across requests
668
669         push @{ $c->session->{items} }, $item_id;
670
671         $c->forward("MyView");
672     }
673
674     sub display_items : Local {
675         my ( $self, $c ) = @_;
676
677         # values in $c->session are restored
678         $c->stash->{items_to_display} =
679           [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
680
681         $c->forward("MyView");
682     }
683
684 =head1 DESCRIPTION
685
686 The Session plugin is the base of two related parts of functionality required
687 for session management in web applications.
688
689 The first part, the State, is getting the browser to repeat back a session key,
690 so that the web application can identify the client and logically string
691 several requests together into a session.
692
693 The second part, the Store, deals with the actual storage of information about
694 the client. This data is stored so that the it may be revived for every request
695 made by the same client.
696
697 This plugin links the two pieces together.
698
699 =head1 RECOMENDED BACKENDS
700
701 =over 4
702
703 =item Session::State::Cookie
704
705 The only really sane way to do state is using cookies.
706
707 =item Session::Store::File
708
709 A portable backend, based on Cache::File.
710
711 =item Session::Store::FastMmap
712
713 A fast and flexible backend, based on Cache::FastMmap.
714
715 =back
716
717 =head1 METHODS
718
719 =over 4
720
721 =item sessionid
722
723 An accessor for the session ID value.
724
725 =item session
726
727 Returns a hash reference that might contain unserialized values from previous
728 requests in the same session, and whose modified value will be saved for future
729 requests.
730
731 This method will automatically create a new session and session ID if none
732 exists.
733
734 You can also set session keys by passing a list of key/value pairs or a
735 hashref.
736
737     $c->session->{foo} = "bar";      # This works.
738     $c->session(one => 1, two => 2); # And this.
739     $c->session({ answer => 42 });   # And this.
740
741 =item session_expires
742
743 This method returns the time when the current session will expire, or 0 if
744 there is no current session. If there is a session and it already expired, it
745 will delete the session and return 0 as well.
746
747 =item flash
748
749 This is like Ruby on Rails' flash data structure. Think of it as a stash that
750 lasts for longer than one request, letting you redirect instead of forward.
751
752 The flash data will be cleaned up only on requests on which actually use
753 $c->flash (thus allowing multiple redirections), and the policy is to delete
754 all the keys which haven't changed since the flash data was loaded at the end
755 of every request.
756
757 Note that use of the flash is an easy way to get data across requests, but
758 it's also strongly disrecommended, due it it being inherently plagued with
759 race conditions. This means that it's unlikely to work well if your
760 users have multiple tabs open at once, or if your site does a lot of AJAX
761 requests.
762
763 L<Catalyst::Plugin::StatusMessage> is the recommended alternative solution,
764 as this doesn't suffer from these issues.
765
766     sub moose : Local {
767         my ( $self, $c ) = @_;
768
769         $c->flash->{beans} = 10;
770         $c->response->redirect( $c->uri_for("foo") );
771     }
772
773     sub foo : Local {
774         my ( $self, $c ) = @_;
775
776         my $value = $c->flash->{beans};
777
778         # ...
779
780         $c->response->redirect( $c->uri_for("bar") );
781     }
782
783     sub bar : Local {
784         my ( $self, $c ) = @_;
785
786         if ( exists $c->flash->{beans} ) { # false
787
788         }
789     }
790
791 =item clear_flash
792
793 Zap all the keys in the flash regardless of their current state.
794
795 =item keep_flash @keys
796
797 If you want to keep a flash key for the next request too, even if it hasn't
798 changed, call C<keep_flash> and pass in the keys as arguments.
799
800 =item delete_session REASON
801
802 This method is used to invalidate a session. It takes an optional parameter
803 which will be saved in C<session_delete_reason> if provided.
804
805 NOTE: This method will B<also> delete your flash data.
806
807 =item session_delete_reason
808
809 This accessor contains a string with the reason a session was deleted. Possible
810 values include:
811
812 =over 4
813
814 =item *
815
816 C<address mismatch>
817
818 =item *
819
820 C<session expired>
821
822 =back
823
824 =item session_expire_key $key, $ttl
825
826 Mark a key to expire at a certain time (only useful when shorter than the
827 expiry time for the whole session).
828
829 For example:
830
831     __PACKAGE__->config('Plugin::Session' => { expires => 10000000000 }); # "forever"
832     (NB If this number is too large, Y2K38 breakage could result.)
833
834     # later
835
836     $c->session_expire_key( __user => 3600 );
837
838 Will make the session data survive, but the user will still be logged out after
839 an hour.
840
841 Note that these values are not auto extended.
842
843 =item change_session_id
844
845 By calling this method you can force a session id change while keeping all
846 session data. This method might come handy when you are paranoid about some
847 advanced variations of session fixation attack.
848
849 If you want to prevent this session fixation scenario:
850
851     0) let us have WebApp with anonymous and authenticated parts
852     1) a hacker goes to vulnerable WebApp and gets a real sessionid,
853        just by browsing anonymous part of WebApp
854     2) the hacker inserts (somehow) this values into a cookie in victim's browser
855     3) after the victim logs into WebApp the hacker can enter his/her session
856
857 you should call change_session_id in your login controller like this:
858
859       if ($c->authenticate( { username => $user, password => $pass } )) {
860         # login OK
861         $c->change_session_id;
862         ...
863       } else {
864         # login FAILED
865         ...
866       }
867
868 =item change_session_expires $expires
869
870 You can change the session expiration time for this session;
871
872     $c->change_session_expires( 4000 );
873
874 Note that this only works to set the session longer than the config setting.
875
876 =back
877
878 =head1 INTERNAL METHODS
879
880 =over 4
881
882 =item setup
883
884 This method is extended to also make calls to
885 C<check_session_plugin_requirements> and C<setup_session>.
886
887 =item check_session_plugin_requirements
888
889 This method ensures that a State and a Store plugin are also in use by the
890 application.
891
892 =item setup_session
893
894 This method populates C<< $c->config('Plugin::Session') >> with the default values
895 listed in L</CONFIGURATION>.
896
897 =item prepare_action
898
899 This method is extended.
900
901 Its only effect is if the (off by default) C<flash_to_stash> configuration
902 parameter is on - then it will copy the contents of the flash to the stash at
903 prepare time.
904
905 =item finalize_headers
906
907 This method is extended and will extend the expiry time before sending
908 the response.
909
910 =item finalize_body
911
912 This method is extended and will call finalize_session before the other
913 finalize_body methods run.  Here we persist the session data if a session exists.
914
915 =item initialize_session_data
916
917 This method will initialize the internal structure of the session, and is
918 called by the C<session> method if appropriate.
919
920 =item create_session_id
921
922 Creates a new session ID using C<generate_session_id> if there is no session ID
923 yet.
924
925 =item validate_session_id SID
926
927 Make sure a session ID is of the right format.
928
929 This currently ensures that the session ID string is any amount of case
930 insensitive hexadecimal characters.
931
932 =item generate_session_id
933
934 This method will return a string that can be used as a session ID. It is
935 supposed to be a reasonably random string with enough bits to prevent
936 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
937 MD5 or SHA-256, depending on the availability of these modules.
938
939 =item session_hash_seed
940
941 This method is actually rather internal to generate_session_id, but should be
942 overridable in case you want to provide more random data.
943
944 Currently it returns a concatenated string which contains:
945
946 =over 4
947
948 =item * A counter
949
950 =item * The current time
951
952 =item * One value from C<rand>.
953
954 =item * The stringified value of a newly allocated hash reference
955
956 =item * The stringified value of the Catalyst context object
957
958 =back
959
960 in the hopes that those combined values are entropic enough for most uses. If
961 this is not the case you can replace C<session_hash_seed> with e.g.
962
963     sub session_hash_seed {
964         open my $fh, "<", "/dev/random";
965         read $fh, my $bytes, 20;
966         close $fh;
967         return $bytes;
968     }
969
970 Or even more directly, replace C<generate_session_id>:
971
972     sub generate_session_id {
973         open my $fh, "<", "/dev/random";
974         read $fh, my $bytes, 20;
975         close $fh;
976         return unpack("H*", $bytes);
977     }
978
979 Also have a look at L<Crypt::Random> and the various openssl bindings - these
980 modules provide APIs for cryptographically secure random data.
981
982 =item finalize_session
983
984 Clean up the session during C<finalize>.
985
986 This clears the various accessors after saving to the store.
987
988 =item dump_these
989
990 See L<Catalyst/dump_these> - ammends the session data structure to the list of
991 dumped objects if session ID is defined.
992
993
994 =item calculate_extended_session_expires
995
996 =item calculate_initial_session_expires
997
998 =item create_session_id_if_needed
999
1000 =item delete_session_id
1001
1002 =item extend_session_expires
1003
1004 Note: this is *not* used to give an individual user a longer session. See
1005 'change_session_expires'.
1006
1007 =item extend_session_id
1008
1009 =item get_session_id
1010
1011 =item reset_session_expires
1012
1013 =item session_is_valid
1014
1015 =item set_session_id
1016
1017 =item initial_session_expires
1018
1019 =back
1020
1021 =head1 USING SESSIONS DURING PREPARE
1022
1023 The earliest point in time at which you may use the session data is after
1024 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
1025
1026 State plugins must set $c->session ID before C<prepare_action>, and during
1027 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
1028 the store.
1029
1030     sub prepare_action {
1031         my $c = shift;
1032
1033         # don't touch $c->session yet!
1034
1035         $c->NEXT::prepare_action( @_ );
1036
1037         $c->session;  # this is OK
1038         $c->sessionid; # this is also OK
1039     }
1040
1041 =head1 CONFIGURATION
1042
1043     $c->config('Plugin::Session' => {
1044         expires => 1234,
1045     });
1046
1047 All configuation parameters are provided in a hash reference under the
1048 C<Plugin::Session> key in the configuration hash.
1049
1050 =over 4
1051
1052 =item expires
1053
1054 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
1055 hours).
1056
1057 =item expiry_threshold
1058
1059 The time (in seconds) before the session expiration to update the
1060 expiration time (and thus the session).
1061
1062 The purpose of this is to keep the session store from being updated
1063 when nothing else in the session is updated.
1064
1065 Defaults to 0 (in which case, the expiration will always be updated).
1066
1067 =item verify_address
1068
1069 When true, C<< $c->request->address >> will be checked at prepare time. If it is
1070 not the same as the address that initiated the session, the session is deleted.
1071
1072 Defaults to false.
1073
1074 =item verify_user_agent
1075
1076 When true, C<< $c->request->user_agent >> will be checked at prepare time. If it
1077 is not the same as the user agent that initiated the session, the session is
1078 deleted.
1079
1080 Defaults to false.
1081
1082 =item flash_to_stash
1083
1084 This option makes it easier to have actions behave the same whether they were
1085 forwarded to or redirected to. On prepare time it copies the contents of
1086 C<flash> (if any) to the stash.
1087
1088 =back
1089
1090 =head1 SPECIAL KEYS
1091
1092 The hash reference returned by C<< $c->session >> contains several keys which
1093 are automatically set:
1094
1095 =over 4
1096
1097 =item __expires
1098
1099 This key no longer exists. Use C<session_expires> instead.
1100
1101 =item __updated
1102
1103 The last time a session was saved to the store.
1104
1105 =item __created
1106
1107 The time when the session was first created.
1108
1109 =item __address
1110
1111 The value of C<< $c->request->address >> at the time the session was created.
1112 This value is only populated if C<verify_address> is true in the configuration.
1113
1114 =item __user_agent
1115
1116 The value of C<< $c->request->user_agent >> at the time the session was created.
1117 This value is only populated if C<verify_user_agent> is true in the configuration.
1118
1119 =back
1120
1121 =head1 CAVEATS
1122
1123 =head2 Round the Robin Proxies
1124
1125 C<verify_address> could make your site inaccessible to users who are behind
1126 load balanced proxies. Some ISPs may give a different IP to each request by the
1127 same client due to this type of proxying. If addresses are verified these
1128 users' sessions cannot persist.
1129
1130 To let these users access your site you can either disable address verification
1131 as a whole, or provide a checkbox in the login dialog that tells the server
1132 that it's OK for the address of the client to change. When the server sees that
1133 this box is checked it should delete the C<__address> special key from the
1134 session hash when the hash is first created.
1135
1136 =head2 Race Conditions
1137
1138 In this day and age where cleaning detergents and Dutch football (not the
1139 American kind) teams roam the plains in great numbers, requests may happen
1140 simultaneously. This means that there is some risk of session data being
1141 overwritten, like this:
1142
1143 =over 4
1144
1145 =item 1.
1146
1147 request a starts, request b starts, with the same session ID
1148
1149 =item 2.
1150
1151 session data is loaded in request a
1152
1153 =item 3.
1154
1155 session data is loaded in request b
1156
1157 =item 4.
1158
1159 session data is changed in request a
1160
1161 =item 5.
1162
1163 request a finishes, session data is updated and written to store
1164
1165 =item 6.
1166
1167 request b finishes, session data is updated and written to store, overwriting
1168 changes by request a
1169
1170 =back
1171
1172 For applications where any given user's session is only making one request
1173 at a time this plugin should be safe enough.
1174
1175 =head1 AUTHORS
1176
1177 Andy Grundman
1178
1179 Christian Hansen
1180
1181 Yuval Kogman, C<nothingmuch@woobling.org>
1182
1183 Sebastian Riedel
1184
1185 Tomas Doran (t0m) C<bobtfish@bobtfish.net> (current maintainer)
1186
1187 Sergio Salvi
1188
1189 kmx C<kmx@volny.cz>
1190
1191 Florian Ragwitz (rafl) C<rafl@debian.org>
1192
1193 Kent Fredric (kentnl)
1194
1195 And countless other contributers from #catalyst. Thanks guys!
1196
1197 =head1 Contributors
1198
1199 Devin Austin (dhoss) <dhoss@cpan.org>
1200
1201 =head1 COPYRIGHT & LICENSE
1202
1203     Copyright (c) 2005 the aforementioned authors. All rights
1204     reserved. This program is free software; you can redistribute
1205     it and/or modify it under the same terms as Perl itself.
1206
1207 =cut
1208
1209