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