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