Use subscriber to check session idle time

This commit is contained in:
Nicolas Le Goff
2014-05-12 15:12:44 +02:00
parent 3434e4cab2
commit 82c5520432
5 changed files with 398 additions and 9 deletions

View File

@@ -0,0 +1,126 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2014 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Event\Subscriber;
use Alchemy\Phrasea\Application;
use Entities\SessionModule;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
/**log real human activity on application, to keep session alive*/
class SessionManagerSubscriber implements EventSubscriberInterface
{
private $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => array('onKernelRequest', -112),
);
}
public function onKernelRequest(GetResponseEvent $event)
{
$modulesIds = array(
"prod" => 1,
"client" => 2,
"admin" => 3,
"thesaurus" => 5,
"report" => 10,
"lightbox" => 6,
);
$pathInfo = array_filter(explode('/', $event->getRequest()->getPathInfo()));
if(count($pathInfo) < 1) {
return;
}
$moduleName = strtolower($pathInfo[1]);
if (!array_key_exists($moduleName, $modulesIds) ) {
return;
}
// this route is polled by js in admin/databox to refresh infos (progress bar...)
if (preg_match("#^/admin/databox/[0-9]+/informations/documents/#", $event->getRequest()->getPathInfo()) == 1) {
return;
}
// this route is polled by js in admin/tasks to refresh tasks status
if ($event->getRequest()->getPathInfo() == "/admin/task-manager/tasks/" && $event->getRequest()->getContentType() == 'json') {
return;
}
// if we are already disconnected (ex. from another window), quit immediatly
if (!($this->app['authentication']->isAuthenticated())) {
if ($event->getRequest()->isXmlHttpRequest()) {
$response = new Response("End-Session", 403);
} else {
$response = new RedirectResponse($this->app["url_generator"]->generate("homepage", array("redirect"=>'..' . $event->getRequest()->getPathInfo())));
}
$response->headers->set('X-Phraseanet-End-Session', '1');
$event->setResponse($response);
return;
}
$session = $this->app['EM']->find('Entities\Session', $this->app['session']->get('session_id'));
$idle = 0;
if (isset($this->app["phraseanet.configuration"]["session"]["idle"])) {
$idle = (int)($this->app["phraseanet.configuration"]["session"]["idle"]);
}
$now = new \DateTime();
$dt = $now->getTimestamp() - $session->getUpdated()->getTimestamp();
if ($idle > 0 && $dt > $idle) {
// we must disconnet due to idletime
$this->app['authentication']->closeAccount();
if ($event->getRequest()->isXmlHttpRequest()) {
$response = new Response("End-Session", 403);
} else {
$response = new RedirectResponse($this->app["url_generator"]->generate("homepage", array("redirect"=>'..' . $event->getRequest()->getPathInfo())));
}
$response->headers->set('X-Phraseanet-End-Session', '1');
$event->setResponse($response);
return;
}
$moduleId = $modulesIds[$moduleName];
$session->setUpdated(new \DateTime());
if (!$session->hasModuleId($moduleId)) {
$module = new SessionModule();
$module->setModuleId($moduleId);
$module->setSession($session);
$session->addModule($module);
$this->app['EM']->persist($module);
} else {
$this->app['EM']->persist($session->getModuleById($moduleId)->setUpdated(new \DateTime()));
}
$this->app['EM']->persist($session);
$this->app['EM']->flush();
}
}

View File

