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