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