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