View source
<?php
namespace Drupal\session_limit\Services;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\Url;
use Drupal\session_limit\Event\SessionLimitBypassEvent;
use Drupal\session_limit\Event\SessionLimitCollisionEvent;
use Drupal\session_limit\Event\SessionLimitDisconnectEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Session\AccountProxy;
use Drupal\Core\Session\SessionManager;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\Core\Config\ConfigFactory;
class SessionLimit implements EventSubscriberInterface {
const ACTION_ASK = 0;
const ACTION_DROP_OLDEST = 1;
const ACTION_PREVENT_NEW = 2;
const USER_UNLIMITED_SESSIONS = -1;
public static function getActions() {
return [
SessionLimit::ACTION_ASK => t('Ask user which session to end.'),
SessionLimit::ACTION_DROP_OLDEST => t('Automatically drop the oldest sessions.'),
SessionLimit::ACTION_PREVENT_NEW => t('Prevent creating of any new sessions.'),
];
}
public static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = [
'onKernelRequest',
32,
];
$events['session_limit.bypass'][] = [
'onSessionLimitBypass',
];
$events['session_limit.collision'][] = [
'onSessionCollision',
];
return $events;
}
protected $currentUser;
protected $routeMatch;
protected $database;
protected $eventDispatcher;
protected $sessionManager;
protected $moduleHandler;
protected $configFactory;
protected $messenger;
public function __construct(Connection $database, EventDispatcherInterface $eventDispatcher, RouteMatchInterface $routeMatch, AccountProxy $currentUser, SessionManager $sessionManager, ModuleHandler $moduleHandler, ConfigFactory $configFactory, MessengerInterface $messenger) {
$this->routeMatch = $routeMatch;
$this->database = $database;
$this->eventDispatcher = $eventDispatcher;
$this->currentUser = $currentUser;
$this->sessionManager = $sessionManager;
$this->moduleHandler = $moduleHandler;
$this->configFactory = $configFactory;
$this->messenger = $messenger;
}
public function getRouteMatch() {
return $this->routeMatch;
}
public function getEventDispatcher() {
return $this->eventDispatcher;
}
protected function getCurrentUser() {
return $this->currentUser;
}
public function onKernelRequest() {
if (isset($_SESSION['messages'])) {
$session_messages = $_SESSION['messages'];
foreach ($session_messages as $severity => $message_object) {
foreach ($message_object as $message) {
\Drupal::messenger()
->addMessage($message, $severity);
}
}
unset($_SESSION['messages']);
}
$bypassEvent = $this
->getEventDispatcher()
->dispatch('session_limit.bypass', new SessionLimitBypassEvent());
if ($bypassEvent
->shouldBypass()) {
return;
}
$active_sessions = $this
->getUserActiveSessionCount($this
->getCurrentUser());
$max_sessions = $this
->getUserMaxSessions($this
->getCurrentUser());
if ($max_sessions > 0 && $active_sessions > $max_sessions) {
$collisionEvent = new SessionLimitCollisionEvent(session_id(), $this
->getCurrentUser(), $active_sessions, $max_sessions);
$this
->getEventDispatcher()
->dispatch('session_limit.collision', $collisionEvent);
}
else {
if (!isset($_SESSION['session_limit_checkonce'])) {
$_SESSION['session_limit_checkonce'] = TRUE;
}
else {
$_SESSION['session_limit'] = TRUE;
}
}
}
public function onSessionLimitBypass(SessionLimitBypassEvent $event) {
$admin_bypass_check = $this->configFactory
->get('session_limit.settings')
->get('session_limit_admin_inclusion');
$uid = $admin_bypass_check ? 1 : 2;
if ($this
->getCurrentUser()
->id() < $uid) {
$event
->setBypass(TRUE);
return;
}
if ($this
->getMasqueradeIgnore() && \Drupal::service('masquerade')
->isMasquerading()) {
$event
->setBypass(TRUE);
return;
}
if (isset($_SESSION['session_limit'])) {
$event
->setBypass(TRUE);
return;
}
$route = $this
->getRouteMatch();
$current_path = $route
->getRouteObject()
->getPath();
$bypass_paths = [
'/session-limit',
'/user/logout',
];
if (in_array($current_path, $bypass_paths)) {
$event
->setBypass(TRUE);
return;
}
}
public function onSessionCollision(SessionLimitCollisionEvent $event) {
switch ($this
->getCollisionBehaviour()) {
case self::ACTION_ASK:
$this
->_onSessionCollision__Ask();
break;
case self::ACTION_PREVENT_NEW:
$this
->_onSessionCollision__PreventNew($event);
break;
case self::ACTION_DROP_OLDEST:
$this
->_onSessionCollision__DropOldest($event);
break;
}
}
protected function _onSessionCollision__Ask() {
$this->messenger
->addMessage(t('You have too many active sessions. Please choose a session to end.'));
$response = new RedirectResponse(Url::fromRoute('session_limit.limit_form')
->toString(), 307, [
'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0',
'Expires' => 'Sat, 26 Jul 1997 05:00:00 GMT',
]);
$response
->send();
exit;
}
protected function _onSessionCollision__PreventNew(SessionLimitCollisionEvent $event) {
$disconnectEvent = $this
->getEventDispatcher()
->dispatch('session_limit.disconnect', new SessionLimitDisconnectEvent($event
->getSessionId(), $event, $this
->getMessage($event
->getAccount())));
if (!$disconnectEvent
->shouldPreventDisconnect()) {
$this
->sessionActiveDisconnect($disconnectEvent
->getMessage());
}
}
protected function _onSessionCollision__DropOldest(SessionLimitCollisionEvent $event) {
$limit = $this->database
->query("SELECT COUNT(DISTINCT(sid)) - :max_sessions FROM {sessions} WHERE uid = :uid", array(
':max_sessions' => $event
->getUserMaxSessions(),
':uid' => $event
->getAccount()
->id(),
))
->fetchField();
if ($limit > 0) {
$result = $this->database
->select('sessions', 's')
->distinct()
->fields('s', array(
'sid',
'timestamp',
))
->condition('s.uid', $event
->getAccount()
->id())
->orderBy('timestamp', 'ASC')
->range(0, $limit)
->execute();
foreach ($result as $session) {
$disconnectEvent = $this
->getEventDispatcher()
->dispatch('session_limit.disconnect', new SessionLimitDisconnectEvent($event
->getSessionId(), $event, $this
->getMessage($event
->getAccount())));
if (!$disconnectEvent
->shouldPreventDisconnect()) {
$this
->sessionDisconnect($session->sid, $disconnectEvent
->getMessage());
}
}
}
}
public function sessionDisconnect($sessionId, $message) {
$serialized_message = '';
if ($this
->hasMessageSeverity() && !empty($message)) {
$serialized_message = 'messages|' . serialize([
$this
->getMessageSeverity() => [
$message,
],
]);
}
$this->database
->update('sessions')
->fields([
'uid' => 0,
'session' => $serialized_message,
])
->condition('sid', $sessionId)
->execute();
}
public function sessionActiveDisconnect($message) {
$this->messenger
->addMessage($message, $this
->getMessageSeverity());
$this->moduleHandler
->invokeAll('user_logout', array(
$this->currentUser,
));
$this->sessionManager
->destroy();
$this->currentUser
->setAccount(new AnonymousUserSession());
}
public function getUserActiveSessionCount(AccountInterface $account) {
$query = $this->database
->select('sessions', 's')
->distinct()
->fields('s', [
'sid',
])
->condition('s.uid', $account
->id());
if ($this
->getMasqueradeIgnore()) {
$like = '%' . $this->database
->escapeLike('masquerading') . '%';
$query
->condition('s.session', $like, 'NOT LIKE');
}
return $query
->countQuery()
->execute()
->fetchField();
}
public function getUserActiveSessions(AccountInterface $account) {
$query = $this->database
->select('sessions', 's')
->fields('s', [
'uid',
'sid',
'hostname',
'timestamp',
])
->condition('s.uid', $account
->id());
if ($this
->getMasqueradeIgnore()) {
$like = '%' . $this->database
->escapeLike('masquerading') . '%';
$query
->condition('s.session', $like, 'NOT LIKE');
}
return $query
->execute()
->fetchAll();
}
public function getUserMaxSessions(AccountInterface $account) {
$limit = $this->configFactory
->get('session_limit.settings')
->get('session_limit_max');
$role_limits = $this->configFactory
->get('session_limit.settings')
->get('session_limit_roles');
foreach ($account
->getRoles() as $rid) {
if (!empty($role_limits[$rid])) {
if ($role_limits[$rid] == self::USER_UNLIMITED_SESSIONS) {
return self::USER_UNLIMITED_SESSIONS;
}
$limit = max($limit, $role_limits[$rid]);
}
}
return $limit;
}
public function getCollisionBehaviour() {
return $this->configFactory
->get('session_limit.settings')
->get('session_limit_behaviour');
}
public function getMasqueradeIgnore() {
$masqueradeModuleExists = $this->moduleHandler
->moduleExists('masquerade');
if (!$masqueradeModuleExists) {
return FALSE;
}
return $this->configFactory
->get('session_limit.settings')
->get('session_limit_masquerade_ignore');
}
public function hasMessageSeverity() {
$severity = $this
->getMessageSeverity();
return !empty($severity) && in_array($severity, [
'error',
'warning',
'status',
]);
}
public function getMessageSeverity() {
return $this->configFactory
->get('session_limit.settings')
->get('session_limit_logged_out_message_severity');
}
public function getMessage(AccountInterface $account) {
return t('You have been automatically logged out. Someone else has logged in with your username and password and the maximum number of @number simultaneous session(s) was exceeded. This may indicate that your account has been compromised or that account sharing is not allowed on this site. Please contact the site administrator if you suspect your account has been compromised.', [
'@number' => $this
->getUserMaxSessions($account),
]);
}
}