Set('transactions_gc_threshold', 100); $iOriginalLifetime = (int) $oConfig->Get('transactions_file_lifetime'); $iBaseLimit = time() - $iOriginalLifetime; $sBaseDir = sys_get_temp_dir(); $sDir = "$sBaseDir/privUITransactionFileTest/cleanupOldTransactions"; if (is_dir($sDir)) { $this->rm($sDir); } mkdir("$sDir", 0777, true); for ($i = 0; $i < $iCleanableCreated; $i++) { touch("$sDir/{$sCleanablePrefix}$i", $iBaseLimit - 10 * 60); } for ($i = 0; $i < $iPreservableCreated; $i++) { touch("$sDir/{$sPreservablePrefix}$i", $iBaseLimit + 10 * 60); } $iCleanableCount = count(glob("$sDir/{$sCleanablePrefix}*")); $iPreservableCount = count(glob("$sDir/{$sPreservablePrefix}*")); $this->assertEquals($iCleanableCreated, $iCleanableCount); $this->assertEquals($iPreservableCreated, $iPreservableCount); $aArgs = [ 'sTransactionDir' => "$sDir", ]; $oprivUITransactionFile = new privUITransactionFile(); $this->InvokeNonPublicMethod(get_class($oprivUITransactionFile), 'CleanupOldTransactions', $oprivUITransactionFile, $aArgs); $iCleanableCount = count(glob("$sDir/{$sCleanablePrefix}*")); $iPreservableCount = count(glob("$sDir/{$sPreservablePrefix}*")); $this->assertEquals(0, $iCleanableCount); $this->assertEquals($iPreservableCreated, $iPreservableCount); } public function cleanupOldTransactionsProvider() { $iBaseLimit = time() - 60 * 10; //ten minutes ago $sBaseDir = sys_get_temp_dir(); $sDir = "$sBaseDir/privUITransactionFileTest/cleanupOldTransactions"; return [ 'linux - no content' => [ 'iCleanableCreated' => 0, 'iPreservableCreated' => 0, 'sCleanablePrefix' => 'cleanable-', 'sPreservablePrefix' => 'preservable-', ], 'linux - cleanable content' => [ 'iCleanableCreated' => 2, 'iPreservableCreated' => 0, 'sCleanablePrefix' => 'cleanable-', 'sPreservablePrefix' => 'preservable-', ], 'linux - preseved content' => [ 'iCleanableCreated' => 0, 'iPreservableCreated' => 2, 'sCleanablePrefix' => 'cleanable-', 'sPreservablePrefix' => 'preservable-', ], 'win - no content' => [ 'iCleanableCreated' => 0, 'iPreservableCreated' => 0, 'sCleanablePrefix' => 'cle', 'sPreservablePrefix' => 'pre', ], 'win - cleanable content' => [ 'iCleanableCreated' => 2, 'iPreservableCreated' => 0, 'sCleanablePrefix' => 'cle', 'sPreservablePrefix' => 'pre', ], 'win - preseved content' => [ 'iCleanableCreated' => 0, 'iPreservableCreated' => 2, 'sCleanablePrefix' => 'cle', 'sPreservablePrefix' => 'pre', ], ]; } public function rm($sDir) { $aFiles = array_diff(scandir($sDir), ['.','..']); foreach ($aFiles as $sFile) { if ((is_dir("$sDir/$sFile"))) { $this->rm("$sDir/$sFile"); } else { unlink("$sDir/$sFile"); } } return rmdir($sDir); } public const USER_TEST_LOGIN = 'support_test_privUITransaction'; /** * @throws \SecurityException * @uses self::SAMPLE_DATA_SUPPORT_PROFILE_ID * @uses self::USER1_TEST_LOGIN * @uses self::USER2_TEST_LOGIN */ public function testIsTransactionValid() { $this->CreateUser(static::USER1_TEST_LOGIN, self::SAMPLE_DATA_SUPPORT_PROFILE_ID); $this->CreateUser(static::USER2_TEST_LOGIN, self::SAMPLE_DATA_SUPPORT_PROFILE_ID); // created users aren't admin, so each one can't see the other (\UserRights::GetSelectFilter) // If calling \UserRights::Login(user1) then \UserRights::Login(user2) we won't be able to load user2 ! // As now we are in the admin context, we are calling FindUser() directly so that user objects will be saved in the UserRights cache ! // we can skip doing this for user1 as the first \UserRights::Login call will initialize the UserRights cache ! $this->InvokeNonPublicStaticMethod(UserRights::class, 'FindUser', [self::USER2_TEST_LOGIN]); // create token in the user1 context $bUser1Login1 = UserRights::Login(self::USER1_TEST_LOGIN); $this->assertTrue($bUser1Login1, 'Login with user1 throw an error'); $sTransactionIdUserSupport = privUITransactionFile::GetNewTransactionId(); $bResult = privUITransactionFile::IsTransactionValid($sTransactionIdUserSupport, false); $this->assertTrue($bResult, 'Token created by support user must be valid in the support user context'); // test token in the user2 context $bUser2Login = UserRights::Login(self::USER2_TEST_LOGIN); $this->assertTrue($bUser2Login, 'Login with user2 throw an error'); $bResult = privUITransactionFile::IsTransactionValid($sTransactionIdUserSupport, false); $this->assertFalse($bResult, 'Token created by support user must be invalid in the admin user context'); $bResult = privUITransactionFile::RemoveTransaction($sTransactionIdUserSupport); $this->assertFalse($bResult, 'Token created by support user cannot be removed in the admin user context'); // test other methods in the user1 context $bUser1Login2 = UserRights::Login(self::USER1_TEST_LOGIN); $this->assertTrue($bUser1Login2, 'Login with user1 throw an error'); $bResult = privUITransactionFile::RemoveTransaction($sTransactionIdUserSupport); $this->assertTrue($bResult, 'Token created by support user must be removed in the support user context'); // test when no user logged (combodo-unauthenticated-form module for example) UserRights::Logoff(); $sTransactionIdUnauthenticatedUser = privUITransactionFile::GetNewTransactionId(); $bResult = privUITransactionFile::IsTransactionValid($sTransactionIdUnauthenticatedUser, false); $this->assertTrue($bResult, 'Token created by unauthenticated user must be valid when no user logged'); $bResult = privUITransactionFile::RemoveTransaction($sTransactionIdUnauthenticatedUser); $this->assertTrue($bResult, 'Token created by unauthenticated user must be removed when no user logged'); } /** * Validate that transactions_file_lifetime drives transaction expiration. * * @throws \Exception */ public function testIsTransactionValidUsesConfiguredFileLifetime() { $this->CreateUser(static::USER1_TEST_LOGIN, self::SAMPLE_DATA_SUPPORT_PROFILE_ID); $bUserLogin = UserRights::Login(self::USER1_TEST_LOGIN); $this->assertTrue($bUserLogin, 'Login with test user throw an error'); $oConfig = MetaModel::GetConfig(); $iOriginalLifetime = (int) $oConfig->Get('transactions_file_lifetime'); $iTestAgeInSeconds = 30; try { $oConfig->Set('transactions_file_lifetime', 3600); $sLongLifetimeTransactionId = privUITransactionFile::GetNewTransactionId(); $sLongLifetimeTransactionFilePath = \utils::GetDataPath().'transactions/'.$sLongLifetimeTransactionId; $bTouchSuccess = touch($sLongLifetimeTransactionFilePath, time() - $iTestAgeInSeconds); $this->assertTrue($bTouchSuccess, 'Unable to age transaction file for long-lifetime scenario'); $bResult = privUITransactionFile::IsTransactionValid($sLongLifetimeTransactionId, false); $this->assertTrue($bResult, 'Transaction should still be valid when its age is below transactions_file_lifetime'); privUITransactionFile::RemoveTransaction($sLongLifetimeTransactionId); $oConfig->Set('transactions_file_lifetime', 5); $sShortLifetimeTransactionId = privUITransactionFile::GetNewTransactionId(); $sShortLifetimeTransactionFilePath = \utils::GetDataPath().'transactions/'.$sShortLifetimeTransactionId; $bTouchSuccess = touch($sShortLifetimeTransactionFilePath, time() - $iTestAgeInSeconds); $this->assertTrue($bTouchSuccess, 'Unable to age transaction file for short-lifetime scenario'); $bResult = privUITransactionFile::IsTransactionValid($sShortLifetimeTransactionId, false); $this->assertFalse($bResult, 'Transaction should be invalid when its age is above transactions_file_lifetime'); } finally { $oConfig->Set('transactions_file_lifetime', $iOriginalLifetime); } } /** * @throws \SecurityException * @throws \Exception */ public function testTransactionIdsContain48RandomHexCharsForSessionAndFileStorage() { $this->CreateUser(static::USER1_TEST_LOGIN, self::SAMPLE_DATA_SUPPORT_PROFILE_ID); $bUserLogin = UserRights::Login(self::USER1_TEST_LOGIN); $this->assertTrue($bUserLogin, 'Login with test user throw an error'); $sSessionTransactionId = \privUITransactionSession::GetNewTransactionId(); $this->assertTransactionIdHas48HexSuffix($sSessionTransactionId); \privUITransactionSession::RemoveTransaction($sSessionTransactionId); $sFileTransactionId = privUITransactionFile::GetNewTransactionId(); $this->assertTransactionIdHas48HexSuffix($sFileTransactionId); privUITransactionFile::RemoveTransaction($sFileTransactionId); } private function assertTransactionIdHas48HexSuffix(string $sTransactionId): void { $aParts = explode('-', $sTransactionId); $sHexSuffix = end($aParts); $this->assertNotFalse($sHexSuffix, 'Transaction ID suffix is missing'); $this->assertSame(48, strlen($sHexSuffix), "Transaction ID '$sTransactionId' must end with 48 hex chars"); $this->assertMatchesRegularExpression('/^[a-f0-9]{48}$/', $sHexSuffix, "Transaction ID '$sTransactionId' suffix must be lowercase hexadecimal"); } }