mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
N°6901 - Enable tracking of iTop active sessions (monitoring) (#568)
Co-authored-by: Molkobain <lajarige.guillaume@free.fr> Co-authored-by: Romain Quetiez <romain.quetiez@combodo.com>
This commit is contained in:
@@ -1123,7 +1123,7 @@ class OQLMenuNode extends MenuNode
|
||||
*/
|
||||
public function RenderContent(WebPage $oPage, $aExtraParams = array())
|
||||
{
|
||||
ContextTag::AddContext(ContextTag::TAG_OBJECT_SEARCH);
|
||||
$oTag = new ContextTag(ContextTag::TAG_OBJECT_SEARCH);
|
||||
ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId());
|
||||
OQLMenuNode::RenderOQLSearch
|
||||
(
|
||||
|
||||
@@ -1193,6 +1193,30 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'sessions_tracking.enabled' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'Whether or not the whole mechanism to track active sessions is enabled. See PHP session.gc_maxlifetime setting to configure session expiration.',
|
||||
'default' => false,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'sessions_tracking.gc_threshold' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'fallback in case cron is not active: probability in percent that session files are cleanup during any itop request (100 means always)',
|
||||
'default' => 1,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'sessions_tracking.gc_duration_in_seconds' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'fallback in case cron is not active: when a cleanup is triggered cleanup duration will not exceed this duration (in seconds).',
|
||||
'default' => 1,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'transaction_storage' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The type of mechanism to use for storing the unique identifiers for transactions (Session|File).',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// This file is part of iTop.
|
||||
//
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
@@ -576,6 +576,11 @@ class LogChannels
|
||||
public const DATATABLE = 'Datatable';
|
||||
|
||||
public const DEADLOCK = 'DeadLock';
|
||||
/**
|
||||
* @var string Everything related to PHP sessions tracking
|
||||
* @since 3.1.1 3.2.0 N°6901
|
||||
*/
|
||||
public const SESSIONTRACKER = 'SessionTracker';
|
||||
|
||||
/**
|
||||
* @var string Everything related to the datamodel CRUD
|
||||
|
||||
@@ -476,6 +476,8 @@ return array(
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectManager' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectManager.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectRepository' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectRepository.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectsEvents' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectsEvents.php',
|
||||
'Combodo\\iTop\\SessionTracker\\SessionGC' => $baseDir . '/sources/SessionTracker/SessionGC.php',
|
||||
'Combodo\\iTop\\SessionTracker\\SessionHandler' => $baseDir . '/sources/SessionTracker/SessionHandler.php',
|
||||
'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php',
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
'Config' => $baseDir . '/core/config.class.inc.php',
|
||||
|
||||
@@ -840,6 +840,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectManager' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectManager.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectRepository' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectRepository.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectsEvents' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectsEvents.php',
|
||||
'Combodo\\iTop\\SessionTracker\\SessionGC' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionGC.php',
|
||||
'Combodo\\iTop\\SessionTracker\\SessionHandler' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionHandler.php',
|
||||
'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php',
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php',
|
||||
|
||||
@@ -69,7 +69,7 @@ try
|
||||
break;
|
||||
|
||||
default:
|
||||
ContextTag::AddContext(ContextTag::TAG_CONSOLE);
|
||||
$oTag = new ContextTag(ContextTag::TAG_CONSOLE);
|
||||
}
|
||||
|
||||
// First check if we can redirect the route to a dedicated controller
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
namespace Combodo\iTop\Application\Helper;
|
||||
|
||||
use Combodo\iTop\SessionTracker\SessionHandler;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
@@ -34,6 +35,7 @@ class Session
|
||||
}
|
||||
|
||||
if (!self::$bIsInitialized) {
|
||||
SessionHandler::session_set_save_handler();
|
||||
session_name('itop-'.md5(APPROOT));
|
||||
}
|
||||
|
||||
@@ -214,4 +216,4 @@ class Session
|
||||
|
||||
return utils::IsModeCLI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
sources/SessionTracker/SessionGC.php
Normal file
32
sources/SessionTracker/SessionGC.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\SessionTracker;
|
||||
|
||||
/**
|
||||
* Class SessionGC
|
||||
*
|
||||
* @author Olivier Dain <olivier.dain@combodo.com>
|
||||
* @package Combodo\iTop\SessionTracker
|
||||
* @since 3.1.1 3.2.0 N°6901
|
||||
*/
|
||||
class SessionGC implements \iBackgroundProcess
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function GetPeriodicity()
|
||||
{
|
||||
return 60 * 1; // seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function Process($iTimeLimit)
|
||||
{
|
||||
$iMaxLifetime = ini_get('session.gc_maxlifetime') ?? 1440;
|
||||
$oSessionHandler = new SessionHandler();
|
||||
$iProcessed = $oSessionHandler->gc_with_time_limit($iMaxLifetime, $iTimeLimit);
|
||||
return "processed $iProcessed tasks";
|
||||
}
|
||||
}
|
||||
240
sources/SessionTracker/SessionHandler.php
Normal file
240
sources/SessionTracker/SessionHandler.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\SessionTracker;
|
||||
|
||||
use Combodo\iTop\Application\Helper\Session;
|
||||
use ContextTag;
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
use ReturnTypeWillChange;
|
||||
use UserRights;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* Class SessionHandler:
|
||||
* defaut PHP SessionHandler already relies on files that are accessible by iTop.
|
||||
* this new iTop SessionHandler creates additional session files that are located under iTop folders.
|
||||
* these new session files are meant to monitor the application and contain additional data:
|
||||
* - current user id
|
||||
* - context
|
||||
* - login_mode
|
||||
* - session creation timestamp
|
||||
*
|
||||
* @author Olivier Dain <olivier.dain@combodo.com>
|
||||
* @package Combodo\iTop\SessionTracker
|
||||
* @since 3.1.1 3.2.0 N°6901
|
||||
*/
|
||||
class SessionHandler extends \SessionHandler
|
||||
{
|
||||
/**
|
||||
* @param string $session_id
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function destroy($session_id) : bool
|
||||
{
|
||||
IssueLog::Debug("Destroy PHP session", \LogChannels::SESSIONTRACKER, [
|
||||
'session_id' => $session_id,
|
||||
]);
|
||||
$bRes = parent::destroy($session_id);
|
||||
|
||||
if ($bRes) {
|
||||
$this->unlink_session_file($session_id);
|
||||
}
|
||||
|
||||
return $bRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $max_lifetime
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function gc($max_lifetime) : bool
|
||||
{
|
||||
IssueLog::Debug("Run PHP sessions garbage collector", \LogChannels::SESSIONTRACKER, [
|
||||
'max_lifetime' => $max_lifetime,
|
||||
]);
|
||||
$iRes = parent::gc($max_lifetime);
|
||||
$this->gc_with_time_limit($max_lifetime);
|
||||
return $iRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $save_path
|
||||
* @param string $session_name
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function open($save_path, $session_name) : bool
|
||||
{
|
||||
$bRes = parent::open($save_path, $session_name);
|
||||
|
||||
$session_id = session_id();
|
||||
IssueLog::Debug("Open PHP session", \LogChannels::SESSIONTRACKER, [
|
||||
'session_id' => $session_id,
|
||||
]);
|
||||
|
||||
if ($bRes) {
|
||||
$this->touch_session_file($session_id);
|
||||
}
|
||||
|
||||
return $bRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $session_id
|
||||
* @param string $data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function write($session_id, $data) : bool
|
||||
{
|
||||
$bRes = parent::write($session_id, $data);
|
||||
|
||||
IssueLog::Debug("Write PHP session", \LogChannels::SESSIONTRACKER, [
|
||||
'session_id' => $session_id,
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
if ($bRes) {
|
||||
$this->touch_session_file($session_id);
|
||||
}
|
||||
|
||||
return $bRes;
|
||||
}
|
||||
|
||||
public static function session_set_save_handler() : void
|
||||
{
|
||||
if (false === utils::GetConfig()->Get('sessions_tracking.enabled')){
|
||||
//feature disabled
|
||||
return;
|
||||
}
|
||||
|
||||
$sessionhandler = new SessionHandler();
|
||||
session_set_save_handler($sessionhandler, true);
|
||||
|
||||
$iThreshold = utils::GetConfig()->Get('sessions_tracking.gc_threshold');
|
||||
$iThreshold = min(100, $iThreshold);
|
||||
$iThreshold = max(1, $iThreshold);
|
||||
if ((100 != $iThreshold) && (rand(1, 100) > $iThreshold)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iMaxLifetime = ini_get('session.gc_maxlifetime') ?? 60;
|
||||
$iMaxDurationInSeconds = utils::GetConfig()->Get('sessions_tracking.gc_duration_in_seconds');
|
||||
$sessionhandler->gc_with_time_limit($iMaxLifetime, time() + $iMaxDurationInSeconds);
|
||||
}
|
||||
|
||||
private function generate_session_content(?string $sPreviousFileVersionContent) : ?string
|
||||
{
|
||||
try {
|
||||
$sUserId = UserRights::GetUserId();
|
||||
if (null === $sUserId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Default value in case of
|
||||
// - First time file creation
|
||||
// - Data corruption (not a json / not an array / no previous creation_time key)
|
||||
$iCreationTime = time();
|
||||
|
||||
if (! is_null($sPreviousFileVersionContent)) {
|
||||
$aJson = json_decode($sPreviousFileVersionContent, true);
|
||||
if (is_array($aJson) && array_key_exists('creation_time', $aJson)) {
|
||||
$iCreationTime = $aJson['creation_time'];
|
||||
}
|
||||
}
|
||||
|
||||
return json_encode (
|
||||
[
|
||||
'login_mode' => Session::Get('login_mode'),
|
||||
'user_id' => $sUserId,
|
||||
'creation_time' => $iCreationTime,
|
||||
'context' => implode('|', ContextTag::GetStack())
|
||||
]
|
||||
);
|
||||
} catch(Exception $e) {
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function get_file_path($session_id) : string
|
||||
{
|
||||
return utils::GetDataPath() . "sessions/session_$session_id";
|
||||
}
|
||||
|
||||
private function touch_session_file($session_id) : ?string
|
||||
{
|
||||
if (strlen($session_id) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
clearstatcache();
|
||||
if (! is_dir(utils::GetDataPath() . "sessions")) {
|
||||
@mkdir(utils::GetDataPath() . "sessions");
|
||||
}
|
||||
|
||||
$sFilePath = $this->get_file_path($session_id);
|
||||
|
||||
$sPreviousFileVersionContent = null;
|
||||
if (is_file($sFilePath)) {
|
||||
$sPreviousFileVersionContent = file_get_contents($sFilePath);
|
||||
}
|
||||
$sNewContent = $this->generate_session_content($sPreviousFileVersionContent);
|
||||
if (is_null($sNewContent) || ($sPreviousFileVersionContent === $sNewContent)) {
|
||||
@touch($sFilePath);
|
||||
} else {
|
||||
file_put_contents($sFilePath, $sNewContent);
|
||||
}
|
||||
|
||||
return $sFilePath;
|
||||
}
|
||||
|
||||
private function unlink_session_file($session_id)
|
||||
{
|
||||
$sFilePath = $this->get_file_path($session_id);
|
||||
if (is_file($sFilePath)) {
|
||||
@unlink($sFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $max_lifetime
|
||||
* @param int $iTimeLimit Unix timestamp of time limit not to exceed. -1 for no limit.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function gc_with_time_limit(int $max_lifetime, int $iTimeLimit = -1) : int
|
||||
{
|
||||
$aFiles = $this->list_session_files();
|
||||
$iProcessed = 0;
|
||||
$now = time();
|
||||
|
||||
foreach ($aFiles as $sFile) {
|
||||
if ($now - filemtime($sFile) > $max_lifetime) {
|
||||
@unlink($sFile);
|
||||
$iProcessed++;
|
||||
}
|
||||
|
||||
if (-1 !== $iTimeLimit && time() > $iTimeLimit) {
|
||||
//cleanup processing has to stop after $iTimeLimit
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $iProcessed;
|
||||
}
|
||||
|
||||
public function list_session_files() : array
|
||||
{
|
||||
clearstatcache();
|
||||
if (! is_dir(utils::GetDataPath() . "sessions")) {
|
||||
@mkdir(utils::GetDataPath() . "sessions");
|
||||
}
|
||||
|
||||
return glob(utils::GetDataPath() . "sessions/session_*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\SessionTracker;
|
||||
|
||||
use Combodo\iTop\Application\Helper\Session;
|
||||
use Combodo\iTop\SessionTracker\SessionHandler;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use ContextTag;
|
||||
|
||||
class SessionHandlerTest extends ItopDataTestCase
|
||||
{
|
||||
private $aFiles ;
|
||||
private $oTag ;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->aFiles=[];
|
||||
$this->oTag = new ContextTag(ContextTag::TAG_REST);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
$this->oTag = null;
|
||||
|
||||
foreach ($this->aFiles as $sFile){
|
||||
if (is_file($sFile)){
|
||||
@unlink($sFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function CreateUserAndLogIn() : ? string {
|
||||
$_SESSION = [];
|
||||
$oUser = $this->CreateContactlessUser("admin" . uniqid(), 1, "1234@Abcdefg");
|
||||
|
||||
\UserRights::Login($oUser->Get('login'));
|
||||
return $oUser->GetKey();
|
||||
}
|
||||
|
||||
private function GenerateSessionContent(SessionHandler $oSessionHandler, ?string $sPreviousFileVersionContent) : ?string {
|
||||
return $this->InvokeNonPublicMethod(SessionHandler::class, "generate_session_content", $oSessionHandler, $aArgs = [$sPreviousFileVersionContent]);
|
||||
}
|
||||
|
||||
/*
|
||||
* @covers SessionHandler::generate_session_content
|
||||
*/
|
||||
public function testGenerateSessionContentNoUserLoggedIn(){
|
||||
$oSessionHandler = new SessionHandler();
|
||||
$sContent = $this->GenerateSessionContent($oSessionHandler, null);
|
||||
$this->assertNull($sContent, "Session content should be null when there is no user logged in");
|
||||
}
|
||||
|
||||
public function GenerateSessionContentCorruptedPreviousFileContentProvider() {
|
||||
return [
|
||||
'not a json' => [ "not a json" ],
|
||||
'not an array' => [ json_encode("not an array") ],
|
||||
'array without creation_time key' => [ json_encode([]) ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers SessionHandler::generate_session_content
|
||||
* @dataProvider GenerateSessionContentCorruptedPreviousFileContentProvider
|
||||
*/
|
||||
public function testGenerateSessionContent_SessionFileRepairment(?string $sFileContent){
|
||||
$sUserId = $this->CreateUserAndLogIn();
|
||||
|
||||
$oSessionHandler = new SessionHandler();
|
||||
Session::Set('login_mode', 'foo_login_mode');
|
||||
|
||||
$sContent = $this->GenerateSessionContent($oSessionHandler, $sFileContent);
|
||||
|
||||
$this->assertNotNull($sContent, 'Should not return null');
|
||||
$aJson = json_decode($sContent, true);
|
||||
$this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sContent);
|
||||
$this->assertEquals($sUserId, $aJson['user_id'] ?? '', "Should report the login of the logged in user in [user_id]: $sContent");
|
||||
$this->assertEquals(ContextTag::TAG_REST, $aJson['context'] ?? '', "Should report the context tag(s) in [context]: $sContent");
|
||||
$this->assertIsInt($aJson['creation_time'] ?? '', "Should report the session start timestamp in [creation_time]: $sContent");
|
||||
$this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "Should report the current login mode in [login_mode]: $sContent");
|
||||
}
|
||||
|
||||
/*
|
||||
* @covers SessionHandler::generate_session_content
|
||||
*/
|
||||
public function testGenerateSessionContent(){
|
||||
$sUserId = $this->CreateUserAndLogIn();
|
||||
|
||||
$oSessionHandler = new SessionHandler();
|
||||
Session::Set('login_mode', 'foo_login_mode');
|
||||
|
||||
//first time
|
||||
$sFirstContent = $this->GenerateSessionContent($oSessionHandler, null);
|
||||
|
||||
$this->assertNotNull($sFirstContent, 'Should not return null');
|
||||
$aJson = json_decode($sFirstContent, true);
|
||||
$this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sFirstContent);
|
||||
$this->assertEquals($sUserId, $aJson['user_id'] ?? '', "Should report the login of the logged in user in [user_id]: $sFirstContent");
|
||||
$this->assertEquals(ContextTag::TAG_REST, $aJson['context'] ?? '', "Should report the context tag(s) in [context]: $sFirstContent");
|
||||
$this->assertIsInt($aJson['creation_time'] ?? '', "Should report the session start timestamp in [creation_time]: $sFirstContent");
|
||||
$this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "Should report the current login mode in [login_mode]: $sFirstContent");
|
||||
|
||||
$iFirstSessionCreationTime = $aJson['creation_time'];
|
||||
|
||||
// Switch context + change user id via impersonation
|
||||
// check it is still tracked in session files
|
||||
$oOtherUser = $this->CreateContactlessUser("admin" . uniqid(), 1, "1234@Abcdefg");
|
||||
$this->assertTrue(\UserRights::Impersonate($oOtherUser->Get('login')), "Failed to execute impersonate on: ".$oOtherUser->Get('login'));
|
||||
$oTag2 = new ContextTag(ContextTag::TAG_SYNCHRO);
|
||||
$sNewContent = $this->GenerateSessionContent($oSessionHandler, $sFirstContent);
|
||||
$this->assertNotNull($sNewContent, 'Should not return null');
|
||||
$aJson = json_decode($sNewContent, true);
|
||||
$this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sNewContent);
|
||||
$this->assertEquals(ContextTag::TAG_REST . '|' . ContextTag::TAG_SYNCHRO, $aJson['context'] ?? '', "After impersonation, should report the new context tags in [context]: $sNewContent");
|
||||
$this->assertEquals($iFirstSessionCreationTime, $aJson['creation_time'] ?? '', "After impersonation, should still report the the session start timestamp in [creation_time]: $sNewContent");
|
||||
$this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "After impersonation, should still report the login mode in [login_mode]: $sNewContent");
|
||||
$this->assertEquals($oOtherUser->GetKey(), $aJson['user_id'] ?? '', "Should report the impersonate user in [user_id]: $sNewContent");
|
||||
}
|
||||
|
||||
private function touchSessionFile(SessionHandler $oSessionHandler, $session_id) : ?string {
|
||||
$sRes = $this->InvokeNonPublicMethod(SessionHandler::class, "touch_session_file", $oSessionHandler, $aArgs = [$session_id]);
|
||||
if (!is_null($sRes) && is_file($sRes)) {
|
||||
// Record the file for cleanup on tearDown
|
||||
$this->aFiles[] = $sRes;
|
||||
}
|
||||
clearstatcache();
|
||||
return $sRes;
|
||||
}
|
||||
|
||||
/*
|
||||
* @covers SessionHandler::touch_session_file
|
||||
*/
|
||||
public function testTouchSessionFile_NoUserLoggedIn(){
|
||||
$oSessionHandler = new SessionHandler();
|
||||
$session_id = uniqid();
|
||||
$sFile = $this->touchSessionFile($oSessionHandler, $session_id);
|
||||
$this->assertEquals(true, is_file($sFile), "Should return a file name: '$sFile' is not a valid file name");
|
||||
$sContent = file_get_contents($sFile);
|
||||
$this->assertEquals(null, $sContent, 'Should create an empty file, found: '.$sContent);
|
||||
}
|
||||
|
||||
/*
|
||||
* @covers SessionHandler::touch_session_file
|
||||
*/
|
||||
public function testTouchSessionFile_UserLoggedIn(){
|
||||
$sUserId = $this->CreateUserAndLogIn();
|
||||
Session::Set('login_mode', 'foo_login_mode');
|
||||
|
||||
$oSessionHandler = new SessionHandler();
|
||||
$session_id = uniqid();
|
||||
$sFile = $this->touchSessionFile($oSessionHandler, $session_id);
|
||||
$this->assertEquals(true, is_file($sFile), "Should return a file name: '$sFile' is not a valid file name");
|
||||
$sFirstContent = file_get_contents($sFile);
|
||||
|
||||
$iFirstCTime = filectime($sFile) - 1;
|
||||
// Set it in the past to check that it will be further updated (without the need to sleep...)
|
||||
touch($sFile, $iFirstCTime);
|
||||
|
||||
$this->assertNotNull($sFirstContent, 'Should not return null');
|
||||
$aJson = json_decode($sFirstContent, true);
|
||||
$this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sFirstContent);
|
||||
$this->assertEquals($sUserId, $aJson['user_id'] ?? '', "Should report the login of the logged in user in [user_id]: $sFirstContent");
|
||||
$this->assertEquals(ContextTag::TAG_REST, $aJson['context'] ?? '', "Should report the context tag(s) in [context]: $sFirstContent");
|
||||
$this->assertIsInt($aJson['creation_time'] ?? '', "Should report the session start timestamp in [creation_time]: $sFirstContent");
|
||||
$this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "Should report the current login mode in [login_mode]: $sFirstContent");
|
||||
|
||||
$this->touchSessionFile($oSessionHandler, $session_id);
|
||||
$sNewContent = file_get_contents($sFile);
|
||||
$this->assertEquals($sFirstContent, $sNewContent, 'On successive calls, should not modify an existing session file');
|
||||
$this->assertGreaterThan($iFirstCTime, filectime($sFile), 'On successive calls, should have changed the file ctime');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers SessionHandler::touch_session_file
|
||||
*/
|
||||
public function testTouchSessionFileWithEmptySessionId() {
|
||||
$this->CreateUserAndLogIn();
|
||||
Session::Set('login_mode', 'toto');
|
||||
|
||||
$oSessionHandler = new SessionHandler();
|
||||
$this->assertNull($this->touchSessionFile($oSessionHandler, ''), 'Should return null when session id is an empty string');
|
||||
$this->assertNull($this->touchSessionFile($oSessionHandler, false), 'Should return null when session id (boolean) false');
|
||||
}
|
||||
|
||||
private function GetFilePath(SessionHandler $oSessionHandler, $session_id) : string {
|
||||
$sFile = $this->InvokeNonPublicMethod(SessionHandler::class, "get_file_path", $oSessionHandler, $aArgs = [$session_id]);
|
||||
// Record file for cleanup on tearDown
|
||||
$this->aFiles[] = $sFile;
|
||||
return $sFile;
|
||||
}
|
||||
|
||||
public function GgcWithTimeLimitProvider(){
|
||||
return [
|
||||
'no cleanup time limit' => [
|
||||
'iTimeLimit' => -1,
|
||||
'iExpectedProcessed' => 2
|
||||
],
|
||||
'cleanup time limit in the pass => first file removed only' => [
|
||||
'iTimeLimit' => time() - 1,
|
||||
'iExpectedProcessed' => 1
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers SessionHandler::gc_with_time_limit
|
||||
* @covers SessionHandler::list_session_files
|
||||
* @dataProvider GgcWithTimeLimitProvider
|
||||
*/
|
||||
public function testGgcWithTimeLimit($iTimeLimit, $iExpectedProcessed) {
|
||||
$oSessionHandler = new SessionHandler();
|
||||
//remove all first
|
||||
$oSessionHandler->gc_with_time_limit(-1);
|
||||
$this->assertEquals([], $oSessionHandler->list_session_files(), 'list_session_files should report no file at startup');
|
||||
|
||||
$max_lifetime = 1440;
|
||||
$iNbExpiredFiles = 2;
|
||||
$iNbFiles = 5;
|
||||
$iExpiredTimeStamp = time() - $max_lifetime - 1;
|
||||
for($i=0; $i<$iNbFiles; $i++) {
|
||||
$sFile = $this->GetFilePath($oSessionHandler, uniqid());
|
||||
file_put_contents($sFile, "fakedata");
|
||||
|
||||
if ($iNbExpiredFiles > 0){
|
||||
$iNbExpiredFiles--;
|
||||
touch($sFile, $iExpiredTimeStamp);
|
||||
}
|
||||
}
|
||||
|
||||
$aFoundSessionFiles = $oSessionHandler->list_session_files();
|
||||
$this->assertEquals($iNbFiles, sizeof($aFoundSessionFiles), 'list_session_files should reports all files');
|
||||
foreach ($aFoundSessionFiles as $sFile){
|
||||
$this->assertTrue(is_file($sFile), 'list_session_files should return a valid file paths, found: '.$sFile);
|
||||
}
|
||||
|
||||
$iProcessed = $oSessionHandler->gc_with_time_limit($max_lifetime, $iTimeLimit);
|
||||
$this->assertEquals($iExpectedProcessed, $iProcessed, 'gc_with_time_limit should report the count of expired files');
|
||||
$this->assertEquals($iNbFiles - $iExpectedProcessed, sizeof($oSessionHandler->list_session_files()), 'gc_with_time_limit should actually remove all processed files');
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ function RunTask(BackgroundTask $oTask, $iTimeLimit)
|
||||
// Time in seconds allowed to the task
|
||||
$iCurrTimeLimit = $iTimeLimit;
|
||||
// Compute allowed time
|
||||
if ($oRefClass->implementsInterface('iScheduledProcess') === false)
|
||||
if ($oRefClass->implementsInterface('iScheduledProcess') === false)
|
||||
{
|
||||
// Periodic task, allow only X times ($iMaxTaskExecutionTime) its periodicity (GetPeriodicity())
|
||||
$iMaxTaskExecutionTime = MetaModel::GetConfig()->Get('cron_task_max_execution_time');
|
||||
@@ -148,7 +148,7 @@ function RunTask(BackgroundTask $oTask, $iTimeLimit)
|
||||
$oTask->Set('first_run_date', $oDateStarted->format('Y-m-d H:i:s'));
|
||||
}
|
||||
$oTask->ComputeDurations($fDuration); // does increment the counter and compute statistics
|
||||
|
||||
|
||||
// Update the timestamp since we want to be able to re-order the tasks based on the time they finished
|
||||
$oDateEnded = new DateTime();
|
||||
$oTask->Set('latest_run_date', $oDateEnded->format('Y-m-d H:i:s'));
|
||||
|
||||
Reference in New Issue
Block a user