mirror of
https://github.com/VTECRM/vtenext.git
synced 2026-02-26 16:18:47 +00:00
345 lines
10 KiB
PHP
345 lines
10 KiB
PHP
<?php
|
|
/*************************************
|
|
* SPDX-FileCopyrightText: 2009-2020 Vtenext S.r.l. <info@vtenext.com>
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
************************************/
|
|
|
|
/* crmv@91082 */
|
|
|
|
require_once('include/BaseClasses.php');
|
|
require_once('modules/Users/LoginHistory.php');
|
|
|
|
/**
|
|
* Class which checks if the current user should be logged out according to some
|
|
* advanced conditions. So far there are 2 implementation, one time based, and another for
|
|
* concurrent logins
|
|
*/
|
|
class SessionValidator extends SDKExtendableUniqueClass {
|
|
|
|
// different types of checks that can be done
|
|
|
|
// This value disables the session expiration (although standard PHP session expiration is
|
|
// still in place)
|
|
const VALIDATION_NONE = 0;
|
|
|
|
// This check limits the maximum number of session a single user can have.
|
|
// Different access point are counted separately, like web, webservice, app.
|
|
// External automatic plugins are not considered
|
|
// This is the default
|
|
const VALIDATION_CONCURRENT_LOGIN = 1;
|
|
|
|
// This check limits the session to a maximum time. Can be also achieved with
|
|
// just the session expiration time, but we wouldn't be able to show the special
|
|
// login page when the session expires.
|
|
const VALIDATION_EXPIRATION = 2;
|
|
|
|
|
|
// the strategy to be used in the validation.
|
|
public $strategy = self::VALIDATION_CONCURRENT_LOGIN;
|
|
|
|
// default config for various validation
|
|
public $validationConfig = array(
|
|
1 => array(
|
|
'allowed_logins' => array( // max number of concurrent accesses (0 means unlimited)
|
|
|
|
'web' => 2, // web access
|
|
'ws' => 5, // webservice acccess (excluding automatic plugins)
|
|
'app' => 3, // app access, this is the maximum number of devices connected
|
|
'other' => 0, // this is for all not handled cases, which by default are forbidden
|
|
),
|
|
|
|
'cache_duration' => 10, // to track different sessions, I have to uswe the database, to store the active sessions,
|
|
// so to avoid a big overhead to read the db at each request, cache the informations
|
|
// for this number of minutes
|
|
),
|
|
2 => array(
|
|
'expiration_time' => 30, // time (in minutes) after which the session expires in case of inactivity
|
|
'cookie_name' => 'last_user_logged', // the name of the cookie to set when saving the old user
|
|
)
|
|
);
|
|
|
|
// crmv@101201
|
|
// these variables are preserved between session expirations
|
|
// be careful not to save any sensitive data in here
|
|
public $preserveSessionVars = array(
|
|
'lvs',
|
|
);
|
|
|
|
public $preserveSessionPath = 'cache/'; // the path where to store the preserved session vars
|
|
|
|
public $preserveSessionDuration = 60; // duration, in minutes, of the session vars
|
|
// crmv@101201e
|
|
|
|
// these files don't trigger the session refresh
|
|
public $skip_files = array(
|
|
'ActivityReminderCallbackAjax',
|
|
'src/Notifications/CheckChanges',
|
|
'TraceIncomingCall', // crmv@185734
|
|
'CheckSession',
|
|
'Validate'
|
|
);
|
|
|
|
public $timer_files = array(
|
|
'ActivityReminderCallbackAjax',
|
|
'src/Notifications/CheckChanges',
|
|
'TraceIncomingCall', // crmv@185734
|
|
);
|
|
|
|
public function getConfig($strategy) {
|
|
return $this->validationConfig[$strategy];
|
|
}
|
|
|
|
/**
|
|
* Check if this is the first time we "check-in"
|
|
*/
|
|
public function isStarted() {
|
|
global $current_user;
|
|
|
|
$ret = false;
|
|
|
|
|
|
if ($this->strategy == self::VALIDATION_NONE) {
|
|
|
|
} elseif ($this->strategy == self::VALIDATION_CONCURRENT_LOGIN) {
|
|
$key = 'last_operation_u_'.$current_user->id;
|
|
$ret = !VteSession::isEmpty($key);
|
|
/*if (!$ret) {
|
|
$LH = LoginHistory::getInstance();
|
|
$LH->set_expired($current_user->user_name);
|
|
|
|
}*/
|
|
} elseif ($this->strategy == self::VALIDATION_EXPIRATION) {
|
|
$key = 'last_operation_u_'.$current_user->id;
|
|
$ret = !VteSession::isEmpty($key);
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Mark the current session as "never expiring" and not accounted in
|
|
* session limits
|
|
*/
|
|
public function setUnlimitedSession() {
|
|
VteSession::set('session_unaccount', 1);
|
|
}
|
|
|
|
/**
|
|
* Remove the mark from the current session, making it expiring according
|
|
* to the regular rules.
|
|
*/
|
|
public function unsetUnlimitedSession() {
|
|
VteSession::remove('session_unaccount');
|
|
}
|
|
|
|
/**
|
|
* Check if the current session is unlimited and unaccounted
|
|
*/
|
|
public function isUnlimitedSession() {
|
|
return (VteSession::get('session_unaccount') == 1);
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if the session is still valid
|
|
*/
|
|
public function isValid($options = array(), &$reason = '') {
|
|
global $adb, $current_user;
|
|
|
|
$ret = true;
|
|
$cfg = $this->getConfig($this->strategy);
|
|
|
|
if ($this->strategy == self::VALIDATION_NONE) {
|
|
return true;
|
|
}
|
|
|
|
// crmv@106590
|
|
if (!$current_user || !$current_user->id) {
|
|
$reason = 'expired';
|
|
return false;
|
|
}
|
|
|
|
if ($this->isUnlimitedSession()) return true;
|
|
|
|
if ($this->strategy == self::VALIDATION_CONCURRENT_LOGIN) {
|
|
// check how many other new connections of the same type has been opened
|
|
// check only after the cache is expired
|
|
//$keyTs = 'last_operation_ts_u_'.$current_user->id;
|
|
//$checkTs = time() - $cfg['cache_duration']*60;
|
|
//if (VteSession::isEmpty($keyTs) || VteSession::get($keyTs) < $checkTs) {
|
|
$LH = LoginHistory::getInstance();
|
|
$ltime = $LH->getLoginTime($current_user->user_name);
|
|
$type = $LH->getLoginType();
|
|
$count = $LH->countOtherOpenSessions($current_user->user_name, 'auto', $ltime);
|
|
$limit = intval($cfg['allowed_logins'][$type]);
|
|
if ($limit > 0 && $count >= $limit) {
|
|
// if there are newer open sessions, bayond the allowed limit, destroy the current session
|
|
$reason = 'concurrent';
|
|
$ret = false;
|
|
}
|
|
//}
|
|
} elseif ($this->strategy == self::VALIDATION_EXPIRATION) {
|
|
$key = 'last_operation_u_'.$current_user->id;
|
|
$tocheck = time() - $cfg['expiration_time']*60;
|
|
$ret = (VteSession::isEmpty($key) || VteSession::get($key) > $tocheck);
|
|
if (!$ret) $reason = 'expired';
|
|
}
|
|
// crmv@106590e
|
|
|
|
if (!$ret) {
|
|
$this->log("The session has expired");
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* Mark the session as active right now, resetting any timer
|
|
*/
|
|
public function refresh() {
|
|
global $current_user;
|
|
|
|
$ret = true;
|
|
$cfg = $this->getConfig($this->strategy);
|
|
|
|
if (!$current_user || !$current_user->id) return false;
|
|
|
|
|
|
if ($this->strategy == self::VALIDATION_NONE) {
|
|
// do nothing
|
|
} elseif ($this->strategy == self::VALIDATION_CONCURRENT_LOGIN) {
|
|
// write in the table the time of the last activity, but only if the cache is not expired
|
|
$key = 'last_operation_u_'.$current_user->id;
|
|
$keyTs = 'last_operation_ts_u_'.$current_user->id;
|
|
$checkTs = time() - $cfg['cache_duration']*60;
|
|
if (VteSession::isEmpty($keyTs) || VteSession::get($keyTs) < $checkTs) {
|
|
$LH = LoginHistory::getInstance();
|
|
$LH->user_activity($current_user->user_name);
|
|
VteSession::set($keyTs, time());
|
|
}
|
|
VteSession::set($key, time());
|
|
} elseif ($this->strategy == self::VALIDATION_EXPIRATION) {
|
|
$key = 'last_operation_u_'.$current_user->id;
|
|
VteSession::set($key, time());
|
|
}
|
|
|
|
$this->log("Current session refreshed");
|
|
return $ret;
|
|
}
|
|
|
|
// crmv@101201
|
|
|
|
/**
|
|
* Save some session variables for the next re-login (only used when doing ajax login)
|
|
*/
|
|
public function saveSessionVars($userid) {
|
|
if (is_writable($this->preserveSessionPath)) {
|
|
$vars = array_intersect_key($_SESSION, array_flip($this->preserveSessionVars));
|
|
$filename = $this->preserveSessionPath.'session_'.$userid.'.tmp';
|
|
@file_put_contents($filename, serialize($vars));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore some previously saved session variables
|
|
*/
|
|
public function restoreSessionVars($userid) {
|
|
$filename = $this->preserveSessionPath.'session_'.$userid.'.tmp';
|
|
if (is_readable($filename)) {
|
|
// check the time
|
|
if (filemtime($filename) >= time() - $this->preserveSessionDuration*60) {
|
|
$vars = @file_get_contents($filename);
|
|
if ($vars) {
|
|
$vars = unserialize($vars);
|
|
if (is_array($vars)) {
|
|
// crmv@128133
|
|
VteSession::reopen(true);
|
|
foreach ($vars as $key => $value) {
|
|
VteSession::set($key, $value);
|
|
}
|
|
VteSession::close(true);
|
|
// crmv@128133e
|
|
}
|
|
}
|
|
}
|
|
// remove it
|
|
@unlink($filename);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the stored session variables
|
|
*/
|
|
public function clearSessionVars($userid) {
|
|
$filename = $this->preserveSessionPath.'session_'.$userid.'.tmp';
|
|
if (is_readable($filename)) {
|
|
@unlink($filename);
|
|
}
|
|
}
|
|
// crmv@101201e
|
|
|
|
public function saveUser() {
|
|
global $current_user;
|
|
|
|
$ret = true;
|
|
$cfg = $this->getConfig($this->strategy);
|
|
|
|
if ($this->strategy == self::VALIDATION_NONE) return $ret;
|
|
|
|
// crmv@146653
|
|
if ($cfg['cookie_name']) {
|
|
// this is not a risk, since the user have to login with the right credentials anyway
|
|
$cookieurl = str_replace('index.php', '', $_SERVER['SCRIPT_NAME']) ?: '/';
|
|
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off');
|
|
setcookie($cfg['cookie_name'], $current_user->user_name, 0, $cookieurl, '', $isHttps, true);
|
|
}
|
|
// crmv@146653e
|
|
|
|
$this->log("Current user saved");
|
|
return $ret;
|
|
}
|
|
|
|
|
|
public function userChanged($focus) {
|
|
$cfg = $this->getConfig($this->strategy);
|
|
|
|
$ret = false;
|
|
if ($this->strategy == self::VALIDATION_NONE) {
|
|
// do nothing
|
|
return null;
|
|
} elseif ($this->strategy == self::VALIDATION_CONCURRENT_LOGIN) {
|
|
|
|
} elseif ($this->strategy == self::VALIDATION_EXPIRATION) {
|
|
$cname = $cfg['cookie_name'];
|
|
$ret = ($_COOKIE[$cname] != $focus->column_fields["user_name"]);
|
|
}
|
|
|
|
if ($ret) {
|
|
$this->log("The new user is different ({$_COOKIE[$cname]} -> {$focus->column_fields['user_name']})");
|
|
} else {
|
|
$this->log("The new user is the same ({$focus->column_fields['user_name']})");
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
public function ajaxOutput($output = array()) {
|
|
echo Zend_Json::encode($output);
|
|
die();
|
|
}
|
|
|
|
public function getSessionId() {
|
|
return session_id();
|
|
}
|
|
|
|
protected function log($message) {
|
|
global $current_user;
|
|
$uname = ($current_user ? $current_user->column_fields['user_name'] : '');
|
|
$sessid = $this->getSessionId();
|
|
$msg = "[".date('Y-m-d H:i:s')."] [$sessid] $uname: $message\n";
|
|
// output, uncomment if needed
|
|
//file_put_contents('logs/session.log', $msg, FILE_APPEND);
|
|
}
|
|
|
|
} |