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