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