Document the new $c->flash "feature"
[catagits/Catalyst-Plugin-Session.git] / lib / Catalyst / Plugin / Session.pm
CommitLineData
9e447f9d 1#!/usr/bin/perl
2
3package Catalyst::Plugin::Session;
4use base qw/Class::Accessor::Fast/;
5
6use strict;
7use warnings;
8
9use NEXT;
10use Catalyst::Exception ();
9a9252c2 11use Digest ();
12use overload ();
d44bc687 13use Object::Signature ();
9e447f9d 14
d0e0cfb8 15our $VERSION = "0.04";
37160715 16
ab634fee 17my @session_data_accessors; # used in delete_session
9e447f9d 18BEGIN {
4207ce8d 19 __PACKAGE__->mk_accessors(
ab634fee 20 "_session_delete_reason",
21 @session_data_accessors = qw/
4207ce8d 22 _sessionid
23 _session
24 _session_expires
25 _session_data_sig
4207ce8d 26 _flash
27 _flash_stale_keys
28 /
29 );
9e447f9d 30}
31
32sub setup {
9a9252c2 33 my $c = shift;
34
35 $c->NEXT::setup(@_);
36
37 $c->check_session_plugin_requirements;
38 $c->setup_session;
39
40 return $c;
9e447f9d 41}
42
43sub check_session_plugin_requirements {
9a9252c2 44 my $c = shift;
9e447f9d 45
9a9252c2 46 unless ( $c->isa("Catalyst::Plugin::Session::State")
47 && $c->isa("Catalyst::Plugin::Session::Store") )
48 {
49 my $err =
50 ( "The Session plugin requires both Session::State "
51 . "and Session::Store plugins to be used as well." );
9e447f9d 52
9a9252c2 53 $c->log->fatal($err);
54 Catalyst::Exception->throw($err);
55 }
9e447f9d 56}
57
58sub setup_session {
9a9252c2 59 my $c = shift;
9e447f9d 60
9a9252c2 61 my $cfg = ( $c->config->{session} ||= {} );
9e447f9d 62
9a9252c2 63 %$cfg = (
64 expires => 7200,
65 verify_address => 1,
66 %$cfg,
67 );
9e447f9d 68
9a9252c2 69 $c->NEXT::setup_session();
9e447f9d 70}
71
19c130c2 72sub prepare_action {
73 my $c = shift;
74
4207ce8d 75 if ( $c->config->{session}{flash_to_stash}
76 and $c->_sessionid
77 and my $flash_data = $c->flash )
78 {
19c130c2 79 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
80 }
81
82 $c->NEXT::prepare_action(@_);
83}
84
9e447f9d 85sub finalize {
9a9252c2 86 my $c = shift;
9e447f9d 87
9b0fa2a6 88 $c->_save_session;
89 $c->_save_flash;
90
91 $c->NEXT::finalize(@_);
92}
93
94sub _save_session {
95 my $c = shift;
4207ce8d 96
ea972e9a 97 if ( my $sid = $c->_sessionid ) {
9e447f9d 98
6687905d 99 # all sessions are extended at the end of the request
100 my $now = time;
101
102 if ( my $expires = $c->session_expires ) {
103 $c->store_session_data( "expires:$sid" => $expires );
104 }
7a02371f 105
6687905d 106 if ( my $session_data = $c->_session ) {
d44bc687 107
108 no warnings 'uninitialized';
4207ce8d 109 if ( Object::Signature::signature($session_data) ne
110 $c->_session_data_sig )
111 {
d44bc687 112 $session_data->{__updated} = $now;
113 $c->store_session_data( "session:$sid" => $session_data );
114 }
ea972e9a 115 }
9a9252c2 116 }
9b0fa2a6 117}
9a9252c2 118
9b0fa2a6 119sub _save_flash {
120 my $c = shift;
121
ea972e9a 122 if ( my $sid = $c->_sessionid ) {
23fbca00 123 if ( my $flash_data = $c->_flash ) {
124 delete @{$flash_data}{ @{ $c->_flash_stale_keys || [] } };
4207ce8d 125
23fbca00 126 if (%$flash_data) {
127 $c->store_session_data( "flash:$sid", $flash_data );
128 }
129 else {
130 $c->delete_session_data("flash:$sid");
131 }
ea972e9a 132 }
9b0fa2a6 133 }
9e447f9d 134}
135
b7acf64e 136sub _load_session {
137 my $c = shift;
138
29d15411 139 if ( my $sid = $c->_sessionid ) {
6687905d 140 if ( $c->session_expires ) { # > 0
0974ac06 141
6687905d 142 my $session_data = $c->get_session_data("session:$sid");
143 $c->_session($session_data);
3f182468 144
6687905d 145 no warnings 'uninitialized'; # ne __address
146 if ( $c->config->{session}{verify_address}
147 && $session_data->{__address} ne $c->request->address )
148 {
149 $c->log->warn(
150 "Deleting session $sid due to address mismatch ("
151 . $session_data->{__address} . " != "
152 . $c->request->address . ")",
153 );
154 $c->delete_session("address mismatch");
155 return;
156 }
4207ce8d 157
6687905d 158 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
159 $c->_session_data_sig(
160 Object::Signature::signature($session_data) );
161 $c->_expire_session_keys;
4207ce8d 162
6687905d 163 return $session_data;
3f182468 164 }
9a9252c2 165 }
29d15411 166
4207ce8d 167 return;
b7acf64e 168}
9a9252c2 169
9b0fa2a6 170sub _load_flash {
171 my $c = shift;
172
173 if ( my $sid = $c->_sessionid ) {
4207ce8d 174 if ( my $flash_data = $c->_flash
175 || $c->_flash( $c->get_session_data("flash:$sid") ) )
176 {
177 $c->_flash_stale_keys( [ keys %$flash_data ] );
9b0fa2a6 178 return $flash_data;
179 }
180 }
181
182 return undef;
183}
184
d44bc687 185sub _expire_session_keys {
b7acf64e 186 my ( $c, $data ) = @_;
187
188 my $now = time;
189
4207ce8d 190 my $expiry = ( $data || $c->_session || {} )->{__expire_keys} || {};
191 foreach my $key ( grep { $expiry->{$_} < $now } keys %$expiry ) {
b7acf64e 192 delete $c->_session->{$key};
193 delete $expiry->{$key};
194 }
9e447f9d 195}
196
197sub delete_session {
9a9252c2 198 my ( $c, $msg ) = @_;
9e447f9d 199
9a9252c2 200 # delete the session data
29d15411 201 my $sid = $c->_sessionid || return;
4207ce8d 202 $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
9e447f9d 203
9a9252c2 204 # reset the values in the context object
ab634fee 205 # see the BEGIN block
206 $c->$_(undef) for @session_data_accessors;
6687905d 207
29d15411 208 $c->_session_delete_reason($msg);
209}
210
211sub session_delete_reason {
212 my $c = shift;
213
4207ce8d 214 $c->_load_session
215 if ( $c->_sessionid && !$c->_session ); # must verify session data
29d15411 216
4207ce8d 217 $c->_session_delete_reason(@_);
9e447f9d 218}
219
6687905d 220sub session_expires {
221 my ( $c, $should_create ) = @_;
222
223 $c->_session_expires || do {
224 if ( my $sid = $c->_sessionid ) {
225 my $now = time;
226
227 if ( !$should_create ) {
228 if ( ( $c->get_session_data("expires:$sid") || 0 ) < $now ) {
229
230 # session expired
231 $c->log->debug("Deleting session $sid (expired)")
232 if $c->debug;
233 $c->delete_session("session expired");
234 return 0;
235 }
236 }
237
238 return $c->_session_expires(
239 $now + $c->config->{session}{expires} );
240 }
241 };
242}
243
0974ac06 244sub sessionid {
4207ce8d 245 my $c = shift;
246
247 if (@_) {
248 if ( $c->validate_session_id( my $sid = shift ) ) {
249 $c->_sessionid($sid);
29d15411 250 return unless defined wantarray;
4207ce8d 251 }
252 else {
253 my $err = "Tried to set invalid session ID '$sid'";
254 $c->log->error($err);
255 Catalyst::Exception->throw($err);
256 }
257 }
258
259 $c->_load_session
260 if ( $c->_sessionid && !$c->_session ); # must verify session data
261
262 return $c->_sessionid;
0974ac06 263}
264
265sub validate_session_id {
4207ce8d 266 my ( $c, $sid ) = @_;
0974ac06 267
4207ce8d 268 $sid and $sid =~ /^[a-f\d]+$/i;
0974ac06 269}
270
9e447f9d 271sub session {
9a9252c2 272 my $c = shift;
9e447f9d 273
29d15411 274 $c->_session || $c->_load_session || do {
78476ce0 275 $c->create_session_id;
9e447f9d 276
29d15411 277 $c->initialize_session_data;
4207ce8d 278 };
9e447f9d 279}
280
873f7011 281sub flash {
282 my $c = shift;
78476ce0 283 $c->_flash || $c->_load_flash || do {
284 $c->create_session_id;
285 $c->_flash( {} );
4207ce8d 286 }
873f7011 287}
288
b7acf64e 289sub session_expire_key {
290 my ( $c, %keys ) = @_;
291
292 my $now = time;
4207ce8d 293 @{ $c->session->{__expire_keys} }{ keys %keys } =
294 map { $now + $_ } values %keys;
b7acf64e 295}
296
9e447f9d 297sub initialize_session_data {
9a9252c2 298 my $c = shift;
9e447f9d 299
9a9252c2 300 my $now = time;
9e447f9d 301
4207ce8d 302 return $c->_session(
303 {
304 __created => $now,
305 __updated => $now,
306
307 (
308 $c->config->{session}{verify_address}
309 ? ( __address => $c->request->address )
310 : ()
311 ),
312 }
313 );
9e447f9d 314}
315
9e447f9d 316sub generate_session_id {
317 my $c = shift;
318
319 my $digest = $c->_find_digest();
320 $digest->add( $c->session_hash_seed() );
321 return $digest->hexdigest;
322}
323
78476ce0 324sub create_session_id {
325 my $c = shift;
326
327 if ( !$c->_sessionid ) {
328 my $sid = $c->generate_session_id;
329
330 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
331
332 $c->sessionid($sid);
6687905d 333 $c->session_expires(1);
78476ce0 334 }
335}
336
9e447f9d 337my $counter;
9a9252c2 338
9e447f9d 339sub session_hash_seed {
9a9252c2 340 my $c = shift;
341
342 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
9e447f9d 343}
344
345my $usable;
9a9252c2 346
9e447f9d 347sub _find_digest () {
9a9252c2 348 unless ($usable) {
d44bc687 349 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
4207ce8d 350 if ( eval { Digest->new($alg) } ) {
5faaa4b0 351 $usable = $alg;
352 last;
353 }
7d139eeb 354 }
4207ce8d 355 Catalyst::Exception->throw(
9a9252c2 356 "Could not find a suitable Digest module. Please install "
4207ce8d 357 . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
358 unless $usable;
9a9252c2 359 }
9e447f9d 360
361 return Digest->new($usable);
362}
363
99b2191e 364sub dump_these {
365 my $c = shift;
366
367 (
368 $c->NEXT::dump_these(),
369
370 $c->sessionid
371 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
372 : ()
373 );
374}
375
9e447f9d 376__PACKAGE__;
377
378__END__
379
380=pod
381
382=head1 NAME
383
384Catalyst::Plugin::Session - Generic Session plugin - ties together server side
fb1a4ac3 385storage and client side state required to maintain session data.
9e447f9d 386
387=head1 SYNOPSIS
388
8f0b4c16 389 # To get sessions to "just work", all you need to do is use these plugins:
390
391 use Catalyst qw/
392 Session
393 Session::Store::FastMmap
394 Session::State::Cookie
395 /;
396
397 # you can replace Store::FastMmap with Store::File - both have sensible
398 # default configurations (see their docs for details)
399
400 # more complicated backends are available for other scenarios (DBI storage,
401 # etc)
402
403
404 # after you've loaded the plugins you can save session data
405 # For example, if you are writing a shopping cart, it could be implemented
406 # like this:
9e447f9d 407
229a5b53 408 sub add_item : Local {
409 my ( $self, $c ) = @_;
410
411 my $item_id = $c->req->param("item");
412
8f0b4c16 413 # $c->session is a hash ref, a bit like $c->stash
414 # the difference is that it' preserved across requests
229a5b53 415
416 push @{ $c->session->{items} }, $item_id;
417
418 $c->forward("MyView");
419 }
420
421 sub display_items : Local {
422 my ( $self, $c ) = @_;
423
424 # values in $c->session are restored
425 $c->stash->{items_to_display} =
8f0b4c16 426 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
229a5b53 427
428 $c->forward("MyView");
429 }
430
9e447f9d 431=head1 DESCRIPTION
432
433The Session plugin is the base of two related parts of functionality required
434for session management in web applications.
435
436The first part, the State, is getting the browser to repeat back a session key,
437so that the web application can identify the client and logically string
438several requests together into a session.
439
440The second part, the Store, deals with the actual storage of information about
441the client. This data is stored so that the it may be revived for every request
442made by the same client.
443
444This plugin links the two pieces together.
445
8f0b4c16 446=head1 RECCOMENDED BACKENDS
447
448=over 4
449
450=item Session::State::Cookie
451
452The only really sane way to do state is using cookies.
453
454=item Session::Store::File
455
456A portable backend, based on Cache::File.
457
458=item Session::Store::FastMmap
459
460A fast and flexible backend, based on Cache::FastMmap.
461
462=back
463
9e447f9d 464=head1 METHODS
465
466=over 4
467
468=item sessionid
469
470An accessor for the session ID value.
471
472=item session
473
474Returns a hash reference that might contain unserialized values from previous
475requests in the same session, and whose modified value will be saved for future
476requests.
477
478This method will automatically create a new session and session ID if none
479exists.
480
ab634fee 481=item session_expires
482
483=item session_expires $reset
484
485This method returns the time when the current session will expire, or 0 if
486there is no current session. If there is a session and it already expired, it
487will delete the session and return 0 as well.
488
489If the C<$reset> parameter is true, and there is a session ID the expiry time
490will be reset to the current time plus the time to live (see
491L</CONFIGURATION>). This is used when creating a new session.
492
07e714d2 493=item flash
494
495This is like Ruby on Rails' flash data structure. Think of it as a stash that
44ab6d1c 496lasts for longer than one request, letting you redirect instead of forward.
497
498The flash data will be cleaned up only on requests on which actually use
499$c->flash (thus allowing multiple redirections), and the policy is to delete
500all the keys which were present at the time the data was loaded just before the
501data is saved.
07e714d2 502
503 sub moose : Local {
504 my ( $self, $c ) = @_;
505
506 $c->flash->{beans} = 10;
507 $c->response->redirect( $c->uri_for("foo") );
508 }
509
510 sub foo : Local {
511 my ( $self, $c ) = @_;
512
513 my $value = $c->flash->{beans};
514
515 # ...
516
517 $c->response->redirect( $c->uri_for("bar") );
518 }
519
520 sub bar : Local {
521 my ( $self, $c ) = @_;
522
523 if ( exists $c->flash->{beans} ) { # false
524
525 }
526 }
527
9e447f9d 528=item session_delete_reason
529
530This accessor contains a string with the reason a session was deleted. Possible
531values include:
532
533=over 4
534
535=item *
536
537C<address mismatch>
538
539=item *
540
541C<session expired>
542
543=back
544
b7acf64e 545=item session_expire_key $key, $ttl
546
547Mark a key to expire at a certain time (only useful when shorter than the
548expiry time for the whole session).
549
550For example:
551
552 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
553
554 # later
555
556 $c->session_expire_key( __user => 3600 );
557
558Will make the session data survive, but the user will still be logged out after
559an hour.
560
561Note that these values are not auto extended.
562
8f0b4c16 563=back
564
10c72079 565=head1 INTERNAL METHODS
8f0b4c16 566
567=over 4
568
9e447f9d 569=item setup
570
571This method is extended to also make calls to
572C<check_session_plugin_requirements> and C<setup_session>.
573
574=item check_session_plugin_requirements
575
576This method ensures that a State and a Store plugin are also in use by the
577application.
578
579=item setup_session
580
581This method populates C<< $c->config->{session} >> with the default values
582listed in L</CONFIGURATION>.
583
584=item prepare_action
585
68fd02ae 586This methoid is extended.
587
588It's only effect is if the (off by default) C<flash_to_stash> configuration
589parameter is on - then it will copy the contents of the flash to the stash at
590prepare time.
9e447f9d 591
592=item finalize
593
594This method is extended and will extend the expiry time, as well as persist the
595session data if a session exists.
596
597=item delete_session REASON
598
599This method is used to invalidate a session. It takes an optional parameter
600which will be saved in C<session_delete_reason> if provided.
601
602=item initialize_session_data
603
604This method will initialize the internal structure of the session, and is
605called by the C<session> method if appropriate.
606
68fd02ae 607=item create_session_id
608
609Creates a new session id using C<generate_session_id> if there is no session ID
610yet.
611
ab634fee 612=item validate_session_id SID
613
614Make sure a session ID is of the right format.
615
616This currently ensures that the session ID string is any amount of case
617insensitive hexadecimal characters.
618
229a5b53 619=item generate_session_id
620
621This method will return a string that can be used as a session ID. It is
622supposed to be a reasonably random string with enough bits to prevent
623collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
624MD5 or SHA-256, depending on the availibility of these modules.
625
626=item session_hash_seed
627
628This method is actually rather internal to generate_session_id, but should be
629overridable in case you want to provide more random data.
630
631Currently it returns a concatenated string which contains:
632
633=over 4
634
635=item *
636
637A counter
638
639=item *
640
641The current time
642
643=item *
644
645One value from C<rand>.
646
647=item *
648
649The stringified value of a newly allocated hash reference
650
651=item *
652
653The stringified value of the Catalyst context object
654
655=back
656
657In the hopes that those combined values are entropic enough for most uses. If
658this is not the case you can replace C<session_hash_seed> with e.g.
659
660 sub session_hash_seed {
661 open my $fh, "<", "/dev/random";
662 read $fh, my $bytes, 20;
663 close $fh;
664 return $bytes;
665 }
666
667Or even more directly, replace C<generate_session_id>:
668
669 sub generate_session_id {
670 open my $fh, "<", "/dev/random";
671 read $fh, my $bytes, 20;
672 close $fh;
673 return unpack("H*", $bytes);
674 }
675
676Also have a look at L<Crypt::Random> and the various openssl bindings - these
677modules provide APIs for cryptographically secure random data.
678
99b2191e 679=item dump_these
680
681See L<Catalyst/dump_these> - ammends the session data structure to the list of
682dumped objects if session ID is defined.
683
9e447f9d 684=back
685
a92c8aeb 686=head1 USING SESSIONS DURING PREPARE
687
688The earliest point in time at which you may use the session data is after
689L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
690
691State plugins must set $c->session ID before C<prepare_action>, and during
692C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
693the store.
694
695 sub prepare_action {
696 my $c = shift;
697
698 # don't touch $c->session yet!
b1cd7d77 699
a92c8aeb 700 $c->NEXT::prepare_action( @_ );
701
702 $c->session; # this is OK
703 $c->sessionid; # this is also OK
704 }
705
9e447f9d 706=head1 CONFIGURATION
707
229a5b53 708 $c->config->{session} = {
709 expires => 1234,
710 };
9e447f9d 711
712All configuation parameters are provided in a hash reference under the
713C<session> key in the configuration hash.
714
715=over 4
716
717=item expires
718
719The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
720hours).
721
722=item verify_address
723
8c7e922c 724When true, C<<$c->request->address>> will be checked at prepare time. If it is
725not the same as the address that initiated the session, the session is deleted.
9e447f9d 726
68fd02ae 727=item flash_to_stash
728
729This option makes it easier to have actions behave the same whether they were
730forwarded to or redirected to. On prepare time it copies the contents of
731C<flash> (if any) to the stash.
732
9e447f9d 733=back
734
735=head1 SPECIAL KEYS
736
737The hash reference returned by C<< $c->session >> contains several keys which
738are automatically set:
739
740=over 4
741
742=item __expires
743
ab634fee 744This key no longer exists. Use C<session_expires> instead.
9e447f9d 745
746=item __updated
747
d44bc687 748The last time a session was saved to the store.
9e447f9d 749
750=item __created
751
752The time when the session was first created.
753
754=item __address
755
756The value of C<< $c->request->address >> at the time the session was created.
8c7e922c 757This value is only populated if C<verify_address> is true in the configuration.
9e447f9d 758
759=back
760
c80e9f04 761=head1 CAVEATS
762
a552e4b5 763=head2 Round the Robin Proxies
764
c80e9f04 765C<verify_address> could make your site inaccessible to users who are behind
766load balanced proxies. Some ISPs may give a different IP to each request by the
767same client due to this type of proxying. If addresses are verified these
768users' sessions cannot persist.
769
770To let these users access your site you can either disable address verification
771as a whole, or provide a checkbox in the login dialog that tells the server
772that it's OK for the address of the client to change. When the server sees that
773this box is checked it should delete the C<__address> sepcial key from the
774session hash when the hash is first created.
775
a552e4b5 776=head2 Race Conditions
777
778In this day and age where cleaning detergents and dutch football (not the
779american kind) teams roam the plains in great numbers, requests may happen
780simultaneously. This means that there is some risk of session data being
781overwritten, like this:
782
783=over 4
784
785=item 1.
786
787request a starts, request b starts, with the same session id
788
789=item 2.
790
791session data is loaded in request a
792
793=item 3.
794
795session data is loaded in request b
796
797=item 4.
798
799session data is changed in request a
800
801=item 5.
802
803request a finishes, session data is updated and written to store
804
805=item 6.
806
807request b finishes, session data is updated and written to store, overwriting
808changes by request a
809
810=back
811
812If this is a concern in your application, a soon to be developed locking
813solution is the only safe way to go. This will have a bigger overhead.
814
815For applications where any given user is only making one request at a time this
816plugin should be safe enough.
817
d45028d6 818=head1 AUTHORS
819
baa9db9c 820=over 4
821
822=item Andy Grundman
823
824=item Christian Hansen
825
826=item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
827
828=item Sebastian Riedel
829
830=back
831
832And countless other contributers from #catalyst. Thanks guys!
d45028d6 833
cc40ae4b 834=head1 COPYRIGHT & LICENSE
d45028d6 835
836 Copyright (c) 2005 the aforementioned authors. All rights
837 reserved. This program is free software; you can redistribute
838 it and/or modify it under the same terms as Perl itself.
839
9e447f9d 840=cut
841
842