package MasonX::Request::WithMultiSession; use strict; use Digest::SHA1 (); use Time::HiRes; use base qw(MasonX::Request::WithApacheSession); use HTML::Mason::Exceptions ( abbr => [ qw( param_error error ) ] ); use Params::Validate qw( validate SCALAR ); Params::Validate::validation_options( on_fail => sub { param_error( join '', @_ ) } ); __PACKAGE__->valid_params ( multi_session_args_param => { type => SCALAR, default => 'sub_session_id', descr => 'The parameter name which contains the sub-session id', }, multi_session_expire => { type => Params::Validate::SCALAR, default => undef, descr => 'How long a sub-session stays valid', }, ); sub session { my $self = shift; return $self->parent_request->session(@_) if $self->is_subrequest; my %p = @_; my %super_p = exists $p{session_id} ? ( session_id => $p{session_id} ) : (); my $session = $self->SUPER::session(%super_p); my %sub_session_p = exists $p{sub_session_id} ? ( sub_session_id => $p{sub_session_id} ) : (); my $id = $self->sub_session_id(%sub_session_p); if ( $p{clone} || $p{new} ) { # forces creation of a new id delete $self->{sub_session_id}; my $new_id = $self->_make_new_sub_session_id; if ( $p{clone} ) { # shallow copy of old session $session->{sub_sessions}{$new_id} = { %{ $session->{sub_sessions}{$id} } }; } $id = $new_id; } $session->{sub_session_ids}{$id} = int(time); return $session->{sub_sessions}{$id}; } sub sub_session_id { my $self = shift; my %p = validate( @_, { sub_session_id => { type => SCALAR, optional => 1, }, } ); unless ( exists $self->{sub_session_id} ) { my $args = $self->request_args; my $args_key = $self->{multi_session_args_param}; my $session = $self->SUPER::session; if ( exists $p{sub_session_id} ) { unless ( exists $session->{sub_session_ids}{ $p{sub_session_id} } ) { $session->{sub_sessions}{ $p{sub_session_id} } = {}; } $self->{sub_session_id} = $p{sub_session_id}; } elsif ( exists $args->{$args_key} && exists $session->{sub_session_ids}{ $args->{$args_key} } ) { $self->{sub_session_id} = $args->{$args_key}; } else { $self->_make_new_sub_session_id; } } return $self->{sub_session_id}; } sub _make_new_sub_session_id { my $self = shift; my $session = $self->SUPER::session; my $new_id; do { # using Time::HiRes means that we get times with very high # floating point resolutions (to 10 or 11 decimal places), so # this is a good seed for a hashing algorithm $new_id = Digest::SHA1::sha1_hex( time() . {} . rand() . $$ ); } while ( exists $session->{sub_session_ids}{$new_id} ); $session->{sub_sessions}{$new_id} = {}; $self->{sub_session_id} = $new_id; return $new_id; } sub delete_sub_session { my $self = shift; my $session = $self->SUPER::session; my %p = validate( @_, { sub_session_id => { type => SCALAR, optional => 1, }, } ); my $sub_id = $p{sub_session_id} ? $p{sub_session_id} : delete $self->{sub_session_id}; delete $session->{sub_sessions}{$sub_id}; delete $session->{sub_session_ids}{$sub_id}; } sub delete_session { my $self = shift; $self->SUPER::delete_session; delete $self->{sub_session_id}; } sub DESTROY { my $self = shift; return unless defined $self->{multi_session_expire}; my $session = $self->SUPER::session; my $cutoff = int(time) - $self->{multi_session_expire}; foreach my $id ( keys %{ $session->{sub_session_ids} } ) { if ( $session->{sub_session_ids}{$id} < $cutoff ) { delete $session->{sub_sessions}{$id}; delete $session->{sub_session_ids}{$id}; } } } 1; __END__ =head1 NAME MasonX::Request::WithMultiSession - Multiple sub-sessions within one "parent" session =head1 SYNOPSIS PerlSetVar MasonRequestClass MasonX::Request::WithMultiSession =head1 DESCRIPTION This module subclasses C in order to allow multiple "sub-sessions" to exist within one parent session. This can be quite useful for a web app where you want to allow the user to open multiple windows, each with a different session, but session ids are stored in a cookie. Like C, sub-sessions are shared between a request and any subrequests it creates. =head1 METHODS This class has an interface quite similar to that of C. =over 4 =item * session The primary interface to this class is through the C method. When this method is called without any parameters, the module looks for an existing sub-session specified by the sub-session id argument parameter (which can be in a query string or POST). This value can be overridden by explicitly passing a "sub_session_id" parameter. If this parameter is found, an existing sub-session is returned. If this parameter is not found, a new sub-session is created. If the C method is called as C<< session( clone => 1 ) >> then a new sub-session will be created, and its contents will be the same as that of the current sub-session. This is a shallow copy of the old session hash, so objects and references are shared between them. If C<< session( new => 1 ) >> is called, then a new, empty, sub-session is created. You can specify the main session id to use via the "session_id" parameter. =item * sub_session_id This method returns the currently active sub-session's id. Use this method to put this id into URL parameters, forms, etc. as needed. If given a "sub_session_id" parameter, it will set the current sub-session id. =item * delete_sub_session By default, this simply defaults the current sub-session. You can pass a "sub_session_id" parameter to delete a specific session. =back =head2 Parameters This module takes two parameters besides those inherited from C: =over 4 =item * multi_session_args_param / MultiSessionArgsParam This parameter can be used to specify which parameter contains the sub-session id. By default, the module will look for a parameter called "sub_session_id". =item * multi_session_expire / MultiSessionExpire This parameter specifies the number of seconds after a sub-session is accessed until it is purged. If not specified, then sub-sessions are never purged. Sub-sessions expiration is checked when the request object goes out of scope. =back =head1 USAGE You will need to manually set the sub-session id argument parameter for each request. The easiest way to do this is to make sure that all URLs contain the sub-session id. This can be done by using a C<< <%filter> >> block in a top-level autohandler (although this won't catch redirects), or by making sure all URLs are generated by a single component/function. =head1 SUPPORT Bug reports and requests for help should be sent to the mason-users list. See http://www.masonhq.com/resources/mailing_lists.html for more details. =head1 AUTHOR Dave Rolsky, Development funded by Marigold Technologies. =head1 SEE ALSO HTML::Mason =cut