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