diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 857dfbaff..66545f161 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -1233,6 +1233,14 @@ class Config 'source_of_value' => '', 'show_in_conf_sample' => false, ], + 'sessions_tracking.session_handler_extension' => [ + 'type' => 'string', + 'description' => 'to store more data in itop session files, set your own iSessionHandlerExtension implementation class in this variable', + 'default' => '', + '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)', diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 270f7b0bd..095101caa 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -525,6 +525,7 @@ return array( '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', + 'Combodo\\iTop\\SessionTracker\\iSessionHandlerExtension' => $baseDir . '/sources/SessionTracker/iSessionHandlerExtension.php', 'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Config' => $baseDir . '/core/config.class.inc.php', @@ -1560,6 +1561,7 @@ return array( 'Sabberworm\\CSS\\Value\\URL' => $vendorDir . '/sabberworm/php-css-parser/src/Value/URL.php', 'Sabberworm\\CSS\\Value\\Value' => $vendorDir . '/sabberworm/php-css-parser/src/Value/Value.php', 'Sabberworm\\CSS\\Value\\ValueList' => $vendorDir . '/sabberworm/php-css-parser/src/Value/ValueList.php', + 'SanitizeTrait' => $baseDir . '/core/restservices.class.inc.php', 'ScalarExpression' => $baseDir . '/core/oql/expression.class.inc.php', 'ScalarOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php', 'ScssPhp\\ScssPhp\\Base\\Range' => $vendorDir . '/scssphp/scssphp/src/Base/Range.php', @@ -3201,6 +3203,7 @@ return array( 'iPreferencesExtension' => $baseDir . '/application/applicationextension.inc.php', 'iProcess' => $baseDir . '/core/backgroundprocess.inc.php', 'iQueryModifier' => $baseDir . '/core/querymodifier.class.inc.php', + 'iRestInputSanitizer' => $baseDir . '/application/applicationextension.inc.php', 'iRestServiceProvider' => $baseDir . '/application/applicationextension.inc.php', 'iScheduledProcess' => $baseDir . '/core/backgroundprocess.inc.php', 'iSelfRegister' => $baseDir . '/core/userrights.class.inc.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 9ecf11d57..5acd9e9b7 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -915,6 +915,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f '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', + 'Combodo\\iTop\\SessionTracker\\iSessionHandlerExtension' => __DIR__ . '/../..' . '/sources/SessionTracker/iSessionHandlerExtension.php', 'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php', @@ -1950,6 +1951,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Sabberworm\\CSS\\Value\\URL' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/URL.php', 'Sabberworm\\CSS\\Value\\Value' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/Value.php', 'Sabberworm\\CSS\\Value\\ValueList' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/ValueList.php', + 'SanitizeTrait' => __DIR__ . '/../..' . '/core/restservices.class.inc.php', 'ScalarExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php', 'ScalarOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php', 'ScssPhp\\ScssPhp\\Base\\Range' => __DIR__ . '/..' . '/scssphp/scssphp/src/Base/Range.php', @@ -3591,6 +3593,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'iPreferencesExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php', 'iProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php', 'iQueryModifier' => __DIR__ . '/../..' . '/core/querymodifier.class.inc.php', + 'iRestInputSanitizer' => __DIR__ . '/../..' . '/application/applicationextension.inc.php', 'iRestServiceProvider' => __DIR__ . '/../..' . '/application/applicationextension.inc.php', 'iScheduledProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php', 'iSelfRegister' => __DIR__ . '/../..' . '/core/userrights.class.inc.php', diff --git a/sources/SessionTracker/SessionHandler.php b/sources/SessionTracker/SessionHandler.php index 5aa0c4072..27891a08f 100644 --- a/sources/SessionTracker/SessionHandler.php +++ b/sources/SessionTracker/SessionHandler.php @@ -139,23 +139,60 @@ class SessionHandler extends \SessionHandler // - Data corruption (not a json / not an array / no previous creation_time key) $iCreationTime = time(); + $aJson=[]; if (! is_null($sPreviousFileVersionContent)) { $aJson = json_decode($sPreviousFileVersionContent, true); - if (is_array($aJson) && array_key_exists('creation_time', $aJson)) { - $iCreationTime = $aJson['creation_time']; + if (is_array($aJson)){ + if (array_key_exists('creation_time', $aJson)) { + $iCreationTime = $aJson['creation_time']; + } + } else { + IssueLog::Warning(__METHOD__ . ': not a json due (session file corruption?)', null, [ 'sPreviousFileVersionContent' => $sPreviousFileVersionContent ]); + $aJson=[]; } } + $aData = [ + 'login_mode' => Session::Get('login_mode'), + 'user_id' => $sUserId, + 'creation_time' => $iCreationTime, + 'context' => implode('|', ContextTag::GetStack()) + ]; + + $oiSessionHandlerExtension = $this->GetSessionHandlerExtension(); + if (! is_null($oiSessionHandlerExtension)){ + $oiSessionHandlerExtension->CompleteSessionData($aJson, $aData); + } + return json_encode ( - [ - 'login_mode' => Session::Get('login_mode'), - 'user_id' => $sUserId, - 'creation_time' => $iCreationTime, - 'context' => implode('|', ContextTag::GetStack()) - ] + $aData ); } catch(Exception $e) { + IssueLog::Error(__METHOD__, null, [ 'error' => $e->getMessage() ]); + } + return null; + } + + private function GetSessionHandlerExtension() : ?iSessionHandlerExtension + { + $sSessionHandlerExtensionClass = utils::GetConfig()->Get('sessions_tracking.session_handler_extension'); + if (strlen($sSessionHandlerExtensionClass) !=0){ + try{ + if (! class_exists($sSessionHandlerExtensionClass)){ + throw new \Exception("Cannot find class"); + } + + /** @var iSessionHandlerExtension $oSessionHandlerExtension */ + $oSessionHandlerExtension = new $sSessionHandlerExtensionClass; + if ($oSessionHandlerExtension instanceof iSessionHandlerExtension){ + return $oSessionHandlerExtension; + } + + throw new \Exception("Not an instance of iSessionHandlerExtension"); + } catch(\Exception $e) { + IssueLog::Error(__METHOD__ . ': cannot instanciate iSessionHandlerExtension', null, ['sessions_tracking.session_handler_extension' => $sSessionHandlerExtensionClass]); + } } return null; diff --git a/sources/SessionTracker/iSessionHandlerExtension.php b/sources/SessionTracker/iSessionHandlerExtension.php new file mode 100644 index 000000000..edd3ef255 --- /dev/null +++ b/sources/SessionTracker/iSessionHandlerExtension.php @@ -0,0 +1,16 @@ +aFiles=[]; + $this->RequireOnceUnitTestFile('./iSessionHandlerExtensionExamples.php'); + $this->aFiles = []; $this->oTag = new ContextTag(ContextTag::TAG_REST); } @@ -24,47 +26,53 @@ class SessionHandlerTest extends ItopDataTestCase parent::tearDown(); $this->oTag = null; - foreach ($this->aFiles as $sFile){ - if (is_file($sFile)){ + foreach ($this->aFiles as $sFile) { + if (is_file($sFile)) { @unlink($sFile); } } } - private function CreateUserAndLogIn() : ? string { + private function CreateUserAndLogIn(): ?string + { $_SESSION = []; - $oUser = $this->CreateContactlessUser("admin" . uniqid(), 1, "1234@Abcdefg"); + $oUser = $this->CreateContactlessUser("admin".uniqid(), 1, "1234@Abcdefg"); \UserRights::Login($oUser->Get('login')); + return $oUser->GetKey(); } - private function GenerateSessionContent(SessionHandler $oSessionHandler, ?string $sPreviousFileVersionContent) : ?string { + 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(){ + 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() { + 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([]) ], + '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 + * @covers SessionHandler::generate_session_content * @dataProvider GenerateSessionContentCorruptedPreviousFileContentProvider */ - public function testGenerateSessionContent_SessionFileRepairment(?string $sFileContent){ + public function testGenerateSessionContent_SessionFileRepairment(?string $sFileContent) + { $sUserId = $this->CreateUserAndLogIn(); $oSessionHandler = new SessionHandler(); @@ -84,7 +92,8 @@ class SessionHandlerTest extends ItopDataTestCase /* * @covers SessionHandler::generate_session_content */ - public function testGenerateSessionContent(){ + public function testGenerateSessionContent() + { $sUserId = $this->CreateUserAndLogIn(); $oSessionHandler = new SessionHandler(); @@ -105,33 +114,36 @@ class SessionHandlerTest extends ItopDataTestCase // Switch context + change user id via impersonation // check it is still tracked in session files - $oOtherUser = $this->CreateContactlessUser("admin" . uniqid(), 1, "1234@Abcdefg"); + $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(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 { + 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(){ + public function testTouchSessionFile_NoUserLoggedIn() + { $oSessionHandler = new SessionHandler(); $session_id = uniqid(); $sFile = $this->touchSessionFile($oSessionHandler, $session_id); @@ -143,7 +155,8 @@ class SessionHandlerTest extends ItopDataTestCase /* * @covers SessionHandler::touch_session_file */ - public function testTouchSessionFile_UserLoggedIn(){ + public function testTouchSessionFile_UserLoggedIn() + { $sUserId = $this->CreateUserAndLogIn(); Session::Set('login_mode', 'foo_login_mode'); @@ -174,7 +187,8 @@ class SessionHandlerTest extends ItopDataTestCase /** * @covers SessionHandler::touch_session_file */ - public function testTouchSessionFileWithEmptySessionId() { + public function testTouchSessionFileWithEmptySessionId() + { $this->CreateUserAndLogIn(); Session::Set('login_mode', 'toto'); @@ -183,32 +197,36 @@ class SessionHandlerTest extends ItopDataTestCase $this->assertNull($this->touchSessionFile($oSessionHandler, false), 'Should return null when session id (boolean) false'); } - private function GetFilePath(SessionHandler $oSessionHandler, $session_id) : string { + 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(){ + public function GgcWithTimeLimitProvider() + { return [ - 'no cleanup time limit' => [ - 'iTimeLimit' => -1, - 'iExpectedProcessed' => 2 + 'no cleanup time limit' => [ + 'iTimeLimit' => -1, + 'iExpectedProcessed' => 2, ], 'cleanup time limit in the pass => first file removed only' => [ - 'iTimeLimit' => time() - 1, - 'iExpectedProcessed' => 1 + 'iTimeLimit' => time() - 1, + 'iExpectedProcessed' => 1, ], ]; } /** - * @covers SessionHandler::gc_with_time_limit - * @covers SessionHandler::list_session_files + * @covers SessionHandler::gc_with_time_limit + * @covers SessionHandler::list_session_files * @dataProvider GgcWithTimeLimitProvider */ - public function testGgcWithTimeLimit($iTimeLimit, $iExpectedProcessed) { + public function testGgcWithTimeLimit($iTimeLimit, $iExpectedProcessed) + { $oSessionHandler = new SessionHandler(); //remove all first $oSessionHandler->gc_with_time_limit(-1); @@ -218,11 +236,11 @@ class SessionHandlerTest extends ItopDataTestCase $iNbExpiredFiles = 2; $iNbFiles = 5; $iExpiredTimeStamp = time() - $max_lifetime - 1; - for($i=0; $i<$iNbFiles; $i++) { + for ($i = 0; $i < $iNbFiles; $i++) { $sFile = $this->GetFilePath($oSessionHandler, uniqid()); file_put_contents($sFile, "fakedata"); - if ($iNbExpiredFiles > 0){ + if ($iNbExpiredFiles > 0) { $iNbExpiredFiles--; touch($sFile, $iExpiredTimeStamp); } @@ -230,7 +248,7 @@ class SessionHandlerTest extends ItopDataTestCase $aFoundSessionFiles = $oSessionHandler->list_session_files(); $this->assertEquals($iNbFiles, sizeof($aFoundSessionFiles), 'list_session_files should reports all files'); - foreach ($aFoundSessionFiles as $sFile){ + foreach ($aFoundSessionFiles as $sFile) { $this->assertTrue(is_file($sFile), 'list_session_files should return a valid file paths, found: '.$sFile); } @@ -238,4 +256,64 @@ class SessionHandlerTest extends ItopDataTestCase $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'); } -} + + public function testGetSessionHandlerExtension_NoExtension() + { + \utils::GetConfig()->Set('sessions_tracking.session_handler_extension', ''); + + $oSessionHandler = new SessionHandler(); + $oSessionHandlerExtension = $this->InvokeNonPublicMethod(SessionHandler::class, "GetSessionHandlerExtension", $oSessionHandler); + $this->assertNull($oSessionHandlerExtension, "by default no extension"); + } + + public function testGetSessionHandlerExtension_InvalidClassExtension() + { + \utils::GetConfig()->Set('sessions_tracking.session_handler_extension', 'dddf'); + + $oSessionHandler = new SessionHandler(); + $oSessionHandlerExtension = $this->InvokeNonPublicMethod(SessionHandler::class, "GetSessionHandlerExtension", $oSessionHandler); + $this->assertNull($oSessionHandlerExtension); + } + + public function testGetSessionHandlerExtension_InvalidExtension() + { + \utils::GetConfig()->Set('sessions_tracking.session_handler_extension', Session::class); + + $oSessionHandler = new SessionHandler(); + $oSessionHandlerExtension = $this->InvokeNonPublicMethod(SessionHandler::class, "GetSessionHandlerExtension", $oSessionHandler); + $this->assertNull($oSessionHandlerExtension); + } + + public function testGetSessionHandlerExtension_OK() + { + \utils::GetConfig()->Set('sessions_tracking.session_handler_extension', 'Combodo\iTop\Test\UnitTest\SessionTracker\BasicSessionHandlerExtension'); + + $oSessionHandler = new SessionHandler(); + $oSessionHandlerExtension = $this->InvokeNonPublicMethod(SessionHandler::class, "GetSessionHandlerExtension", $oSessionHandler); + $this->assertNotNull($oSessionHandlerExtension, "by default no extension"); + $this->assertTrue($oSessionHandlerExtension instanceof iSessionHandlerExtension); + $this->assertInstanceOf(BasicSessionHandlerExtension::class, $oSessionHandlerExtension); + } + + public function testGenerateSessionContent_WithAdditionalDataProvidedBySessionHandlerExtension() + { + \utils::GetConfig()->Set('sessions_tracking.session_handler_extension', 'Combodo\iTop\Test\UnitTest\SessionTracker\BasicSessionHandlerExtension'); + $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"); + + $this->assertEquals('gabuzomeu', $aJson['shadok'] ?? '', "Should report the current login mode in [shadok]: $sFirstContent"); + } +} \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/sources/SessionTracker/iSessionHandlerExtensionExamples.php b/tests/php-unit-tests/unitary-tests/sources/SessionTracker/iSessionHandlerExtensionExamples.php new file mode 100644 index 000000000..b4c1a1241 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/sources/SessionTracker/iSessionHandlerExtensionExamples.php @@ -0,0 +1,15 @@ +