Make author tests properly author only
[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
9e447f9d 1004=back
1005
a92c8aeb 1006=head1 USING SESSIONS DURING PREPARE
1007
1008The earliest point in time at which you may use the session data is after
1009L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
1010
1011State plugins must set $c->session ID before C<prepare_action>, and during
1012C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
1013the store.
1014
7048c24e 1015 sub prepare_action {
1016 my $c = shift;
a92c8aeb 1017
7048c24e 1018 # don't touch $c->session yet!
b1cd7d77 1019
7048c24e 1020 $c->NEXT::prepare_action( @_ );
a92c8aeb 1021
7048c24e 1022 $c->session; # this is OK
1023 $c->sessionid; # this is also OK
1024 }
a92c8aeb 1025
9e447f9d 1026=head1 CONFIGURATION
1027
064c3709 1028 $c->config('Plugin::Session' => {
229a5b53 1029 expires => 1234,
064c3709 1030 });
9e447f9d 1031
1032All configuation parameters are provided in a hash reference under the
064c3709 1033C<Plugin::Session> key in the configuration hash.
9e447f9d 1034
1035=over 4
1036
1037=item expires
1038
1039The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
1040hours).
1041
1042=item verify_address
1043
8c7e922c 1044When true, C<<$c->request->address>> will be checked at prepare time. If it is
1045not the same as the address that initiated the session, the session is deleted.
9e447f9d 1046
33641c1d 1047Defaults to false.
1048
06c621b5 1049=item verify_user_agent
1050
1051When true, C<<$c->request->user_agent>> will be checked at prepare time. If it
af1e4bc8 1052is not the same as the user agent that initiated the session, the session is
06c621b5 1053deleted.
1054
1055Defaults to false.
1056
68fd02ae 1057=item flash_to_stash
1058
1059This option makes it easier to have actions behave the same whether they were
1060forwarded to or redirected to. On prepare time it copies the contents of
1061C<flash> (if any) to the stash.
1062
9e447f9d 1063=back
1064
1065=head1 SPECIAL KEYS
1066
1067The hash reference returned by C<< $c->session >> contains several keys which
1068are automatically set:
1069
1070=over 4
1071
1072=item __expires
1073
ab634fee 1074This key no longer exists. Use C<session_expires> instead.
9e447f9d 1075
1076=item __updated
1077
d44bc687 1078The last time a session was saved to the store.
9e447f9d 1079
1080=item __created
1081
1082The time when the session was first created.
1083
1084=item __address
1085
1086The value of C<< $c->request->address >> at the time the session was created.
8c7e922c 1087This value is only populated if C<verify_address> is true in the configuration.
9e447f9d 1088
06c621b5 1089=item __user_agent
1090
e79a686c 1091The value of C<< $c->request->user_agent >> at the time the session was created.
06c621b5 1092This value is only populated if C<verify_user_agent> is true in the configuration.
1093
9e447f9d 1094=back
1095
c80e9f04 1096=head1 CAVEATS
1097
a552e4b5 1098=head2 Round the Robin Proxies
1099
c80e9f04 1100C<verify_address> could make your site inaccessible to users who are behind
1101load balanced proxies. Some ISPs may give a different IP to each request by the
1102same client due to this type of proxying. If addresses are verified these
1103users' sessions cannot persist.
1104
1105To let these users access your site you can either disable address verification
1106as a whole, or provide a checkbox in the login dialog that tells the server
1107that it's OK for the address of the client to change. When the server sees that
bfa4f9cc 1108this box is checked it should delete the C<__address> special key from the
c80e9f04 1109session hash when the hash is first created.
1110
a552e4b5 1111=head2 Race Conditions
1112
bfa4f9cc 1113In this day and age where cleaning detergents and Dutch football (not the
1114American kind) teams roam the plains in great numbers, requests may happen
a552e4b5 1115simultaneously. This means that there is some risk of session data being
1116overwritten, like this:
1117
1118=over 4
1119
1120=item 1.
1121
bfa4f9cc 1122request a starts, request b starts, with the same session ID
a552e4b5 1123
1124=item 2.
1125
1126session data is loaded in request a
1127
1128=item 3.
1129
1130session data is loaded in request b
1131
1132=item 4.
1133
1134session data is changed in request a
1135
1136=item 5.
1137
1138request a finishes, session data is updated and written to store
1139
1140=item 6.
1141
1142request b finishes, session data is updated and written to store, overwriting
1143changes by request a
1144
1145=back
1146
b6dae3d3 1147For applications where any given user's session is only making one request
1148at a time this plugin should be safe enough.
a552e4b5 1149
d45028d6 1150=head1 AUTHORS
1151
7048c24e 1152Andy Grundman
baa9db9c 1153
7048c24e 1154Christian Hansen
baa9db9c 1155
2842d938 1156Yuval Kogman, C<nothingmuch@woobling.org>
baa9db9c 1157
7048c24e 1158Sebastian Riedel
baa9db9c 1159
2842d938 1160Tomas Doran (t0m) C<bobtfish@bobtfish.net> (current maintainer)
1161
1162Sergio Salvi
1163
af1e4bc8 1164kmx C<kmx@volny.cz>
1165
836b0a11 1166Florian Ragwitz (rafl) C<rafl@debian.org>
1167
1168Kent Fredric (kentnl)
1169
baa9db9c 1170And countless other contributers from #catalyst. Thanks guys!
d45028d6 1171
1cc09185 1172=head1 Contributors
1173
1174Devin Austin (dhoss) <dhoss@cpan.org>
1175
cc40ae4b 1176=head1 COPYRIGHT & LICENSE
d45028d6 1177
7048c24e 1178 Copyright (c) 2005 the aforementioned authors. All rights
1179 reserved. This program is free software; you can redistribute
1180 it and/or modify it under the same terms as Perl itself.
d45028d6 1181
9e447f9d 1182=cut
1183
1184