diff --git a/lib/Alchemy/Phrasea/Application.php b/lib/Alchemy/Phrasea/Application.php index 2634a5dfa9..3024f7e04e 100644 --- a/lib/Alchemy/Phrasea/Application.php +++ b/lib/Alchemy/Phrasea/Application.php @@ -136,6 +136,8 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Generator\UrlGenerator; use Symfony\Component\Form\FormFactory; use Symfony\Component\Form\FormTypeInterface; @@ -788,6 +790,94 @@ class Application extends SilexApplication $this->mount('/datafiles/', new Datafiles()); + // log real human activity on application, to keep session alive + $app = $this; + $this->before(function(Request $request) use ($app) { + + static $modulesIds = array( + "prod" => 1, + "client" => 2, + "admin" => 3, + "thesaurus" => 5, + "report" => 10, + "lightbox" => 6, + ); + + $pathInfo = explode('/', $request->getPathInfo()); + + if(count($pathInfo) < 2) { + return; + } + + $moduleName = strtolower($pathInfo[1]); + if(!array_key_exists($moduleName, $modulesIds) ) { // || !($app['authentication']->isAuthenticated()) ) { + return; + } + + // this route is polled by js in admin/databox to refresh infos (progress bar...) + if(preg_match("#^/admin/databox/[0-9]+/informations/documents/#", $request->getPathInfo()) == 1) { + return; + } + + // this route is polled by js in admin/tasks to refresh tasks status + if($request->getPathInfo() == "/admin/task-manager/tasks/" && $request->getContentType() == 'json') { + return; + } + + // if we are already disconnected (ex. from another window), quit immediatly + if(!($app['authentication']->isAuthenticated())) { + if($request->isXmlHttpRequest()) { + $r = new Response("End-Session", 403); + } + else { + $r = new RedirectResponse($app["url_generator"]->generate("homepage")); + } + $r->headers->set('X-Phraseanet-End-Session', '1'); + + return $r; + } + + $session = $app['EM']->find('Entities\Session', $app['session']->get('session_id')); + + $idle = 0; + if(isset($app["phraseanet.configuration"]["session"]["idle"])) { + $idle = (int)($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 + $app['authentication']->closeAccount(); + if($request->isXmlHttpRequest()) { + $r = new Response("End-Session", 403); + } + else { + $r = new RedirectResponse($app["url_generator"]->generate("homepage")); + } + $r->headers->set('X-Phraseanet-End-Session', '1'); + + return $r; + } + + $moduleId = $modulesIds[$moduleName]; + + $session->setUpdated(new \DateTime()); + + if (!$session->hasModuleId($moduleId)) { + $module = new \Entities\SessionModule(); + $module->setModuleId($moduleId); + $module->setSession($session); + $session->addModule($module); + + $app['EM']->persist($module); + } else { + $app['EM']->persist($session->getModuleById($moduleId)->setUpdated(new \DateTime())); + } + + $app['EM']->persist($session); + $app['EM']->flush(); + }); + $this->mount('/admin/', new AdminRoot()); $this->mount('/admin/dashboard', new Dashboard()); $this->mount('/admin/collection', new Collection()); diff --git a/lib/Alchemy/Phrasea/Controller/Admin/Root.php b/lib/Alchemy/Phrasea/Controller/Admin/Root.php index b3f66692e5..aaa8218009 100644 --- a/lib/Alchemy/Phrasea/Controller/Admin/Root.php +++ b/lib/Alchemy/Phrasea/Controller/Admin/Root.php @@ -17,6 +17,8 @@ use Silex\ControllerProviderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Alchemy\Phrasea\Controller\Root\Session; + /** * * @license http://opensource.org/licenses/gpl-3.0 GPLv3 @@ -28,7 +30,6 @@ class Root implements ControllerProviderInterface public function connect(Application $app) { $controllers = $app['controllers_factory']; - $app['firewall']->addMandatoryAuthentication($controllers); $controllers->before(function (Request $request) use ($app) { diff --git a/lib/Alchemy/Phrasea/Controller/Root/Login.php b/lib/Alchemy/Phrasea/Controller/Root/Login.php index aa933b52ae..90f0bec9ce 100644 --- a/lib/Alchemy/Phrasea/Controller/Root/Login.php +++ b/lib/Alchemy/Phrasea/Controller/Root/Login.php @@ -766,7 +766,7 @@ class Login implements ControllerProviderInterface $feeds = $public_feeds->get_feeds(); array_unshift($feeds, $public_feeds->get_aggregate()); - $form = $app->form(new PhraseaAuthenticationForm()); + $form = $app->form(new PhraseaAuthenticationForm($app)); $form->setData(array( 'redirect' => $request->query->get('redirect') )); @@ -788,7 +788,7 @@ class Login implements ControllerProviderInterface */ public function authenticate(PhraseaApplication $app, Request $request) { - $form = $app->form(new PhraseaAuthenticationForm()); + $form = $app->form(new PhraseaAuthenticationForm($app)); $redirector = function (array $params = array()) use ($app) { return $app->redirectPath('homepage', $params); }; @@ -1079,7 +1079,7 @@ class Login implements ControllerProviderInterface $session->setToken($token)->setNonce($nonce); - $response->headers->setCookie(new Cookie('persistent', $token)); + $response->headers->setCookie(new Cookie('persistent', $token, time() + $app['phraseanet.configuration']['session']['lifetime'])); $app['EM']->persist($session); $app['EM']->flush(); diff --git a/lib/Alchemy/Phrasea/Controller/Root/Session.php b/lib/Alchemy/Phrasea/Controller/Root/Session.php index 5e95c608bb..0ecce7976b 100644 --- a/lib/Alchemy/Phrasea/Controller/Root/Session.php +++ b/lib/Alchemy/Phrasea/Controller/Root/Session.php @@ -38,15 +38,87 @@ class Session implements ControllerProviderInterface $controllers->post('/update/', $this->call('updateSession')) ->bind('update_session'); + $controllers->post('/notifications/', $this->call('getNotifications')) + ->bind('get_notifications'); $controller = $controllers->post('/delete/{id}', $this->call('deleteSession')) - ->bind('delete_session'); + ->bind('delete_session'); $app['firewall']->addMandatoryAuthentication($controller); return $controllers; } + /** + * Check things to notify + * + * @param Application $app + * @param Request $request + * @return JsonResponse + */ + public function getNotifications(Application $app, Request $request) + { + if (!$request->isXmlHttpRequest()) { + $app->abort(400); + } + + $ret = array( + 'status' => 'unknown', + 'message' => '', + 'notifications' => false, + 'changed' => array() + ); + + if ($app['authentication']->isAuthenticated()) { + $usr_id = $app['authentication']->getUser()->get_id(); + if ($usr_id != $request->request->get('usr')) { // I logged with another user + $ret['status'] = 'disconnected'; + + return $app->json($ret); + } + } else { + $ret['status'] = 'disconnected'; + + return $app->json($ret); + } + + try { + $app['phraseanet.appbox']->get_connection(); + } catch (\Exception $e) { + return $app->json($ret); + } + + if (1 > $moduleId = (int) $request->request->get('module')) { + $ret['message'] = 'Missing or Invalid `module` parameter'; + + return $app->json($ret); + } + + $ret['status'] = 'ok'; + + $ret['notifications'] = $app['twig']->render('prod/notifications.html.twig', array( + 'notifications' => $app['events-manager']->get_notifications() + )); + + $baskets = $app['EM']->getRepository('\Entities\Basket')->findUnreadActiveByUser($app['authentication']->getUser()); + + foreach ($baskets as $basket) { + $ret['changed'][] = $basket->getId(); + } + + if (in_array($app['session']->get('phraseanet.message'), array('1', null))) { + if ($app['phraseanet.configuration']['main']['maintenance']) { + $ret['message'] .= _('The application is going down for maintenance, please logout.'); + } + + if ($app['phraseanet.registry']->get('GV_message_on')) { + $ret['message'] .= strip_tags($app['phraseanet.registry']->get('GV_message')); + } + } + + return $app->json($ret); + } + /** * Check session state * diff --git a/lib/Alchemy/Phrasea/Form/Login/PhraseaAuthenticationForm.php b/lib/Alchemy/Phrasea/Form/Login/PhraseaAuthenticationForm.php index 708c45f698..25778b8f21 100644 --- a/lib/Alchemy/Phrasea/Form/Login/PhraseaAuthenticationForm.php +++ b/lib/Alchemy/Phrasea/Form/Login/PhraseaAuthenticationForm.php @@ -14,9 +14,17 @@ namespace Alchemy\Phrasea\Form\Login; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Constraints as Assert; +use Silex\Application; class PhraseaAuthenticationForm extends AbstractType { + private $app; + + public function __construct(Application $app) + { + $this->app = $app; + } + public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('login', 'text', array( @@ -37,12 +45,11 @@ class PhraseaAuthenticationForm extends AbstractType ), )); - $builder->add('remember-me', 'checkbox', array( - 'label' => _('Remember me'), + $builder->add('remember-me', $this->app['phraseanet.configuration']['session']['idle'] < 1 ? 'checkbox' : 'hidden', array( + 'label' => $this->app['phraseanet.configuration']['session']['idle'] < 1 ? _('Remember me') : "", 'mapped' => false, 'required' => false, 'attr' => array( - 'checked' => 'checked', 'value' => '1', ) )); diff --git a/lib/conf.d/configuration.yml b/lib/conf.d/configuration.yml index 3392f71923..5b5811f9ff 100644 --- a/lib/conf.d/configuration.yml +++ b/lib/conf.d/configuration.yml @@ -157,3 +157,8 @@ api_cors: expose_headers: [] max_age: 0 hosts: [] +session: + idle: 0 + # 1 week + lifetime: 604800 + diff --git a/templates/web/admin/databox/databox.html.twig b/templates/web/admin/databox/databox.html.twig index 48139db94b..b2dfdebffa 100644 --- a/templates/web/admin/databox/databox.html.twig +++ b/templates/web/admin/databox/databox.html.twig @@ -245,6 +245,11 @@ diff --git a/templates/web/admin/index.html.twig b/templates/web/admin/index.html.twig index d44bc3f9b9..e3e0f22ecf 100644 --- a/templates/web/admin/index.html.twig +++ b/templates/web/admin/index.html.twig @@ -45,20 +45,20 @@ reset_template_ask_choice: '{{ 'Would you like to reset rights before applying the template?' | trans | e('js') }}' }; - function sessionactive(){ + function pollNotifications(){ $.ajax({ type: "POST", - url: "/session/update/", + url: "{{ path('get_notifications') }}", dataType: 'json', data: { module : 3, usr : {{ app['authentication'].getUser().get_id() }} }, error: function(){ - window.setTimeout("sessionactive();", 10000); + window.setTimeout("pollNotifications();", 10000); }, timeout: function(){ - window.setTimeout("sessionactive();", 10000); + window.setTimeout("pollNotifications();", 10000); }, success: function(data){ if(data) @@ -66,7 +66,7 @@ var t = 120000; if(data.apps && parseInt(data.apps)>1) t = Math.round((Math.sqrt(parseInt(data.apps)-1) * 1.3 * 120000)); - window.setTimeout("sessionactive();", t); + window.setTimeout("pollNotifications();", t); return; } @@ -160,7 +160,7 @@ $(document).ready( function(){ resize(); - setTimeout('sessionactive();',15000); + setTimeout('pollNotifications();',15000); activeTree(true); } ); diff --git a/templates/web/client/index.html.twig b/templates/web/client/index.html.twig index 525c4398b7..a55bf38e01 100644 --- a/templates/web/client/index.html.twig +++ b/templates/web/client/index.html.twig @@ -438,20 +438,20 @@ document.styleSheets[0][propname][3].style.height = (w)+"px"; // .h160px } - function sessionactive(){ + function pollNotifications(){ $.ajax({ type: "POST", - url: "/session/update/", + url: "/session/notifications/", dataType: 'json', data: { app : 2, usr : {{ app['authentication'].getUser().get_id() }} }, error: function(){ - window.setTimeout("sessionactive();", 10000); + window.setTimeout("pollNotifications();", 10000); }, timeout: function(){ - window.setTimeout("sessionactive();", 10000); + window.setTimeout("pollNotifications();", 10000); }, success: function(data){ if(data) @@ -459,7 +459,7 @@ var t = 120000; if(data.apps && parseInt(data.apps)>1) t = Math.round((Math.sqrt(parseInt(data.apps)-1) * 1.3 * 120000)); - window.setTimeout("sessionactive();", t); + window.setTimeout("pollNotifications();", t); return; } diff --git a/templates/web/prod/index.html.twig b/templates/web/prod/index.html.twig index 1423cfb941..756cfd9939 100644 --- a/templates/web/prod/index.html.twig +++ b/templates/web/prod/index.html.twig @@ -1017,24 +1017,25 @@ diff --git a/www/include/jquery.common.js b/www/include/jquery.common.js index 80d7c32374..df6fd47c1f 100644 --- a/www/include/jquery.common.js +++ b/www/include/jquery.common.js @@ -82,9 +82,15 @@ $(document).ready(function () { } }); - set_notif_position(); + $(document).ajaxError(function( event, jqxhr, settings, exception ) { + if(jqxhr.status == 403 && jqxhr.getResponseHeader("X-Phraseanet-End-Session") == "1") { + disconnected(); + } + }); + set_notif_position(); + }); @@ -334,6 +340,7 @@ function infoDialog(el) { } }).dialog('open').css({'overflow-x': 'auto', 'overflow-y': 'auto'}); } + function manageSession(data, showMessages) { if (typeof(showMessages) == "undefined") showMessages = false; @@ -438,15 +445,20 @@ function showModal(cas, options) { content = language.serverDisconnected; escape = false; callback = function (e) { - self.location.replace(self.location.href) + self.location.replace(self.location.href); }; break; default: break; } - p4.Alerts(options.title, content, callback); - + if(typeof(p4.Alerts) == "undefined") { + alert("disconnected"); + self.location.replace(self.location.href); + } + else { + p4.Alerts(options.title, content, callback); + } return; } diff --git a/www/skins/client/jquery.p4client.1.0.js b/www/skins/client/jquery.p4client.1.0.js index 5a9b895078..c7e9fe28bf 100644 --- a/www/skins/client/jquery.p4client.1.0.js +++ b/www/skins/client/jquery.p4client.1.0.js @@ -105,7 +105,7 @@ $(document).ready(function () { $(this).removeClass("hover"); }); - sessionactive(); + window.setTimeout("pollNotifications();", 10000); resize(); $(window).resize(function () { resize(); diff --git a/www/skins/common/main.css b/www/skins/common/main.css index 6f2e3e3c69..03e1a3be10 100644 --- a/www/skins/common/main.css +++ b/www/skins/common/main.css @@ -236,3 +236,7 @@ input.checkbox{ .ui-dialog-titlebar { min-height: 20px; } + +.ui-dialog[aria-describedby="p4_alerts"]{ + z-index:2000 !important; +} diff --git a/www/skins/prod/jquery.Alerts.js b/www/skins/prod/jquery.Alerts.js index c25a4a735c..c98f837c80 100644 --- a/www/skins/prod/jquery.Alerts.js +++ b/www/skins/prod/jquery.Alerts.js @@ -6,7 +6,6 @@ var p4 = p4 || {}; if ($('#p4_alerts').length === 0) { $('body').append('
'); } - return $('#p4_alerts'); } @@ -21,7 +20,6 @@ var p4 = p4 || {}; else dialog.dialog('close'); }; - if (dialog.data('ui-dialog')) { dialog.dialog('destroy'); } diff --git a/www/skins/prod/jquery.main-prod.js b/www/skins/prod/jquery.main-prod.js index 6a3b2681d6..9fe34d5f7b 100644 --- a/www/skins/prod/jquery.main-prod.js +++ b/www/skins/prod/jquery.main-prod.js @@ -1131,7 +1131,7 @@ $(document).ready(function () { initLook(); - setTimeout("sessionactive();", 30000); + setTimeout("pollNotifications();", 10000); $(this).bind('keydown', function (event) { var cancelKey = false; diff --git a/www/skins/report/report.js b/www/skins/report/report.js index 013672d965..f65658dce9 100644 --- a/www/skins/report/report.js +++ b/www/skins/report/report.js @@ -1067,21 +1067,21 @@ function resize() { $('.answers:visible').height(Math.round(bodySize.y - $('.answers:visible').offset().top)); }; -//update session -function sessionactive() { + +function pollNotifications() { $.ajax({ type: "POST", - url: "/session/update/", + url: "/session/notifications/", dataType: 'json', data: { module: 10, usr: usrId }, error: function () { - window.setTimeout("sessionactive();", 10000); + window.setTimeout("pollNotifications();", 10000); }, timeout: function () { - window.setTimeout("sessionactive();", 10000); + window.setTimeout("pollNotifications();", 10000); }, success: function (data) { if (data) { @@ -1091,10 +1091,10 @@ function sessionactive() { if (data.apps && parseInt(data.apps) > 1) { t = Math.round((Math.sqrt(parseInt(data.apps) - 1) * 1.3 * 120000)); } - window.setTimeout("sessionactive();", t); + window.setTimeout("pollNotifications();", t); return; } }); }; -sessionactive(); +window.setTimeout("pollNotifications();", 10000);