Checking in changes prior to tagging of version 0.23. 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
e79a686c 16our $VERSION = '0.23';
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
4207ce8d 83 if ( $c->config->{session}{flash_to_stash}
e5b2372a 84 and $c->sessionid
4207ce8d 85 and my $flash_data = $c->flash )
86 {
19c130c2 87 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
88 }
89
fe0d5ebe 90 $c->maybe::next::method(@_);
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 }
1dbf4cd5 171
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 });
e5b2372a 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
9e447f9d 291sub delete_session {
9a9252c2 292 my ( $c, $msg ) = @_;
9e447f9d 293
49727697 294 $c->log->debug("Deleting session" . ( defined($msg) ? "($msg)" : '(no reason given)') ) if $c->debug;
b92ce604 295
9a9252c2 296 # delete the session data
b92ce604 297 if ( my $sid = $c->sessionid ) {
298 $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
c6eaa93a 299 $c->delete_session_id($sid);
b92ce604 300 }
e5b2372a 301
9a9252c2 302 # reset the values in the context object
ab634fee 303 # see the BEGIN block
260b14c4 304 $c->_clear_session_instance_data;
6687905d 305
29d15411 306 $c->_session_delete_reason($msg);
307}
308
309sub session_delete_reason {
310 my $c = shift;
311
e5b2372a 312 $c->session_is_valid; # check that it was loaded
29d15411 313
4207ce8d 314 $c->_session_delete_reason(@_);
9e447f9d 315}
316
6687905d 317sub session_expires {
e5b2372a 318 my $c = shift;
6687905d 319
260b14c4 320 if ( defined( my $expires = $c->_extended_session_expires ) ) {
e5b2372a 321 return $expires;
1dbf4cd5 322 } elsif ( defined( $expires = $c->_load_session_expires ) ) {
260b14c4 323 return $c->extend_session_expires( $expires );
e5b2372a 324 } else {
1dbf4cd5 325 return 0;
e5b2372a 326 }
327}
6687905d 328
e5b2372a 329sub extend_session_expires {
330 my ( $c, $expires ) = @_;
260b14c4 331 $c->_extended_session_expires( my $updated = $c->calculate_extended_session_expires( $expires ) );
4c79feb6 332 $c->extend_session_id( $c->sessionid, $updated );
e5b2372a 333 return $updated;
334}
6687905d 335
e5b2372a 336sub calculate_initial_session_expires {
337 my $c = shift;
338 return ( time() + $c->config->{session}{expires} );
339}
340
341sub calculate_extended_session_expires {
342 my ( $c, $prev ) = @_;
343 $c->calculate_initial_session_expires;
344}
345
346sub reset_session_expires {
347 my ( $c, $sid ) = @_;
260b14c4 348
349 my $exp = $c->calculate_initial_session_expires;
350 $c->_session_expires( $exp );
351 $c->_extended_session_expires( $exp );
e5b2372a 352 $exp;
6687905d 353}
354
0974ac06 355sub sessionid {
4207ce8d 356 my $c = shift;
e5b2372a 357
1dbf4cd5 358 return $c->_sessionid || $c->_load_sessionid;
359}
360
361sub _load_sessionid {
362 my $c = shift;
363 return if $c->_tried_loading_session_id;
364 $c->_tried_loading_session_id(1);
365
366 if ( defined( my $sid = $c->get_session_id ) ) {
367 if ( $c->validate_session_id($sid) ) {
ec299c02 368 # temporarily set the inner key, so that validation will work
369 $c->_sessionid($sid);
1dbf4cd5 370 return $sid;
371 } else {
372 my $err = "Tried to set invalid session ID '$sid'";
373 $c->log->error($err);
374 Catalyst::Exception->throw($err);
4207ce8d 375 }
376 }
1dbf4cd5 377
378 return;
e5b2372a 379}
380
381sub session_is_valid {
382 my $c = shift;
4207ce8d 383
b92ce604 384 # force a check for expiry, but also __address, etc
385 if ( $c->_load_session ) {
e5b2372a 386 return 1;
387 } else {
388 return;
389 }
0974ac06 390}
391
392sub validate_session_id {
4207ce8d 393 my ( $c, $sid ) = @_;
0974ac06 394
4207ce8d 395 $sid and $sid =~ /^[a-f\d]+$/i;
0974ac06 396}
397
9e447f9d 398sub session {
9a9252c2 399 my $c = shift;
9e447f9d 400
29d15411 401 $c->_session || $c->_load_session || do {
1dbf4cd5 402 $c->create_session_id_if_needed;
29d15411 403 $c->initialize_session_data;
4207ce8d 404 };
9e447f9d 405}
406
f4d79f85 407sub keep_flash {
408 my ( $c, @keys ) = @_;
2e412459 409 my $href = $c->_flash_keep_keys || $c->_flash_keep_keys({});
410 (@{$href}{@keys}) = ((undef) x @keys);
f4d79f85 411}
412
c9396824 413sub _flash_data {
873f7011 414 my $c = shift;
78476ce0 415 $c->_flash || $c->_load_flash || do {
1dbf4cd5 416 $c->create_session_id_if_needed;
78476ce0 417 $c->_flash( {} );
c9396824 418 };
419}
420
421sub _set_flash {
422 my $c = shift;
423 if (@_) {
424 my $items = @_ > 1 ? {@_} : $_[0];
425 croak('flash takes a hash or hashref') unless ref $items;
426 @{ $c->_flash }{ keys %$items } = values %$items;
1dbf4cd5 427 }
873f7011 428}
429
c9396824 430sub flash {
431 my $c = shift;
432 $c->_flash_data;
f6009cac 433 $c->_set_flash(@_);
434 return $c->_flash;
c9396824 435}
436
49727697 437sub clear_flash {
438 my $c = shift;
5a1f6ed4 439
440 #$c->delete_session_data("flash:" . $c->sessionid); # should this be in here? or delayed till finalization?
49727697 441 $c->_flash_key_hashes({});
5a1f6ed4 442 $c->_flash_keep_keys({});
49727697 443 $c->_flash({});
444}
445
b7acf64e 446sub session_expire_key {
447 my ( $c, %keys ) = @_;
448
449 my $now = time;
4207ce8d 450 @{ $c->session->{__expire_keys} }{ keys %keys } =
451 map { $now + $_ } values %keys;
b7acf64e 452}
453
9e447f9d 454sub initialize_session_data {
9a9252c2 455 my $c = shift;
9e447f9d 456
9a9252c2 457 my $now = time;
9e447f9d 458
4207ce8d 459 return $c->_session(
460 {
461 __created => $now,
462 __updated => $now,
463
464 (
465 $c->config->{session}{verify_address}
466 ? ( __address => $c->request->address )
467 : ()
468 ),
06c621b5 469 (
470 $c->config->{session}{verify_user_agent}
471 ? ( __user_agent => $c->request->user_agent )
472 : ()
473 ),
4207ce8d 474 }
475 );
9e447f9d 476}
477
9e447f9d 478sub generate_session_id {
479 my $c = shift;
480
481 my $digest = $c->_find_digest();
482 $digest->add( $c->session_hash_seed() );
483 return $digest->hexdigest;
484}
485
1dbf4cd5 486sub create_session_id_if_needed {
78476ce0 487 my $c = shift;
1dbf4cd5 488 $c->create_session_id unless $c->sessionid;
489}
78476ce0 490
1dbf4cd5 491sub create_session_id {
492 my $c = shift;
493
e5b2372a 494 my $sid = $c->generate_session_id;
78476ce0 495
e5b2372a 496 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
78476ce0 497
e5b2372a 498 $c->_sessionid($sid);
499 $c->reset_session_expires;
500 $c->set_session_id($sid);
501
502 return $sid;
78476ce0 503}
504
9e447f9d 505my $counter;
9a9252c2 506
9e447f9d 507sub session_hash_seed {
9a9252c2 508 my $c = shift;
509
510 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
9e447f9d 511}
512
513my $usable;
9a9252c2 514
9e447f9d 515sub _find_digest () {
9a9252c2 516 unless ($usable) {
d44bc687 517 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
4207ce8d 518 if ( eval { Digest->new($alg) } ) {
5faaa4b0 519 $usable = $alg;
520 last;
521 }
7d139eeb 522 }
4207ce8d 523 Catalyst::Exception->throw(
9a9252c2 524 "Could not find a suitable Digest module. Please install "
4207ce8d 525 . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
526 unless $usable;
9a9252c2 527 }
9e447f9d 528
529 return Digest->new($usable);
530}
531
99b2191e 532sub dump_these {
533 my $c = shift;
534
535 (
fe0d5ebe 536 $c->maybe::next::method(),
99b2191e 537
538 $c->sessionid
539 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
540 : ()
541 );
542}
543
e5b2372a 544
fe0d5ebe 545sub get_session_id { shift->maybe::next::method(@_) }
546sub set_session_id { shift->maybe::next::method(@_) }
547sub delete_session_id { shift->maybe::next::method(@_) }
548sub extend_session_id { shift->maybe::next::method(@_) }
e5b2372a 549
9e447f9d 550__PACKAGE__;
551
552__END__
553
554=pod
555
556=head1 NAME
557
7048c24e 558Catalyst::Plugin::Session - Generic Session plugin - ties together server side storage and client side state required to maintain session data.
9e447f9d 559
560=head1 SYNOPSIS
561
8f0b4c16 562 # To get sessions to "just work", all you need to do is use these plugins:
563
564 use Catalyst qw/
565 Session
566 Session::Store::FastMmap
567 Session::State::Cookie
568 /;
569
7048c24e 570 # you can replace Store::FastMmap with Store::File - both have sensible
571 # default configurations (see their docs for details)
8f0b4c16 572
7048c24e 573 # more complicated backends are available for other scenarios (DBI storage,
574 # etc)
8f0b4c16 575
576
577 # after you've loaded the plugins you can save session data
578 # For example, if you are writing a shopping cart, it could be implemented
579 # like this:
9e447f9d 580
229a5b53 581 sub add_item : Local {
582 my ( $self, $c ) = @_;
583
584 my $item_id = $c->req->param("item");
585
8f0b4c16 586 # $c->session is a hash ref, a bit like $c->stash
587 # the difference is that it' preserved across requests
229a5b53 588
589 push @{ $c->session->{items} }, $item_id;
590
591 $c->forward("MyView");
592 }
593
594 sub display_items : Local {
595 my ( $self, $c ) = @_;
596
597 # values in $c->session are restored
598 $c->stash->{items_to_display} =
8f0b4c16 599 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
229a5b53 600
601 $c->forward("MyView");
602 }
603
9e447f9d 604=head1 DESCRIPTION
605
606The Session plugin is the base of two related parts of functionality required
607for session management in web applications.
608
609The first part, the State, is getting the browser to repeat back a session key,
610so that the web application can identify the client and logically string
611several requests together into a session.
612
613The second part, the Store, deals with the actual storage of information about
614the client. This data is stored so that the it may be revived for every request
615made by the same client.
616
617This plugin links the two pieces together.
618
2a323f6f 619=head1 RECOMENDED BACKENDS
8f0b4c16 620
621=over 4
622
623=item Session::State::Cookie
624
625The only really sane way to do state is using cookies.
626
627=item Session::Store::File
628
629A portable backend, based on Cache::File.
630
631=item Session::Store::FastMmap
632
633A fast and flexible backend, based on Cache::FastMmap.
634
635=back
636
9e447f9d 637=head1 METHODS
638
639=over 4
640
641=item sessionid
642
643An accessor for the session ID value.
644
645=item session
646
647Returns a hash reference that might contain unserialized values from previous
648requests in the same session, and whose modified value will be saved for future
649requests.
650
651This method will automatically create a new session and session ID if none
652exists.
653
ab634fee 654=item session_expires
655
656=item session_expires $reset
657
658This method returns the time when the current session will expire, or 0 if
659there is no current session. If there is a session and it already expired, it
660will delete the session and return 0 as well.
661
662If the C<$reset> parameter is true, and there is a session ID the expiry time
663will be reset to the current time plus the time to live (see
664L</CONFIGURATION>). This is used when creating a new session.
665
07e714d2 666=item flash
667
668This is like Ruby on Rails' flash data structure. Think of it as a stash that
44ab6d1c 669lasts for longer than one request, letting you redirect instead of forward.
670
671The flash data will be cleaned up only on requests on which actually use
672$c->flash (thus allowing multiple redirections), and the policy is to delete
bf6bd311 673all the keys which haven't changed since the flash data was loaded at the end
674of every request.
07e714d2 675
676 sub moose : Local {
677 my ( $self, $c ) = @_;
678
679 $c->flash->{beans} = 10;
680 $c->response->redirect( $c->uri_for("foo") );
681 }
682
683 sub foo : Local {
684 my ( $self, $c ) = @_;
685
686 my $value = $c->flash->{beans};
687
688 # ...
689
690 $c->response->redirect( $c->uri_for("bar") );
691 }
692
693 sub bar : Local {
694 my ( $self, $c ) = @_;
695
696 if ( exists $c->flash->{beans} ) { # false
697
698 }
699 }
700
49727697 701=item clear_flash
702
703Zap all the keys in the flash regardless of their current state.
704
bf6bd311 705=item keep_flash @keys
706
1961343c 707If you want to keep a flash key for the next request too, even if it hasn't
bf6bd311 708changed, call C<keep_flash> and pass in the keys as arguments.
709
fffeb18f 710=item delete_session REASON
711
712This method is used to invalidate a session. It takes an optional parameter
713which will be saved in C<session_delete_reason> if provided.
714
eb250519 715NOTE: This method will B<also> delete your flash data.
716
9e447f9d 717=item session_delete_reason
718
719This accessor contains a string with the reason a session was deleted. Possible
720values include:
721
722=over 4
723
724=item *
725
726C<address mismatch>
727
728=item *
729
730C<session expired>
731
732=back
733
b7acf64e 734=item session_expire_key $key, $ttl
735
736Mark a key to expire at a certain time (only useful when shorter than the
737expiry time for the whole session).
738
739For example:
740
741 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
742
743 # later
744
745 $c->session_expire_key( __user => 3600 );
746
747Will make the session data survive, but the user will still be logged out after
748an hour.
749
750Note that these values are not auto extended.
751
8f0b4c16 752=back
753
10c72079 754=head1 INTERNAL METHODS
8f0b4c16 755
756=over 4
757
9e447f9d 758=item setup
759
760This method is extended to also make calls to
761C<check_session_plugin_requirements> and C<setup_session>.
762
763=item check_session_plugin_requirements
764
765This method ensures that a State and a Store plugin are also in use by the
766application.
767
768=item setup_session
769
770This method populates C<< $c->config->{session} >> with the default values
771listed in L</CONFIGURATION>.
772
773=item prepare_action
774
bfa4f9cc 775This method is extended.
68fd02ae 776
bfa4f9cc 777Its only effect is if the (off by default) C<flash_to_stash> configuration
68fd02ae 778parameter is on - then it will copy the contents of the flash to the stash at
779prepare time.
9e447f9d 780
ccc77553 781=item finalize_headers
9e447f9d 782
d3c97126 783This method is extended and will extend the expiry time before sending
784the response.
785
92eaec32 786=item finalize_body
d3c97126 787
92eaec32 788This method is extended and will call finalize_session before the other
789finalize_body methods run. Here we persist the session data if a session exists.
9e447f9d 790
9e447f9d 791=item initialize_session_data
792
793This method will initialize the internal structure of the session, and is
794called by the C<session> method if appropriate.
795
68fd02ae 796=item create_session_id
797
bfa4f9cc 798Creates a new session ID using C<generate_session_id> if there is no session ID
68fd02ae 799yet.
800
ab634fee 801=item validate_session_id SID
802
803Make sure a session ID is of the right format.
804
805This currently ensures that the session ID string is any amount of case
806insensitive hexadecimal characters.
807
229a5b53 808=item generate_session_id
809
810This method will return a string that can be used as a session ID. It is
811supposed to be a reasonably random string with enough bits to prevent
812collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
bfa4f9cc 813MD5 or SHA-256, depending on the availability of these modules.
229a5b53 814
815=item session_hash_seed
816
817This method is actually rather internal to generate_session_id, but should be
818overridable in case you want to provide more random data.
819
820Currently it returns a concatenated string which contains:
821
822=over 4
823
7048c24e 824=item * A counter
229a5b53 825
7048c24e 826=item * The current time
229a5b53 827
7048c24e 828=item * One value from C<rand>.
229a5b53 829
7048c24e 830=item * The stringified value of a newly allocated hash reference
229a5b53 831
7048c24e 832=item * The stringified value of the Catalyst context object
229a5b53 833
834=back
835
bfa4f9cc 836in the hopes that those combined values are entropic enough for most uses. If
229a5b53 837this is not the case you can replace C<session_hash_seed> with e.g.
838
839 sub session_hash_seed {
840 open my $fh, "<", "/dev/random";
841 read $fh, my $bytes, 20;
842 close $fh;
843 return $bytes;
844 }
845
846Or even more directly, replace C<generate_session_id>:
847
848 sub generate_session_id {
849 open my $fh, "<", "/dev/random";
850 read $fh, my $bytes, 20;
851 close $fh;
852 return unpack("H*", $bytes);
853 }
854
855Also have a look at L<Crypt::Random> and the various openssl bindings - these
856modules provide APIs for cryptographically secure random data.
857
8f236527 858=item finalize_session
859
860Clean up the session during C<finalize>.
861
862This clears the various accessors after saving to the store.
863
99b2191e 864=item dump_these
865
866See L<Catalyst/dump_these> - ammends the session data structure to the list of
867dumped objects if session ID is defined.
868
d3c97126 869
870=item calculate_extended_session_expires
871
872=item calculate_initial_session_expires
873
874=item create_session_id_if_needed
875
876=item delete_session_id
877
878=item extend_session_expires
879
880=item extend_session_id
881
882=item get_session_id
883
884=item reset_session_expires
885
886=item session_is_valid
887
888=item set_session_id
889
9e447f9d 890=back
891
a92c8aeb 892=head1 USING SESSIONS DURING PREPARE
893
894The earliest point in time at which you may use the session data is after
895L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
896
897State plugins must set $c->session ID before C<prepare_action>, and during
898C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
899the store.
900
7048c24e 901 sub prepare_action {
902 my $c = shift;
a92c8aeb 903
7048c24e 904 # don't touch $c->session yet!
b1cd7d77 905
7048c24e 906 $c->NEXT::prepare_action( @_ );
a92c8aeb 907
7048c24e 908 $c->session; # this is OK
909 $c->sessionid; # this is also OK
910 }
a92c8aeb 911
9e447f9d 912=head1 CONFIGURATION
913
229a5b53 914 $c->config->{session} = {
915 expires => 1234,
916 };
9e447f9d 917
918All configuation parameters are provided in a hash reference under the
919C<session> key in the configuration hash.
920
921=over 4
922
923=item expires
924
925The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
926hours).
927
928=item verify_address
929
8c7e922c 930When true, C<<$c->request->address>> will be checked at prepare time. If it is
931not the same as the address that initiated the session, the session is deleted.
9e447f9d 932
33641c1d 933Defaults to false.
934
06c621b5 935=item verify_user_agent
936
937When true, C<<$c->request->user_agent>> will be checked at prepare time. If it
938is not the same as the user agent that initiated the session, the session is
939deleted.
940
941Defaults to false.
942
68fd02ae 943=item flash_to_stash
944
945This option makes it easier to have actions behave the same whether they were
946forwarded to or redirected to. On prepare time it copies the contents of
947C<flash> (if any) to the stash.
948
9e447f9d 949=back
950
951=head1 SPECIAL KEYS
952
953The hash reference returned by C<< $c->session >> contains several keys which
954are automatically set:
955
956=over 4
957
958=item __expires
959
ab634fee 960This key no longer exists. Use C<session_expires> instead.
9e447f9d 961
962=item __updated
963
d44bc687 964The last time a session was saved to the store.
9e447f9d 965
966=item __created
967
968The time when the session was first created.
969
970=item __address
971
972The value of C<< $c->request->address >> at the time the session was created.
8c7e922c 973This value is only populated if C<verify_address> is true in the configuration.
9e447f9d 974
06c621b5 975=item __user_agent
976
e79a686c 977The value of C<< $c->request->user_agent >> at the time the session was created.
06c621b5 978This value is only populated if C<verify_user_agent> is true in the configuration.
979
9e447f9d 980=back
981
c80e9f04 982=head1 CAVEATS
983
a552e4b5 984=head2 Round the Robin Proxies
985
c80e9f04 986C<verify_address> could make your site inaccessible to users who are behind
987load balanced proxies. Some ISPs may give a different IP to each request by the
988same client due to this type of proxying. If addresses are verified these
989users' sessions cannot persist.
990
991To let these users access your site you can either disable address verification
992as a whole, or provide a checkbox in the login dialog that tells the server
993that it's OK for the address of the client to change. When the server sees that
bfa4f9cc 994this box is checked it should delete the C<__address> special key from the
c80e9f04 995session hash when the hash is first created.
996
a552e4b5 997=head2 Race Conditions
998
bfa4f9cc 999In this day and age where cleaning detergents and Dutch football (not the
1000American kind) teams roam the plains in great numbers, requests may happen
a552e4b5 1001simultaneously. This means that there is some risk of session data being
1002overwritten, like this:
1003
1004=over 4
1005
1006=item 1.
1007
bfa4f9cc 1008request a starts, request b starts, with the same session ID
a552e4b5 1009
1010=item 2.
1011
1012session data is loaded in request a
1013
1014=item 3.
1015
1016session data is loaded in request b
1017
1018=item 4.
1019
1020session data is changed in request a
1021
1022=item 5.
1023
1024request a finishes, session data is updated and written to store
1025
1026=item 6.
1027
1028request b finishes, session data is updated and written to store, overwriting
1029changes by request a
1030
1031=back
1032
bfa4f9cc 1033If this is a concern in your application, a soon-to-be-developed locking
a552e4b5 1034solution is the only safe way to go. This will have a bigger overhead.
1035
1036For applications where any given user is only making one request at a time this
1037plugin should be safe enough.
1038
d45028d6 1039=head1 AUTHORS
1040
7048c24e 1041Andy Grundman
baa9db9c 1042
7048c24e 1043Christian Hansen
baa9db9c 1044
2842d938 1045Yuval Kogman, C<nothingmuch@woobling.org>
baa9db9c 1046
7048c24e 1047Sebastian Riedel
baa9db9c 1048
2842d938 1049Tomas Doran (t0m) C<bobtfish@bobtfish.net> (current maintainer)
1050
1051Sergio Salvi
1052
baa9db9c 1053And countless other contributers from #catalyst. Thanks guys!
d45028d6 1054
cc40ae4b 1055=head1 COPYRIGHT & LICENSE
d45028d6 1056
7048c24e 1057 Copyright (c) 2005 the aforementioned authors. All rights
1058 reserved. This program is free software; you can redistribute
1059 it and/or modify it under the same terms as Perl itself.
d45028d6 1060
9e447f9d 1061=cut
1062
1063