diff --git a/lib/WebGUI/Session.pm b/lib/WebGUI/Session.pm index 8f7048453..65bff7eeb 100644 --- a/lib/WebGUI/Session.pm +++ b/lib/WebGUI/Session.pm @@ -19,7 +19,6 @@ use 5.010; use CHI; use File::Temp qw( tempdir ); -use Scalar::Util qw( weaken ); use HTTP::Message::PSGI; use HTTP::Request::Common; use WebGUI::Config; @@ -417,7 +416,10 @@ The epoch date when this user session will expire if it's not accessed again by sub get { my $self = shift; my $varName = shift; - return $self->{_var}{$varName}; + if ($varName) { + return $self->{_var}{$varName}; + } + return $self->{_var}; } #------------------------------------------------------------------- @@ -547,7 +549,7 @@ If you have a L env hash, you might find the sessionId at: $env->{'psgix.s =head3 noFuss -Uses simple session vars. See WebGUI::Session::Var::new() for more details. +Uses simple session vars. See WebGUI::Session->open() for more details. =cut diff --git a/lib/WebGUI/Session/Var.pm b/lib/WebGUI/Session/Var.pm deleted file mode 100644 index 6f611b1c0..000000000 --- a/lib/WebGUI/Session/Var.pm +++ /dev/null @@ -1,297 +0,0 @@ -package WebGUI::Session::Var; - -=head1 LEGAL - - ------------------------------------------------------------------- - WebGUI is Copyright 2001-2009 Plain Black Corporation. - ------------------------------------------------------------------- - Please read the legal notices (docs/legal.txt) and the license - (docs/license.txt) that came with this distribution before using - this software. - ------------------------------------------------------------------- - http://www.plainblack.com info@plainblack.com - ------------------------------------------------------------------- - -=cut - -use strict; -use Scalar::Util qw(weaken); - -=head1 NAME - -Package WebGUI::Session::Var - -=head1 DESCRIPTION - -This package is the persistence layer for WebGUI session variables. - -=head1 SYNOPSIS - -$var = WebGUI::Session::Var->new($session); - -$value = $var->get('lastIP'); - -$var->start; -$var->end; - -$boolean = $var->isAdminOn; - -$var->switchAdminOff; -$var->switchAdminOn; - - -=head1 METHODS - -These methods are available from this package: - -=cut - - -#------------------------------------------------------------------- - -=head2 end ( ) - -Removes the specified user session from memory and database. - -=cut - -sub end { - my $self = shift; - my $session = $self->session; - my $id = $self->getId; - $session->cache->remove($id); - $session->scratch->deleteAll; - $session->db->write("delete from userSession where sessionId=?",[$id]); - delete $session->{_user}; -} - -#------------------------------------------------------------------- - -=head2 get ( varName ) - -Retrieves the current value of a session variable. - -=head3 varName - -The name of the variable. - -=head4 lastIP - -The last IP address the user came from. - -=head4 lastPageView - -The epoch date of the last interaction with the session. - -=head4 userId - -The unique id of the user this session currently bound to. - -=head4 adminOn - -A boolean indicating whether this session has admin mode enabled or not. - -=head4 sessionId - -The sessionId associated with this session. - -=head4 expires - -The epoch date when this user session will expire if it's not accessed again by then. - -=cut - -sub get { - my $self = shift; - my $var = shift; - return $self->{_var}{$var}; -} - -#------------------------------------------------------------------- - -=head2 getId ( ) - -Returns the ID of the current session. - -=cut - -sub getId { - my $self = shift; - $self->get("sessionId"); -} - - -#------------------------------------------------------------------- - -=head2 isAdminOn ( ) - -Returns a boolean indicating whether admin mode is on or not. - -=cut - -sub isAdminOn { - my $self = shift; - return $self->get("adminOn"); -} - -#------------------------------------------------------------------- - -=head2 new ( session, sessionId, noFuss ) - -Constructor. Overwrites the sessionId of $session with its own id. Returns a var object. - -=head3 session - -A reference to the session. - -=head3 sessionId - -The specific sessionId you want to instantiate. - -=head3 noFuss - -A boolean, that if true will not update the session, or check if it's -expired. This is mainly for WebGUI session maintenance, and shouldn't -normally be used by anyone. - -=cut - -sub new { - my ($class, $session, $sessionId, $noFuss) = @_; - my $self = bless { _session => $session }, $class; - weaken $self->{_session}; - if ($sessionId eq "") { ##New session - $self->start(1); - } - else { ##existing session requested - $self->{_var} = $session->cache->get($sessionId); - unless ($self->{_var}{sessionId} eq $sessionId) { - $self->{_var} = $session->db->quickHashRef("select * from userSession where sessionId=?",[$sessionId]); - } - ##We have to make sure that the session variable has a sessionId, otherwise downstream users of - ##the object will break - if ($noFuss && $self->{_var}{sessionId}) { - $self->session->{_sessionId} = $self->{_var}{sessionId}; - return $self; - } - if ($self->{_var}{expires} && $self->{_var}{expires} < time()) { ##Session expired, start a new one with the same Id - $self->end; - $self->start(1,$sessionId); - } - elsif ($self->{_var}{sessionId} ne "") { ##Fetched an existing session. Update variables with recent data. - my $time = time(); - my $timeout = $session->setting->get("sessionTimeout"); - $self->{_var}{lastPageView} = $time; - $self->{_var}{lastIP} = $session->request->address; - $self->{_var}{expires} = $time + $timeout; - if ($self->{_var}{nextCacheFlush} > 0 && $self->{_var}{nextCacheFlush} < $time) { - delete $self->{_var}{nextCacheFlush}; - $session->db->setRow("userSession","sessionId",$self->{_var}); - } - else { - $self->{_var}{nextCacheFlush} = $time + $session->config->get("hotSessionFlushToDb"); - $session->cache->set($sessionId, $self->{_var}, $timeout); - } - $self->session->{_sessionId} = $self->{_var}{sessionId}; - return $self; - } - else { ##Start a new default session with the requested, non-existant id. - $self->start(1,$sessionId); - } - } - return $self; -} - - -#------------------------------------------------------------------- - -=head2 session ( ) - -Returns a reference to the session object. - -=cut - -sub session { - my $self = shift; - return $self->{_session}; -} - - -#------------------------------------------------------------------- - -=head2 start ( [ userId, sessionId ] ) - -Start a new user session. Returns the user session id. The session variable's sessionId -is set to the var object's session id. Also sets the user's CSRF token. - -=head3 userId - -The user id of the user to create a session for. Defaults to 1 (Visitor). - -=head3 sessionId - -Session id will be generated if not specified. In almost every case you should let the system generate the session id. - -=cut - -sub start { - my $self = shift; - my $userId = shift; - $userId = 1 if ($userId eq ""); - my $sessionId = shift; - my $session = $self->session; - my $id = $session->id; - $sessionId = $id->generate if ($sessionId eq ""); - my $timeout = $session->setting->get('sessionTimeout'); - my $time = time(); - $self->{_var} = { - expires => $time + $timeout, - lastPageView => $time, - lastIP => $session->request->address, - adminOn => 0, - userId => $userId - }; - $self->session->{_sessionId} = $sessionId; - $session->cache->set($sessionId, $self->{_var}, $timeout); - delete $self->{_var}{nextCacheFlush}; - $session->db->setRow("userSession","sessionId",$self->{_var},$sessionId); - $self->{_sessionId} = $sessionId; - $session->scratch->set('webguiCsrfToken', $id->generate); # create cross site request forgery token -} - -#------------------------------------------------------------------- - -=head2 switchAdminOff ( ) - -Disables admin mode. - -=cut - -sub switchAdminOff { - my $self = shift; - $self->{_var}{adminOn} = 0; - my $session = $self->session; - $session->cache->set($self->getId, $self->{_var}, $session->setting->get('sessionTimeout')); - delete $self->{_var}{nextCacheFlush}; - $session->db->setRow("userSession","sessionId", $self->{_var}); -} - -#------------------------------------------------------------------- - -=head2 switchAdminOn ( ) - -Enables admin mode. - -=cut - -sub switchAdminOn { - my $self = shift; - $self->{_var}{adminOn} = 1; - my $session = $self->session; - $session->cache->set($self->getId, $self->{_var}, $session->setting->get('sessionTimeout')); - delete $self->{_var}{nextCacheFlush}; - $self->session->db->setRow("userSession","sessionId", $self->{_var}); -} - - -1; diff --git a/t/Session.t b/t/Session.t index 822cabe32..810a97ab5 100644 --- a/t/Session.t +++ b/t/Session.t @@ -16,6 +16,7 @@ use WebGUI::Session; use WebGUI::User; use Test::More; +use Test::Deep; my $session = WebGUI::Test->session; @@ -76,7 +77,10 @@ my $id = $session->getId; my ($count) = $session->db->quickArray("select count(*) from userSession where sessionId=?", [$id]); is($count, 1, "created an user session entry in the database"); -my $varSession = WebGUI::Session->open($session->config); +my $env; +$session->request->env->{REMOTE_ADDR} = '192.168.0.34'; + +my $varSession = WebGUI::Session->open($session->config, $session->request->env); WebGUI::Test->addToCleanup($varSession); my $varTime = time(); isnt($varSession->scratch->get('webguiCsrfToken'), $token, '... calling new without sessionId creates a new token'); @@ -93,16 +97,120 @@ is($varSession->get('adminOn'), 0, "adminOn is off by default"); ##retest is($varSession->get('lastIP'), '192.168.0.34', "lastIP fetched"); -my $var2 = WebGUI::Session->open($session->config, undef, 'illegalSessionIdThatIsTooLong'); -# '1234567890123456789012' -isa_ok($var2, 'WebGUI::Session', 'invalid sessionId will still produce a Session object'); -($count) = $session->db->quickArray("select count(*) from userSession where sessionId=?",[$var2->getId]); -is($count, 0, "object store of sessionId does not match database record"); -my $var2Id = $var2->getId; -$var2->end; -my $idToDelete = substr $var2Id,0,22; -($count) = $session->db->quickArray("select count(*) from userSession where sessionId=?",[$idToDelete]); -is($count, 1, "Unable to delete database record for Var object with invalid sessionId"); +my $illegalSessionId = 'illegalSessionIdThatIsTooLong'; +# '1234567890123456789012' +my $varIllegal = WebGUI::Session->open($session->config, undef, ); +WebGUI::Test->addToCleanup($varIllegal); + +isa_ok($varIllegal, 'WebGUI::Session', 'invalid sessionId will still produce a Session object'); +ok($session->id->valid($varIllegal->getId), 'valid ID created for the new session, when bad Id was suggested'); +ok(index($varIllegal->getId, $illegalSessionId) == -1, 'illegal session was not truncated to make the new Id'); + +$session->request->env->{REMOTE_ADDR} = '10.0.0.5'; +my $varCopy = WebGUI::Session->open($session->config, $session->request->env, $varSession->getId); +is($varCopy->scratch->get('webguiCsrfToken'), $varSession->scratch->get('webguiCsrfToken'), 'opening a copy of a user session did not change the CSRF token'); + +cmp_deeply( + $varCopy, + methods( + ['get', 'sessionId'] => $varSession->get('sessionId'), + ['get', 'userId'] => $varSession->get('userId'), + ['get', 'adminOn'] => $varSession->get('adminOn'), + ), + 'similar methods in copy of original var object' +); + +is($varCopy->get('lastIP'), '10.0.0.5', "lastIP set on copy"); + +my $varSessionId = $varSession->getId; +$varSession->end; +($count) = $session->db->quickArray("select count(*) from userSession where sessionId=?",[$varSession->getId]); +ok($count == 0,"end() removes current entry from database"); + +{ + my $sessionId = 'nonExistantIdButValid0'; + # '1234567890123456789012' + my $testSession = WebGUI::Session->open($session->config, undef, $sessionId); + my $guard = WebGUI::Test->cleanupGuard($testSession); + isa_ok($testSession, 'WebGUI::Session', 'non-existant sessionId will still produce a Var object'); + is($testSession->getId, $sessionId, 'user session Id set to non-existant Id'); +} + +{ + my $expire = WebGUI::Session->open($session->config); + my $guard = WebGUI::Test->cleanupGuard($expire); + $expire->switchAdminOn; + # jury rig the database and the cache to expire + my $expire_time = $expire->get('lastPageView') - 1; + $session->db->write("update userSession set userId=?, expires=? where sessionId=?", [3, $expire_time, $expire->getId]); + $session->user({userId => 3}); + my $copyOfSession = { %{ $expire->get() } }; + $copyOfSession->{expires} = $expire_time; + $session->cache->set($expire->getId, $copyOfSession); + + my $copy = WebGUI::Session->open($session->config, undef, $expire->getId); + my $guard2 = WebGUI::Test->cleanupGuard($copy); + is $copy->getId, $expire->getId, 'new Var object has correct id'; + isnt $copy->isAdminOn, $expire->isAdminOn, 'new adminOn not equal to old adminOn'; + is $copy->isAdminOn, 0, 'new Var object has default adminOn'; + isnt $copy->get('userId'), 3, 'new userId not equal to old userId'; +} + +{ + ##Var objects for noFuss tests + my $trial = WebGUI::Session->open($session->config); + my $expiring = WebGUI::Session->open($session->config); + my $guard = WebGUI::Test->cleanupGuard($trial, $expiring); + $session->db->write("update userSession set expires=? where sessionId=?", [$expiring->get('lastPageView')-5, $expiring->getId]); + $expiring->{_var}{expires} = $expiring->get('lastPageView')-5; + + ##Valid fetch with no fuss + my $varTest = WebGUI::Session->open($session->config, $session->request->env, $trial->getId, 1); + my $guard2 = WebGUI::Test->cleanupGuard($varTest); + + cmp_deeply( + $varTest, + methods( + ['get', 'sessionId'] => $trial->getId, + ['get', 'userId'] => 1, + ['get', 'adminOn'] => 0, + ['get', 'lastIP'] => '127.0.0.1', + ['get', 'expires'] => $trial->get('expires'), + ['get', 'lastPageView'] => $trial->get('lastPageView'), + ), + 'fetching a valid session with noFuss does not update the object info' + ); + + ##Test a valid fetch + my $expired = WebGUI::Session->open($session->config, undef, $expiring->getId, 1); + my $guard3 = WebGUI::Test->cleanupGuard($expired); + + cmp_deeply( + $expired, + methods( + ['get', 'sessionId'] => $expiring->getId, + ['get', 'userId'] => 1, + ['get', 'adminOn'] => 0, + ['get', 'lastIP'] => '127.0.0.1', + ['get', 'lastPageView'] => $expiring->get('lastPageView'), + ['get', 'expires'] => $expiring->get('expires'), + ), + 'fetching a valid session with noFuss does not update the object info, even if it has expired' + ); + +} + +my $varId4 = 'idDoesNotExist00779988'; +# '1234567890123456789012' +my $varTest = WebGUI::Session->open($session->config, undef, $varId4, 1); +WebGUI::Test->addToCleanup($varTest); +isa_ok($varTest, "WebGUI::Session", "non-existant Id with noFuss returns a valid object..."); +is($varTest->getId, $varId4, "...and we got our requested Id"); + +$varTest->start(3, $varTest->getId); +is($varTest->get('userId'), 3, 'userId set via start'); +$varTest->start("", $varTest->getId); +is($varTest->get('userId'), 1, 'calling start with null userId returns default user (visitor)'); done_testing; diff --git a/t/Session/Var.t b/t/Session/Var.t deleted file mode 100644 index 20dcaea4c..000000000 --- a/t/Session/Var.t +++ /dev/null @@ -1,216 +0,0 @@ -#------------------------------------------------------------------- -# WebGUI is Copyright 2001-2009 Plain Black Corporation. -#------------------------------------------------------------------- -# Please read the legal notices (docs/legal.txt) and the license -# (docs/license.txt) that came with this distribution before using -# this software. -#------------------------------------------------------------------- -# http://www.plainblack.com info@plainblack.com -#------------------------------------------------------------------- - -use strict; - -use WebGUI::Test; -use WebGUI::Session; -use WebGUI::Session::Var; - -use Test::More tests => 44; # increment this value for each test you create -use Test::Deep; - -my $session = WebGUI::Test->session; - -ok($session->var->getId ne "", "getId()"); -cmp_ok($session->var->get("lastPageView"), '>', 0, "get(lastPageView)"); -is($session->var->isAdminOn, 0, "isAdminOn()"); -$session->var->switchAdminOn; -is($session->var->isAdminOn, 1, "switchAdminOn()"); -$session->var->switchAdminOff; -is($session->var->isAdminOn, 0, "switchAdminOff()"); - -my $token = $session->scratch->get('webguiCsrfToken'); -ok( $token, 'CSRF token set'); -ok( $session->id->valid($token), '...is a valid GUID'); - -my $id = $session->var->getId; -my ($count) = $session->db->quickArray("select count(*) from userSession where sessionId=?",[$id]); -is($count, 1, "created an user session entry in the database"); - -my $env = $session->request->env; -$env->{REMOTE_ADDR} = '192.168.0.34'; - -my $var = WebGUI::Session::Var->new($session); -my $varTime = time(); -my $varExpires = $varTime + $session->setting->get('sessionTimeout'); -isa_ok($var, 'WebGUI::Session::Var', 'new returns Var object'); -isnt($session->scratch->get('webguiCsrfToken'), $token, '... calling new without sessionId creates a new token'); -$token = $session->scratch->get('webguiCsrfToken'); - -cmp_ok(abs($var->get('lastPageView') - $varTime), '<=', 1, 'lastPageView set correctly'); -cmp_ok(abs($var->get('expires') - $varExpires), '<=', 1, 'expires set correctly'); - -is($var->get('userId'), 1, 'default userId is 1'); - -is($var->get('sessionId'), $var->getId, "get('sessionId') and getId return the same thing"); -isnt($var->getId, $session->var->getId, "a sessionId different from our Session's var sessionId was created"); -is($var->getId, $session->getId, 'SessionId set to userSessionId from var'); - -is($var->get('adminOn'), $var->isAdminOn, "get('adminOn') and isAdminOn return the same thing"); -is($var->get('adminOn'), 0, "adminOn is off by default"); ##retest - -is($var->get('lastIP'), '192.168.0.34', "lastIP fetched"); - -isa_ok($var->session, 'WebGUI::Session', 'session method returns a Session object'); -is($var->session->getId, $session->getId, 'session method returns our Session object'); - -sleep(2); -$env->{REMOTE_ADDR} = '10.0.5.5'; - -#Grab a more recent version of our user session object -$varTime = time(); -my $var2 = WebGUI::Session::Var->new($session, $session->getId); -$varExpires = $varTime + $session->setting->get('sessionTimeout'); -is($var2->session->scratch->get('webguiCsrfToken'), $token, 'opening a new user session did not change the CSRF token'); - -cmp_deeply( - $var2, - methods( - ['get', 'sessionId'] => $var->get('sessionId'), - ['get', 'userId'] => $var->get('userId'), - ['get', 'adminOn'] => $var->get('adminOn'), - ), - 'similar methods in copy of original var object' -); - -cmp_ok(abs($var2->get('lastPageView') - $varTime), '<=', 1, 'lastPageView set correctly on copy'); -cmp_ok(abs($var2->get('expires') - $varExpires), '<=', 1, 'expires set correctly on copy'); -is($var2->get('lastIP'), '10.0.5.5', "lastIP set on copy"); - -my $var2Id = $var2->getId; -$var2->end; -($count) = $session->db->quickArray("select count(*) from userSession where sessionId=?",[$var2->getId]); -ok($count == 0,"end() removes current entry from database"); -$var->end; - -$var2 = WebGUI::Session::Var->new($session, 'illegalSessionIdThatIsTooLong'); -# '1234567890123456789012' -isa_ok($var2, 'WebGUI::Session::Var', 'invalid sessionId will still produce a Var object'); -($count) = $session->db->quickArray("select count(*) from userSession where sessionId=?",[$var2->getId]); -is($count, 0, "object store of sessionId does not match database record"); -$var2Id = $var2->getId; -$var2->end; -my $idToDelete = substr $var2Id,0,22; -($count) = $session->db->quickArray("select count(*) from userSession where sessionId=?",[$idToDelete]); -is($count, 1, "Unable to delete database record for Var object with invalid sessionId"); - -my $varId3 = 'nonExistantIdButValid0'; -# '1234567890123456789012' -$var = WebGUI::Session::Var->new($session, $varId3); -isa_ok($var, 'WebGUI::Session::Var', 'non-existant sessionId will still produce a Var object'); -is($var->getId, $varId3, 'user session Id set to non-existant Id'); -is($session->getId, $varId3, 'session Id set to non-existant Id'); - -cmp_deeply( - $var, - methods( - ['get', 'sessionId'] => $varId3, - ['get', 'userId'] => 1, - ['get', 'adminOn'] => 0, - ['get', 'lastIP'] => '10.0.5.5', - ), - 'non-existant Id returns default values' -); - -$var->end; - -##Grab a new Var object that we'll expire. We'll detect the expiration -##by looking for admin status and userId -$var2 = WebGUI::Session::Var->new($session); -$var2->switchAdminOn; - -# jury rig the database and the cache to expire -$session->db->write("update userSession set userId=? where sessionId=?", - [3, $var2->getId]); -$session->db->write("update userSession set expires=? where sessionId=?", - [$var2->get('lastPageView')-1, $var2->getId]); -my %copyOfVar2 = %{$var2->{_var}}; -$copyOfVar2{expires} = $var2->get('lastPageView')-1; -$copyOfVar2{userId} = 3; -$session->cache->set($var2->getId, \%copyOfVar2); - -my $var3 = WebGUI::Session::Var->new($session, $var2->getId); -is $var3->getId, $var2->getId, 'new Var object has correct id'; -isnt $var3->isAdminOn, $var2->isAdminOn, 'new adminOn not equal to old adminOn'; -is $var3->isAdminOn, 0, 'new Var object has default adminOn'; -isnt $var3->get('userId'), 3, 'new userId not equal to old userId'; -$var2->end; -$var3->end; - -##Var objects for noFuss tests -my $var4 = WebGUI::Session::Var->new($session); -my $varExpiring = WebGUI::Session::Var->new($session); -$session->db->write("update userSession set expires=? where sessionId=?", - [$varExpiring->get('lastPageView')-1, $varExpiring->getId]); -$varExpiring->{_var}{expires} = $varExpiring->get('lastPageView')-1; - -sleep 1; - -$env->{REMOTE_ADDR} = '127.0.0.1'; - -##Test a valid fetch -my $varTest = WebGUI::Session::Var->new($session, $var4->getId, 1); - -cmp_deeply( - $varTest, - methods( - ['get', 'sessionId'] => $var4->getId, - ['get', 'userId'] => 1, - ['get', 'adminOn'] => 0, - ['get', 'lastIP'] => '10.0.5.5', - ['get', 'expires'] => $var4->get('expires'), - ['get', 'lastPageView'] => $var4->get('lastPageView'), - ), - 'fetching a valid session with noFuss does not update the object info' -); - -$varTest->end; -$var4->end; - -##Test a valid fetch -$varTest = WebGUI::Session::Var->new($session, $varExpiring->getId, 1); - -cmp_deeply( - $varTest, - methods( - ['get', 'sessionId'] => $varExpiring->getId, - ['get', 'userId'] => 1, - ['get', 'adminOn'] => 0, - ['get', 'lastIP'] => '10.0.5.5', - ['get', 'lastPageView'] => $varExpiring->get('lastPageView'), - ['get', 'expires'] => $varExpiring->get('expires'), - ), - 'fetching a valid session with noFuss does not update the object info, even if it has expired' -); - -$varExpiring->end; -$varTest->end; - -my $varId4 = 'idDoesNotExist00779988'; -# '1234567890123456789012' -$varTest = WebGUI::Session::Var->new($session, $varId4, 1); -isa_ok($varTest, "WebGUI::Session::Var", "non-existant Id with noFuss returns a valid object..."); -is($varTest->getId, $varId4, "...and we got our requested Id"); - -$varTest->start(3, $varTest->getId); -is($varTest->get('userId'), 3, 'userId set via start'); -$varTest->start("", $varTest->getId); -is($varTest->get('userId'), 1, 'calling start with null userId returns default user (visitor)'); - -END { - - foreach my $varObj ($var, $var2, $var3, $var4, $varExpiring, $varTest) { - if (defined $varObj and ref $varObj eq 'WebGUI::Session::Var') { - $varObj->end(); - } - } - $session->db->write("delete from userSession where sessionId=?",[$idToDelete]); -}