Compare commits

...

8 Commits

Author SHA1 Message Date
Benjamin DALSASS
4904d60bba N°8420 - [SECU] Improve transaction ID mechanism
- greptile feedback
2026-08-18 08:24:19 +02:00
Benjamin Dalsass
b7cd3a331a Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-18 08:07:44 +02:00
Benjamin Dalsass
c026461864 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-18 08:06:58 +02:00
Benjamin DALSASS
88602c407f N°8420 - [SECU] Improve transaction ID mechanism
- update synchro replia usage
2026-08-18 07:32:21 +02:00
Benjamin DALSASS
23ba2581aa N°8420 - [SECU] Improve transaction ID mechanism
- remove unexisting helper
2026-08-17 16:42:31 +02:00
Benjamin DALSASS
ed6edb5a3e N°8420 - [SECU] Improve transaction ID mechanism
- remove unwanted dictionnary line
2026-08-17 16:37:38 +02:00
Benjamin DALSASS
1188ffc221 N°8420 - [SECU] Improve transaction ID mechanism
- add unitary test
2026-08-17 16:33:59 +02:00
Benjamin DALSASS
28bafd95b4 N°8420 - [SECU] Improve transaction ID mechanism 2026-08-17 16:19:44 +02:00
25 changed files with 160 additions and 51 deletions

View File

@@ -5203,7 +5203,7 @@ EOF
// Not in preview mode, do the update for real
$sTransactionId = utils::ReadPostedParam('transaction_id', '', 'transaction_id');
if (!utils::IsTransactionValid($sTransactionId, false)) {
throw new Exception(Dict::S('UI:Error:ObjectAlreadyUpdated'));
throw new Exception(Dict::S('UI:Error:InvalidToken'));
}
utils::RemoveTransaction($sTransactionId);
}

View File

