diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php
index 641820262b..9836c31027 100644
--- a/application/cmdbabstract.class.inc.php
+++ b/application/cmdbabstract.class.inc.php
@@ -4651,7 +4651,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);
}
diff --git a/application/transaction.class.inc.php b/application/transaction.class.inc.php
index 6a6c3837f2..e4d5de3443 100644
--- a/application/transaction.class.inc.php
+++ b/application/transaction.class.inc.php
@@ -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(utils::GetDataPath().'transactions', static::GetUserPrefix());
+ $sTransactionIdFullPath = static::CreateUniqueTransactionFilePath(utils::GetDataPath().'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/*" : utils::GetDataPath().'transactions/*';
$aTransactions = glob($sPattern);
foreach ($aTransactions as $sFileName) {
diff --git a/core/config.class.inc.php b/core/config.class.inc.php
index fae11cc153..d95967d965 100644
--- a/core/config.class.inc.php
+++ b/core/config.class.inc.php
@@ -1288,6 +1288,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). This only works if the "transaction_storage" parameter is set to "File".',
+ '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.',
diff --git a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
index 8d0bfb46d5..d1c7423b63 100644
--- a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
+++ b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
@@ -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],
];
diff --git a/dictionaries/cs.dictionary.itop.ui.php b/dictionaries/cs.dictionary.itop.ui.php
index 8819ba11e1..385a1cbcf3 100755
--- a/dictionaries/cs.dictionary.itop.ui.php
+++ b/dictionaries/cs.dictionary.itop.ui.php
@@ -503,7 +503,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ů',
diff --git a/dictionaries/da.dictionary.itop.ui.php b/dictionaries/da.dictionary.itop.ui.php
index e05152ba86..18c231d852 100644
--- a/dictionaries/da.dictionary.itop.ui.php
+++ b/dictionaries/da.dictionary.itop.ui.php
@@ -503,7 +503,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',
diff --git a/dictionaries/de.dictionary.itop.ui.php b/dictionaries/de.dictionary.itop.ui.php
index f3b3ef9093..1f45117efe 100644
--- a/dictionaries/de.dictionary.itop.ui.php
+++ b/dictionaries/de.dictionary.itop.ui.php
@@ -506,7 +506,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',
diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php
index 844a69c68b..ae258f0096 100644
--- a/dictionaries/en.dictionary.itop.ui.php
+++ b/dictionaries/en.dictionary.itop.ui.php
@@ -521,7 +521,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:TwigController' => 'Internal error in form controller',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP provider %1$s does not exist (email_transport_smtp.oauth.provider)',
diff --git a/dictionaries/en_gb.dictionary.itop.ui.php b/dictionaries/en_gb.dictionary.itop.ui.php
index 4f0f45cb52..526c679b30 100644
--- a/dictionaries/en_gb.dictionary.itop.ui.php
+++ b/dictionaries/en_gb.dictionary.itop.ui.php
@@ -521,7 +521,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)',
diff --git a/dictionaries/es_cr.dictionary.itop.ui.php b/dictionaries/es_cr.dictionary.itop.ui.php
index 414b3f9f4d..387c7d9b18 100644
--- a/dictionaries/es_cr.dictionary.itop.ui.php
+++ b/dictionaries/es_cr.dictionary.itop.ui.php
@@ -501,7 +501,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',
diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php
index f7e536aa47..b29c9dccd6 100644
--- a/dictionaries/fr.dictionary.itop.ui.php
+++ b/dictionaries/fr.dictionary.itop.ui.php
@@ -517,7 +517,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:TwigController' => 'Erreur interne dans le contrôleur de formulaire',
'UI:Error:SMTP:UnknownVendor' => 'Le provider SMTP OAuth 2.0 %1$s n\'existe pas',
'UI:GroupBy:Count' => 'Nombre',
diff --git a/dictionaries/hu.dictionary.itop.ui.php b/dictionaries/hu.dictionary.itop.ui.php
index 271cd566b4..646ec4af6d 100755
--- a/dictionaries/hu.dictionary.itop.ui.php
+++ b/dictionaries/hu.dictionary.itop.ui.php
@@ -505,7 +505,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+' => '',
diff --git a/dictionaries/it.dictionary.itop.ui.php b/dictionaries/it.dictionary.itop.ui.php
index e7f5df25bc..7259db0963 100644
--- a/dictionaries/it.dictionary.itop.ui.php
+++ b/dictionaries/it.dictionary.itop.ui.php
@@ -507,7 +507,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+' => '',
diff --git a/dictionaries/ja.dictionary.itop.ui.php b/dictionaries/ja.dictionary.itop.ui.php
index 99dba42a09..297875bc95 100644
--- a/dictionaries/ja.dictionary.itop.ui.php
+++ b/dictionaries/ja.dictionary.itop.ui.php
@@ -506,7 +506,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+' => '要素数',
diff --git a/dictionaries/nl.dictionary.itop.ui.php b/dictionaries/nl.dictionary.itop.ui.php
index 851deb8010..3c130438e4 100644
--- a/dictionaries/nl.dictionary.itop.ui.php
+++ b/dictionaries/nl.dictionary.itop.ui.php
@@ -504,7 +504,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',
diff --git a/dictionaries/pl.dictionary.itop.ui.php b/dictionaries/pl.dictionary.itop.ui.php
index 6741b18a14..1e6bb791af 100644
--- a/dictionaries/pl.dictionary.itop.ui.php
+++ b/dictionaries/pl.dictionary.itop.ui.php
@@ -506,7 +506,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',
diff --git a/dictionaries/pt_br.dictionary.itop.ui.php b/dictionaries/pt_br.dictionary.itop.ui.php
index 5c0c86060b..9c7de6d28b 100644
--- a/dictionaries/pt_br.dictionary.itop.ui.php
+++ b/dictionaries/pt_br.dictionary.itop.ui.php
@@ -503,7 +503,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' => 'O provedor de autenticação SMTP OAuth %1$s não existe (email_transport_smtp.oauth.provider)',
'UI:GroupBy:Count' => 'Número',
'UI:GroupBy:Count+' => 'Número de elementos',
diff --git a/dictionaries/ru.dictionary.itop.ui.php b/dictionaries/ru.dictionary.itop.ui.php
index c25a9fe786..72d92919db 100644
--- a/dictionaries/ru.dictionary.itop.ui.php
+++ b/dictionaries/ru.dictionary.itop.ui.php
@@ -505,7 +505,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'UI:Error:InvalidDashboard' => 'Ошибка: недопустимый дашборд',
'UI:Error:MaintenanceMode' => 'Приложение в режиме технического обслуживания',
'UI:Error:MaintenanceTitle' => 'Техническое обслуживание',
- 'UI:Error:InvalidToken' => 'Ошибка: запрошенная операция уже была выполнена (CSRF-токен не найден)',
+ 'UI:Error:InvalidToken' => 'Текущий сеанс недействителен. Обновите страницу и повторите попытку. Если проблема сохраняется, выйдите из системы и войдите снова.',
'UI:Error:SMTP:UnknownVendor' => 'Провайдер OAuth SMTP %1$s не существует (email_transport_smtp.oauth.provider)',
'UI:GroupBy:Count' => 'Количество',
'UI:GroupBy:Count+' => 'Количество элементов',
diff --git a/dictionaries/sk.dictionary.itop.ui.php b/dictionaries/sk.dictionary.itop.ui.php
index 3c06c69b99..45203fc63e 100644
--- a/dictionaries/sk.dictionary.itop.ui.php
+++ b/dictionaries/sk.dictionary.itop.ui.php
@@ -509,7 +509,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~~',
diff --git a/dictionaries/tr.dictionary.itop.ui.php b/dictionaries/tr.dictionary.itop.ui.php
index cf45dfb7a4..d3ecd73a51 100644
--- a/dictionaries/tr.dictionary.itop.ui.php
+++ b/dictionaries/tr.dictionary.itop.ui.php
@@ -506,7 +506,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ı',
diff --git a/dictionaries/zh_cn.dictionary.itop.ui.php b/dictionaries/zh_cn.dictionary.itop.ui.php
index f5bebf14bf..d3325bbfe3 100644
--- a/dictionaries/zh_cn.dictionary.itop.ui.php
+++ b/dictionaries/zh_cn.dictionary.itop.ui.php
@@ -521,7 +521,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:TwigController' => '表单控制器内部错误',
'UI:Error:SMTP:UnknownVendor' => 'OAuth SMTP提供者%1$s不存在 (email_transport_smtp.oauth.provider)',
diff --git a/pages/UI.php b/pages/UI.php
index 3b6d5f71b3..a579356bd4 100644
--- a/pages/UI.php
+++ b/pages/UI.php
@@ -924,7 +924,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);
@@ -1105,7 +1105,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();
diff --git a/sources/Controller/Base/Layout/ObjectController.php b/sources/Controller/Base/Layout/ObjectController.php
index 7062c62dbf..1cfc573e43 100644
--- a/sources/Controller/Base/Layout/ObjectController.php
+++ b/sources/Controller/Base/Layout/ObjectController.php
@@ -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("".Dict::S('UI:Error:ObjectAlreadyUpdated')."\n");
+ $oPage->p("".Dict::S('UI:Error:InvalidToken')."\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, [
diff --git a/sources/Controller/Links/LinkSetController.php b/sources/Controller/Links/LinkSetController.php
index 9a5b278115..e5e9886a5d 100644
--- a/sources/Controller/Links/LinkSetController.php
+++ b/sources/Controller/Links/LinkSetController.php
@@ -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,
diff --git a/tests/php-unit-tests/unitary-tests/application/privUITransactionFileTest.php b/tests/php-unit-tests/unitary-tests/application/privUITransactionFileTest.php
index e46acde8cc..8fc30f81c0 100644
--- a/tests/php-unit-tests/unitary-tests/application/privUITransactionFileTest.php
+++ b/tests/php-unit-tests/unitary-tests/application/privUITransactionFileTest.php
@@ -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");
+ }
+
}