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