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