@@ -45,14 +45,22 @@ class PhraseaAuthenticationForm extends AbstractType
),
));
$builder->add('remember-me', $this->app['phraseanet.configuration']['session']['idle'] < 1 ? 'checkbox' : 'hidden', array(
'label' => $this->app['phraseanet.configuration']['session']['idle'] < 1 ? _('Remember me') : "",
if ($this->app['phraseanet.configuration']['session']['idle'] < 1) {
$builder->add('remember-me', 'checkbox' , array(
'label' => _('Remember me'),
'mapped' => false,
'required' => false,
'attr' => array(
'value' => '1',
)
));
} else {
$builder->add('remember-me', 'hidden' , array(
'label' => '',
'mapped' => false,
'required' => false
));
}
$builder->add('redirect', 'hidden', array(
'required' => false,

View File

@@ -1738,6 +1738,34 @@ class LoginTest extends \PhraseanetWebTestCaseAuthenticatedAbstract
$this->assertSame(200, self::$DI['client']->getResponse()->getStatusCode());
}
public function testLoginPageWithIdleSessionTime()
{
$this->logout(self::$DI['app']);
self::$DI['app']['phraseanet.configuration']['session'] = array(
'idle' =>10,
'lifetime' => 60475,
);
$crawler = self::$DI['client']->request('GET', '/login/');
$this->assertSame(200, self::$DI['client']->getResponse()->getStatusCode());
$this->assertEquals('hidden', $crawler->filter('input[name="remember-me"]')->attr('type'));
}
public function testLoginPageWithNoIdleSessionTime()
{
$this->logout(self::$DI['app']);
self::$DI['app']['phraseanet.configuration']['session'] = array(
'idle' => 0,
'lifetime' => 60475,
);
$crawler = self::$DI['client']->request('GET', '/login/');
$this->assertSame(200, self::$DI['client']->getResponse()->getStatusCode());
$this->assertEquals('checkbox', $crawler->filter('input[name="remember-me"]')->attr('type'));
}
private function addUsrAuthDoctrineEntitySupport($id, $out, $participants = false)
{
$repo = $this->getMockBuilder('Doctrine\ORM\EntityRepository\UsrAuthProviderRepository')

View File

@@ -0,0 +1,227 @@
<?php
namespace Alchemy\Tests\Phrasea\Core\Event\Subscriber;
use Alchemy\Phrasea\Core\Event\Subscriber\SessionManagerSubscriber;
use Alchemy\Phrasea\Application;
use Entities\Session;
use Symfony\Component\HttpKernel\Client;
class SessionManagerSubscriberTest extends \PhraseanetWebTestCaseAuthenticatedAbstract
{
public function testEndSession()
{
$app = new Application('test');
$app['dispatcher']->addSubscriber(new SessionManagerSubscriber($app));
$app['phraseanet.configuration']['session'] = array(
'idle' => 0,
'lifetime' => 60475,
);
$app->get('/login', function () {
return '';
})->bind("homepage");
$app->get('/prod', function () {
return '';
});
$client = new Client($app);
$client->request('GET', '/prod');
$this->assertTrue($client->getResponse()->isRedirect());
$this->assertNotNUll($client->getResponse()->headers->get('x-phraseanet-end-session'));
$this->assertNotNUll($client->getResponse()->headers->get('location'));
$this->assertEquals('/login?redirect=..%2Fprod', $client->getResponse()->headers->get('location'));
}
public function testEndSessionXmlXhttpRequest()
{
$app = new Application('test');
$app['dispatcher']->addSubscriber(new SessionManagerSubscriber($app));
$app['phraseanet.configuration']['session'] = array(
'idle' => 0,
'lifetime' => 60475,
);
$app->get('/login', function () {
return '';
})->bind("homepage");
$app->get('/prod', function () {
return '';
});
$client = new Client($app);
$client->request('GET', '/prod', array(), array(), array(
'HTTP_ACCEPT' => 'application/json',
'HTTP_X-Requested-With' => 'XMLHttpRequest',
));
$this->assertTrue($client->getResponse()->isClientError());
$this->assertNotNUll($client->getResponse()->headers->get('x-phraseanet-end-session'));
}
public function testEndSessionAuthenticated()
{
$app = new Application('test');
$app['dispatcher']->addSubscriber(new SessionManagerSubscriber($app));
$app['authentication'] = $this->getMockBuilder('Alchemy\Phrasea\Authentication\Authenticator')->disableOriginalConstructor()->getMock();
$app['authentication']->expects($this->any())->method('isAuthenticated')->will($this->returnValue(true));
$session = new Session();
$session->setUpdated(new \DateTime());
$app['EM'] = $this->getMockBuilder('Doctrine\ORM\EntityManager')->disableOriginalConstructor()->getMock();
$app['EM']->expects($this->once())->method('find')->with($this->equalTo('Entities\Session'))->will($this->returnValue($session));
$app['EM']->expects($this->exactly(2))->method('persist')->will($this->returnValue(null));
$app['EM']->expects($this->once())->method('flush')->will($this->returnValue(null));
$app['phraseanet.configuration']['session'] = array(
'idle' => 0,
'lifetime' => 60475,
);
$app->get('/login', function () {
return '';
})->bind("homepage");
$app->get('/prod', function () {
return '';
});
$client = new Client($app);
$client->request('GET', '/prod');
$this->assertTrue($client->getResponse()->isOK());
}
public function testEndSessionAuthenticatedWithOutdatedIdle()
{
$app = new Application('test');
$app['dispatcher']->addSubscriber(new SessionManagerSubscriber($app));
$app['authentication'] = $this->getMockBuilder('Alchemy\Phrasea\Authentication\Authenticator')->disableOriginalConstructor()->getMock();
$app['authentication']->expects($this->any())->method('isAuthenticated')->will($this->returnValue(true));
$app['authentication']->expects($this->once())->method('closeAccount')->will($this->returnValue(null));
$session = new Session();
$session->setUpdated(new \DateTime('-1 hour'));
$app['EM'] = $this->getMockBuilder('Doctrine\ORM\EntityManager')->disableOriginalConstructor()->getMock();
$app['EM']->expects($this->once())->method('find')->with($this->equalTo('Entities\Session'))->will($this->returnValue($session));
$app['phraseanet.configuration']['session'] = array(
'idle' => 10,
'lifetime' => 60475,
);
$app->get('/login', function () {
return '';
})->bind("homepage");
$app->get('/prod', function () {
return '';
});
$client = new Client($app);
$client->request('GET', '/prod');
$this->assertTrue($client->getResponse()->isRedirect());
$this->assertNotNUll($client->getResponse()->headers->get('x-phraseanet-end-session'));
$this->assertNotNUll($client->getResponse()->headers->get('location'));
$this->assertEquals('/login?redirect=..%2Fprod', $client->getResponse()->headers->get('location'));
}
public function testEndSessionAuthenticatedWithOutdatedIdleXmlHttpRequest()
{
$app = new Application('test');
$app['dispatcher']->addSubscriber(new SessionManagerSubscriber($app));
$app['authentication'] = $this->getMockBuilder('Alchemy\Phrasea\Authentication\Authenticator')->disableOriginalConstructor()->getMock();
$app['authentication']->expects($this->any())->method('isAuthenticated')->will($this->returnValue(true));
$app['authentication']->expects($this->once())->method('closeAccount')->will($this->returnValue(null));
$session = new Session();
$session->setUpdated(new \DateTime('-1 hour'));
$app['EM'] = $this->getMockBuilder('Doctrine\ORM\EntityManager')->disableOriginalConstructor()->getMock();
$app['EM']->expects($this->once())->method('find')->with($this->equalTo('Entities\Session'))->will($this->returnValue($session));
$app['phraseanet.configuration']['session'] = array(
'idle' => 10,
'lifetime' => 60475,
);
$app->get('/login', function () {
return '';
})->bind("homepage");
$app->get('/prod', function () {
return '';
});
$client = new Client($app);
$client->request('GET', '/prod', array(), array(), array(
'HTTP_ACCEPT' => 'application/json',
'HTTP_X-Requested-With' => 'XMLHttpRequest',
));
$this->assertTrue($client->getResponse()->isClientError());
$this->assertNotNUll($client->getResponse()->headers->get('x-phraseanet-end-session'));
}
public function testUndefinedModule()
{
$app = new Application('test');
$app['dispatcher']->addSubscriber(new SessionManagerSubscriber($app));
$app->get('/login', function () {
return '';
})->bind("homepage");
$app->get('/undefined-module', function () {
return 'undefined-module';
});
$client = new Client($app);
$client->request('GET', '/undefined-module');
$this->assertTrue($client->getResponse()->isOk());
$this->assertEquals('undefined-module', $client->getResponse()->getContent());
}
/**
* @dataProvider forbiddenRouteProvider
*/
public function testForbiddenRoutes($route)
{
$app = new Application('test');
$app['dispatcher']->addSubscriber(new SessionManagerSubscriber($app));
$app['authentication'] = $this->getMockBuilder('Alchemy\Phrasea\Authentication\Authenticator')->disableOriginalConstructor()->getMock();
$app['authentication']->expects($this->never())->method('isAuthenticated');
$app['EM'] = $this->getMockBuilder('Doctrine\ORM\EntityManager')->disableOriginalConstructor()->getMock();
$app['EM']->expects($this->never())->method('flush');
$app->get('/login', function () {
return '';
})->bind("homepage");
$app->get($route, function () {
return '';
});
$client = new Client($app);
$client->request('GET', $route, array(), array(), array(
'HTTP_CONTENT-TYPE' => 'application/json',
'HTTP_ACCEPT' => 'application/json',
'HTTP_X-Requested-With' => 'XMLHttpRequest',
));
}
public function forbiddenRouteProvider()
{
return array(
array('/admin/databox/17/informations/documents/'),
array('/admin/task-manager/tasks/'),
);
}
}

View File

@@ -9,6 +9,6 @@ class PhraseaAuthenticationFormTest extends FormTestCase
{
protected function getForm()
{
return new PhraseaAuthenticationForm();
return new PhraseaAuthenticationForm(self::$DI['app']);
}
}