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