@@ -115,7 +115,7 @@ class privUITransactionSession
// Strictly speaking, the two lines below should be grouped together
// by a critical section
// sem_acquire($rSemIdentified);
$id = static::GetUserPrefix().str_replace(['.', ' '], '', microtime());
$id = static::GetUserPrefix().str_replace(['.', ' '], '', microtime()).'-'.bin2hex(random_bytes(24));
Session::Set(['transactions', $id], true);
// sem_release($rSemIdentified);
@@ -236,7 +236,7 @@ class privUITransactionFile
self::CleanupOldTransactions();
$sTransactionIdFullPath = tempnam(APPROOT.'data/transactions', static::GetUserPrefix());
$sTransactionIdFullPath = static::CreateUniqueTransactionFilePath(APPROOT.'data/transactions', static::GetUserPrefix());
file_put_contents($sTransactionIdFullPath, $iCurrentUserId, LOCK_EX);
$sTransactionIdFileName = basename($sTransactionIdFullPath);
@@ -245,6 +245,33 @@ class privUITransactionFile
return $sTransactionIdFileName;
}
/**
* Create a transaction file path with a longer random suffix and create it atomically.
*
* @param string $sDir
* @param string $sPrefix
*
* @return string
*
* @throws CoreException
*/
private static function CreateUniqueTransactionFilePath(string $sDir, string $sPrefix): string
{
$iMaxAttempts = 10;
for ($i = 0; $i < $iMaxAttempts; $i++) {
$sFileName = $sPrefix.bin2hex(random_bytes(24)); // 48 random hex chars
$sFullPath = $sDir.'/'.$sFileName;
$hFile = @fopen($sFullPath, 'x');
if ($hFile !== false) {
fclose($hFile);
return $sFullPath;
}
}
throw new CoreException('Failed to allocate a unique transaction file name after multiple attempts.');
}
/**
* Check whether a transaction is valid or not and (optionally) remove the valid transaction from
* the session so that another call to IsTransactionValid for the same transaction id
@@ -281,6 +308,18 @@ class privUITransactionFile
return false;
}
$iTime = filemtime($sFilepath);
$iLifetime = (int) MetaModel::GetConfig()->Get('transactions_file_lifetime');
$iLimit = time() - $iLifetime;
if ($iTime < $iLimit) {
self::Info("IsTransactionValid: Transaction '$id' expired.");
if ($iLifetime < 3600) {
IssueLog::Warning("IsTransactionValid: Transaction '$id' expired after only $iLifetime seconds. Consider increasing the 'transactions_file_lifetime' configuration parameter.");
}
@unlink($sFilepath);
return false;
}
if ($bRemoveTransaction) {
$bResult = @unlink($sFilepath);
if (!$bResult) {
@@ -314,7 +353,7 @@ class privUITransactionFile
}
/**
* Cleanup old transactions which have been pending since more than 24 hours
* Cleanup old transactions which have been pending since more than the lifetime defined in the configuration parameter 'transactions_file_lifetime'.
* Use filemtime instead of filectime since filectime may be affected by operations on the directory (like changing the access rights)
*/
protected static function CleanupOldTransactions($sTransactionDir = null)
@@ -327,7 +366,7 @@ class privUITransactionFile
}
clearstatcache();
$iLimit = time() - 24 * 3600;
$iLimit = time() - (int) MetaModel::GetConfig()->Get('transactions_file_lifetime');
$sPattern = $sTransactionDir ? "$sTransactionDir/*" : APPROOT.'data/transactions/*';
$aTransactions = glob($sPattern);
foreach ($aTransactions as $sFileName) {

View File

@@ -1289,6 +1289,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'transactions_file_lifetime' => [
'type' => 'integer',
'description' => 'Value in seconds for the lifetime of a transaction file (after this duration, the transaction will be invalid and the garbage collector could delete it).',
'default' => 3600,
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'log_transactions' => [
'type' => 'bool',
'description' => 'Whether or not to enable the debug log for the transactions.',

View File

@@ -793,12 +793,7 @@ class ObjectFormManager extends FormManager
{
$isTransactionValid = \utils::IsTransactionValid($this->oForm->GetTransactionId(), false); //The transaction token is kept in order to preserve BC with ajax forms (the second call would fail if the token is deleted). (The GC will take care of cleaning the token for us later on)
if (!$isTransactionValid) {
if ($this->oObject->IsNew()) {
$sError = Dict::S('UI:Error:ObjectAlreadyCreated');
} else {
$sError = Dict::S('UI:Error:ObjectAlreadyUpdated');
}
$sError = Dict::S('UI:Error:InvalidToken');
$aData['messages']['error'] += [
'_main' => [$sError],
];

View File

@@ -485,7 +485,7 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
'UI:Error:InvalidDashboard' => 'Chyba: neplatná nástěnka',
'UI:Error:MaintenanceMode' => 'Aktuálně probíhá údržba systému',
'UI:Error:MaintenanceTitle' => 'Údržba aplikace',
'UI:Error:InvalidToken' => 'Chyba: požadovaná operace byla již provedena (CSRF token nebyl nalezen)',
'UI:Error:InvalidToken' => 'Aktuální relace je neplatná. Obnovte stránku a zkuste to znovu. Pokud problém přetrvává, odhlaste se a znovu se přihlaste.',
'UI:Error:SMTP:UnknownVendor' => 'Poskytovatel OAuth SMTP %1$s neexistuje (email_transport_smtp.oauth.provider)',
'UI:GroupBy:Count' => 'Množství',
'UI:GroupBy:Count+' => 'Množství prvků',

View File

@@ -485,7 +485,7 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
'UI:Error:InvalidDashboard' => 'Error: invalid dashboard~~',
'UI:Error:MaintenanceMode' => 'Application is currently in maintenance~~',
'UI:Error:MaintenanceTitle' => 'Maintenance~~',
'UI:Error:InvalidToken' => 'Error: the requested operation has already been performed (CSRF token not found)~~',
'UI:Error:InvalidToken' => 'Den nuværende session er ugyldig. Opdater siden, og prøv igen. Hvis problemet fortsætter, skal du logge ud og logge ind igen.',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP provider %1$s does not exist (email_transport_smtp.oauth.provider)~~',
'UI:GroupBy:Count' => 'Antal',
'UI:GroupBy:Count+' => 'Antal af elementer',

View File

@@ -484,7 +484,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
'UI:Error:InvalidDashboard' => 'Fehler: Ungültiges Dashboard',
'UI:Error:MaintenanceMode' => 'Die Anwendung befindet sich derzeit im Wartungsmodus.',
'UI:Error:MaintenanceTitle' => 'Wartung',
'UI:Error:InvalidToken' => 'Fehler: The angeforderte Operation wurde bereits ausgeführt (CSRF-Token nicht gefunden)',
'UI:Error:InvalidToken' => 'Die aktuelle Sitzung ist ungültig. Bitte aktualisieren Sie die Seite und versuchen Sie es erneut. Wenn das Problem weiterhin besteht, melden Sie sich bitte ab und wieder an.',
'UI:Error:SMTP:UnknownVendor' => 'Der oAuth-SMTP-Provider %1$s existiert nicht (email_transport_smtp.oauth.provider)',
'UI:GroupBy:Count' => 'Anzahl',
'UI:GroupBy:Count+' => 'Anzahl der Elemente',

View File

@@ -503,7 +503,7 @@ Dict::Add('EN US', 'English', 'English', [
'UI:Error:InvalidDashboard' => 'Error: invalid dashboard',
'UI:Error:MaintenanceMode' => 'Application is currently in maintenance',
'UI:Error:MaintenanceTitle' => 'Maintenance',
'UI:Error:InvalidToken' => 'Error: the requested operation has already been performed (CSRF token not found)',
'UI:Error:InvalidToken' => 'The current session is invalid. Please refresh the page and try again. If the problem persists, please log out and log in again.',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP provider %1$s does not exist (email_transport_smtp.oauth.provider)',

View File

@@ -503,7 +503,7 @@ Dict::Add('EN GB', 'British English', 'British English', [
'UI:Error:InvalidDashboard' => 'Error: invalid dashboard',
'UI:Error:MaintenanceMode' => 'Application is currently in maintenance',
'UI:Error:MaintenanceTitle' => 'Maintenance',
'UI:Error:InvalidToken' => 'Error: the requested operation has already been performed (CSRF token not found)',
'UI:Error:InvalidToken' => 'The current session is invalid. Please refresh the page and try again. If the problem persists, please log out and log in again.',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP provider %1$s does not exist (email_transport_smtp.oauth.provider)',

View File

@@ -483,7 +483,7 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
'UI:Error:InvalidDashboard' => 'Error: Dashboard inválido',
'UI:Error:MaintenanceMode' => 'La aplicación se encuentra actualmente en mantenimiento',
'UI:Error:MaintenanceTitle' => 'Mantenimiento',
'UI:Error:InvalidToken' => 'Error: La operación solicitada ya se habia realizado (CSRF token not found)',
'UI:Error:InvalidToken' => 'La sesión actual no es válida. Actualice la página e inténtelo de nuevo. Si el problema persiste, cierre sesión y vuelva a iniciarla.',
'UI:Error:SMTP:UnknownVendor' => 'El proveedor SMTP de OAuth %1$s no existe (email_transport_smtp.oauth.provider)',
'UI:GroupBy:Count' => 'Cuenta',
'UI:GroupBy:Count+' => 'Número de Elementos',

View File

@@ -498,7 +498,7 @@ Nous espérons que vous aimerez cette version autant que nous avons eu du plaisi
'UI:Error:InvalidDashboard' => 'Erreur: Le tableau de bord est invalide',
'UI:Error:MaintenanceMode' => 'L\'application est en maintenance',
'UI:Error:MaintenanceTitle' => 'Maintenance',
'UI:Error:InvalidToken' => 'Erreur: l\'opération a déjà été effectuée (CSRF token not found)',
'UI:Error:InvalidToken' => 'La session actuelle est invalide. Veuillez actualiser la page et réessayer. Si le problème persiste, veuillez vous déconnecter puis vous reconnecter.',
'UI:Error:SMTP:UnknownVendor' => 'Le provider SMTP OAuth 2.0 %1$s n\'existe pas',
'UI:GroupBy:Count' => 'Nombre',
'UI:GroupBy:Count+' => 'Nombre d\'éléments',

View File

@@ -487,7 +487,7 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', [
'UI:Error:InvalidDashboard' => 'Hiba: Érvénytelen műszerfal',
'UI:Error:MaintenanceMode' => 'Az alkalmazás jelenleg karbantartás alatt van',
'UI:Error:MaintenanceTitle' => 'Karbantartás',
'UI:Error:InvalidToken' => 'Hiba: a kért művelet már végrehajtásra került (CSRF token nem található)',
'UI:Error:InvalidToken' => 'Az aktuális munkamenet érvénytelen. Frissítse az oldalt, majd próbálja újra. Ha a probléma továbbra is fennáll, jelentkezzen ki, majd jelentkezzen be újra.',
'UI:Error:SMTP:UnknownVendor' => 'A %1$s OAuth SMTP szolgáltató nem létezik (email_transport_smtp.oauth.provider)',
'UI:GroupBy:Count' => 'Mennyiség',
'UI:GroupBy:Count+' => '',

View File

@@ -487,7 +487,7 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
'UI:Error:InvalidDashboard' => 'Errore: cruscotto non valido',
'UI:Error:MaintenanceMode' => 'L\'applicazione è attualmente in manutenzione',
'UI:Error:MaintenanceTitle' => 'Manutenzione',
'UI:Error:InvalidToken' => 'Errore: l\'operazione richiesta è già stata eseguita (token CSRF non trovato)',
'UI:Error:InvalidToken' => 'La sessione corrente non è valida. Aggiorna la pagina e riprova. Se il problema persiste, esegui la disconnessione e accedi di nuovo.',
'UI:Error:SMTP:UnknownVendor' => 'Il fornitore OAuth SMTP %1$s non esiste (email_transport_smtp.oauth.provider)',
'UI:GroupBy:Count' => 'Conteggio',
'UI:GroupBy:Count+' => '',

View File

@@ -488,7 +488,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
'UI:Error:InvalidDashboard' => 'Error: invalid dashboard~~',
'UI:Error:MaintenanceMode' => 'Application is currently in maintenance~~',
'UI:Error:MaintenanceTitle' => 'Maintenance~~',
'UI:Error:InvalidToken' => 'Error: the requested operation has already been performed (CSRF token not found)~~',
'UI:Error:InvalidToken' => '現在のセッションは無効です。ページを更新してもう一度お試しください。問題が解決しない場合は、一度ログアウトしてから再度ログインしてください。',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP provider %1$s does not exist (email_transport_smtp.oauth.provider)~~',
'UI:GroupBy:Count' => 'カウント',
'UI:GroupBy:Count+' => '要素数',

View File

@@ -486,7 +486,7 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
'UI:Error:InvalidDashboard' => 'Fout: ongeldig dashboard',
'UI:Error:MaintenanceMode' => 'Toepassing is momenteel in onderhoud',
'UI:Error:MaintenanceTitle' => 'Onderhoud',
'UI:Error:InvalidToken' => 'Fout: de gevraagde bewerking werd al uitgevoerd (CSRF token niet gevonden)',
'UI:Error:InvalidToken' => 'De huidige sessie is ongeldig. Vernieuw de pagina en probeer het opnieuw. Als het probleem aanhoudt, log dan uit en weer in.',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP provider %1$s bestaat niet (email_transport_smtp.oauth.provider)',
'UI:GroupBy:Count' => 'Aantal',
'UI:GroupBy:Count+' => 'Aantal objecten',

View File

@@ -488,7 +488,7 @@ Dict::Add('PL PL', 'Polish', 'Polski', [
'UI:Error:InvalidDashboard' => 'Błąd: nieprawidłowy pulpit nawigacyjny',
'UI:Error:MaintenanceMode' => 'Aplikacja jest obecnie w trakcie konserwacji',
'UI:Error:MaintenanceTitle' => 'Konserwacja',
'UI:Error:InvalidToken' => 'Błąd: żądana operacja została już wykonana (nie znaleziono tokena CSRF)',
'UI:Error:InvalidToken' => 'Bieżąca sesja jest nieprawidłowa. Odśwież stronę i spróbuj ponownie. Jeśli problem będzie się powtarzał, wyloguj się i zaloguj ponownie.',
'UI:Error:SMTP:UnknownVendor' => 'Dostawca OAuth SMTP %1$s nie istnieje (email_transport_smtp.oauth.provider)',
'UI:GroupBy:Count' => 'Licznik',
'UI:GroupBy:Count+' => 'Liczba elementów',

View File

@@ -484,7 +484,7 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
'UI:Error:InvalidDashboard' => 'Erro: painel inválido',
'UI:Error:MaintenanceMode' => 'A aplicação está em manutenção',
'UI:Error:MaintenanceTitle' => 'Manutenção',
'UI:Error:InvalidToken' => 'Erro: A operação solicitada já foi executada (token CSRF não encontrado)',
'UI:Error:InvalidToken' => 'A sessão atual é inválida. Atualize a página e tente novamente. Se o problema persistir, saia e entre novamente.',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP provider %1$s does not exist (email_transport_smtp.oauth.provider)~~',
'UI:GroupBy:Count' => 'Número',
'UI:GroupBy:Count+' => 'Número de elementos',

View File

@@ -487,7 +487,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'UI:Error:InvalidDashboard' => 'Ошибка: недопустимый дашборд',
'UI:Error:MaintenanceMode' => 'Приложение в режиме технического обслуживания',
'UI:Error:MaintenanceTitle' => 'Техническое обслуживание',
'UI:Error:InvalidToken' => 'Error: the requested operation has already been performed (CSRF token not found)~~',
'UI:Error:InvalidToken' => 'Текущий сеанс недействителен. Обновите страницу и повторите попытку. Если проблема сохраняется, выйдите из системы и войдите снова.',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP provider %1$s does not exist (email_transport_smtp.oauth.provider)~~',
'UI:GroupBy:Count' => 'Количество',
'UI:GroupBy:Count+' => 'Количество элементов',

View File

@@ -491,7 +491,7 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
'UI:Error:InvalidDashboard' => 'Error: invalid dashboard~~',
'UI:Error:MaintenanceMode' => 'Application is currently in maintenance~~',
'UI:Error:MaintenanceTitle' => 'Maintenance~~',
'UI:Error:InvalidToken' => 'Error: the requested operation has already been performed (CSRF token not found)~~',
'UI:Error:InvalidToken' => 'Aktuálna relácia je neplatná. Obnovte stránku a skúste to znova. Ak problém pretrváva, odhláste sa a znova sa prihláste.',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP provider %1$s does not exist (email_transport_smtp.oauth.provider)~~',
'UI:GroupBy:Count' => 'Počet',
'UI:GroupBy:Count+' => 'Number of elements~~',

View File

@@ -488,7 +488,7 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
'UI:Error:InvalidDashboard' => 'Error: invalid dashboard~~',
'UI:Error:MaintenanceMode' => 'Application is currently in maintenance~~',
'UI:Error:MaintenanceTitle' => 'Maintenance~~',
'UI:Error:InvalidToken' => 'Error: the requested operation has already been performed (CSRF token not found)~~',
'UI:Error:InvalidToken' => 'Mevcut oturum geçersiz. Lütfen sayfayı yenileyip tekrar deneyin. Sorun devam ederse, oturumu kapatıp yeniden açın.',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP provider %1$s does not exist (email_transport_smtp.oauth.provider)~~',
'UI:GroupBy:Count' => 'Say',
'UI:GroupBy:Count+' => 'Eleman sayısı',

View File

@@ -503,7 +503,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'UI:Error:InvalidDashboard' => '错误: 无效的仪表盘',
'UI:Error:MaintenanceMode' => '应用正处于维护中',
'UI:Error:MaintenanceTitle' => '维护',
'UI:Error:InvalidToken' => '错误: 所请求的操作已执行 (没有CSRF token)',
'UI:Error:InvalidToken' => '当前会话无效。请刷新页面后重试。如果问题仍然存在,请先退出登录再重新登录。',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP 提供者 %1$s 不存在 (email_transport_smtp.oauth.provider)',

View File

@@ -905,7 +905,7 @@ try {
if (!utils::IsTransactionValid($sTransactionId)) {
$sUser = UserRights::GetUser();
IssueLog::Error("UI.php '$operation' : invalid transaction_id ! data: user='$sUser'");
$oP->p(Dict::S('UI:Error:ObjectAlreadyUpdated'));
$oP->p(Dict::S('UI:Error:InvalidToken'));
} else {
// For archiving the modification
$oFilter = DBObjectSearch::unserialize($sFilter);
@@ -1086,7 +1086,7 @@ try {
if (!utils::IsTransactionValid($sTransactionId)) {
$sUser = UserRights::GetUser();
IssueLog::Error("UI.php '$operation' : invalid transaction_id ! data: user='$sUser', class='$sClass'");
$sMessage = Dict::S('UI:Error:ObjectAlreadyUpdated');
$sMessage = Dict::S('UI:Error:InvalidToken');
$sSeverity = 'info';
} elseif ((get_class($aStimuli[$sStimulus]) !== 'StimulusUserAction') || (UserRights::IsStimulusAllowed($sClass, $sStimulus) === UR_ALLOWED_NO)) {
$sUser = UserRights::GetUser();

View File

@@ -7,7 +7,6 @@
namespace Combodo\iTop\Controller\Base\Layout;
use Combodo\iTop\Application\WebPage\AjaxPage;
use ApplicationContext;
use ApplicationException;
use cmdbAbstractObject;
@@ -18,9 +17,13 @@ use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\QuickCreate\QuickCreateHelper;
use Combodo\iTop\Application\UI\Base\Layout\Object\ObjectSummary;
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
use Combodo\iTop\Application\WebPage\AjaxPage;
use Combodo\iTop\Application\WebPage\iTopWebPage;
use Combodo\iTop\Application\WebPage\JsonPage;
use Combodo\iTop\Controller\AbstractController;
use Combodo\iTop\Service\Base\ObjectRepository;
use Combodo\iTop\Service\Router\Router;
use Combodo\iTop\Service\SummaryCard\SummaryCardService;
use CoreCannotSaveObjectException;
use DBObjectSearch;
use DBObjectSet;
@@ -31,11 +34,8 @@ use Dict;
use Exception;
use IssueLog;
use iTopOwnershipLock;
use Combodo\iTop\Application\WebPage\iTopWebPage;
use Combodo\iTop\Application\WebPage\JsonPage;
use MetaModel;
use SecurityException;
use Combodo\iTop\Service\SummaryCard\SummaryCardService;
use UserRights;
use utils;
@@ -361,9 +361,9 @@ JS;
IssueLog::Error(__CLASS__.'::'.__METHOD__." : invalid transaction_id ! data: user='$sUser', class='$sClass'");
if ($this->IsHandlingXmlHttpRequest()) {
$aResult['data'] = ['error_message' => Dict::S('UI:Error:ObjectAlreadyCreated')];
$aResult['data'] = ['error_message' => Dict::S('UI:Error:InvalidToken')];
} else {
$oErrorAlert = AlertUIBlockFactory::MakeForFailure(Dict::S('UI:Error:ObjectAlreadyCreated'));
$oErrorAlert = AlertUIBlockFactory::MakeForFailure(Dict::S('UI:Error:InvalidToken'));
$oErrorAlert->SetIsClosable(false)
->SetIsCollapsible(false);
$oPage->AddUiBlock($oErrorAlert);
@@ -552,13 +552,13 @@ JS;
IssueLog::Error(__CLASS__.'::'.__METHOD__." : invalid transaction_id ! data: user='$sUser', class='$sClass'");
if ($this->IsHandlingXmlHttpRequest()) {
$aResult['data'] = ['error_message' => Dict::S('UI:Error:ObjectAlreadyUpdated')];
$aResult['data'] = ['error_message' => Dict::S('UI:Error:InvalidToken')];
} else {
$oPage->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding
$oPage->p("<strong>".Dict::S('UI:Error:ObjectAlreadyUpdated')."</strong>\n");
$oPage->p("<strong>".Dict::S('UI:Error:InvalidToken')."</strong>\n");
}
$sMessage = Dict::Format('UI:Error:ObjectAlreadyUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName());
$sMessage = Dict::Format('UI:Error:InvalidToken');
$sSeverity = 'error';
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not updated (invalid transaction_id)', $sClass, [

View File

@@ -7,19 +7,18 @@
namespace Combodo\iTop\Controller\Links;
use Combodo\iTop\Application\WebPage\AjaxPage;
use cmdbAbstractObject;
use Combodo\iTop\Application\Helper\FormHelper;
use Combodo\iTop\Application\UI\Base\Component\Form\FormUIBlockFactory;
use Combodo\iTop\Controller\AbstractController;
use Combodo\iTop\Service\Links\LinkSetModel;
use Combodo\iTop\Service\Router\Router;
use Combodo\iTop\Service\Base\ObjectRepository;
use Dict;
use Exception;
use Combodo\iTop\Application\WebPage\AjaxPage;
use Combodo\iTop\Application\WebPage\JsonPage;
use Combodo\iTop\Controller\AbstractController;
use Combodo\iTop\Service\Base\ObjectRepository;
use Combodo\iTop\Service\Router\Router;
use CoreException;
use DBObject;
use Dict;
use Exception;
use MetaModel;
use UserRights;
use utils;
@@ -68,7 +67,7 @@ class LinkSetController extends AbstractController
$sErrorMessage = $e->getMessage();
}
} else {
$sErrorMessage = 'invalid transaction id';
$sErrorMessage = Dict::S('UI:Error:InvalidToken');
}
$oPage->SetData([
'success' => $bOperationSuccess,
@@ -111,7 +110,7 @@ class LinkSetController extends AbstractController
$sErrorMessage = $e->getMessage();
}
} else {
$sErrorMessage = 'invalid transaction id';
$sErrorMessage = Dict::S('UI:Error:InvalidToken');
}
$oPage->SetData([
'success' => $bOperationSuccess,

View File

@@ -42,9 +42,10 @@ class privUITransactionFileTest extends ItopDataTestCase
*/
public function testCleanupOldTransactions($iCleanableCreated, $iPreservableCreated, $sCleanablePrefix, $sPreservablePrefix)
{
MetaModel::GetConfig()->Set('transactions_gc_threshold', 100);
$iBaseLimit = time() - 24 * 3600; //24h
$oConfig = MetaModel::GetConfig();
$oConfig->Set('transactions_gc_threshold', 100);
$iOriginalLifetime = (int) $oConfig->Get('transactions_file_lifetime');
$iBaseLimit = time() - $iOriginalLifetime;
$sBaseDir = sys_get_temp_dir();
$sDir = "$sBaseDir/privUITransactionFileTest/cleanupOldTransactions";
@@ -185,4 +186,71 @@ class privUITransactionFileTest extends ItopDataTestCase
$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");
}
}