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