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