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