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