Compare commits

..

15 Commits

Author SHA1 Message Date
Vaibhav Srivastava
0a5b0b403e docs: fix typo 2026-08-26 15:22:39 +02:00
odain-cbd
a346b71e9a N°9979 - Hide "keep current choice" setup button in case of a package upgrade (#1026)
* N°9979 - phstan cleanup

* N°9979 - Hide keep current choice setup button in case of a package upgrade

* N°9979 - optimize SQL queries
2026-08-26 14:09:05 +02:00
Timmy38
8065343ea6 N°9974 Show extension code in metadata for extensions (#1025) 2026-08-26 10:37:15 +02:00
Eric Espie
9c6087fd54 N°9982 - Install history is broken for MTP 2026-08-25 17:53:29 +02:00
Eric Espie
66073ba390 N°9982 - Install history is broken for MTP 2026-08-25 17:50:58 +02:00
jf-cbd
4756e2474c Merge remote-tracking branch 'origin/support/3.2' into develop 2026-08-25 17:05:57 +02:00
jf-cbd
7dbbf742c4 Fix code syntax (that was only 8.4+) 2026-08-25 17:05:30 +02:00
jf-cbd
2cd4fd1e0e Merge remote-tracking branch 'origin/support/3.2' into develop 2026-08-25 16:09:31 +02:00
jf-cbd
fffff76c82 N°9975 - PHPmailer: Sending email with same headers as before (#1023) 2026-08-25 16:08:51 +02:00
jf-cbd
06d593631d Merge remote-tracking branch 'origin/support/3.2' into develop 2026-08-25 09:13:59 +02:00
Benjamin DALSASS
54a0dfd06f Merge branch 'support/3.2' into develop
# Conflicts:
#	application/transaction.class.inc.php
#	dictionaries/en.dictionary.itop.ui.php
#	dictionaries/fr.dictionary.itop.ui.php
#	dictionaries/pt_br.dictionary.itop.ui.php
#	dictionaries/ru.dictionary.itop.ui.php
#	dictionaries/zh_cn.dictionary.itop.ui.php
2026-08-25 07:16:55 +02:00
odain
e31492fe27 N°9949 - code style 2026-08-24 19:49:01 +02:00
odain-cbd
dfd7f4cd0a N°9949 - Cannot uninstall IPAM from setup wizard screen - extension management fix (#1024)
* N°9949 - Cannot uninstall IPAM from setup wizard screen - fix extension mgt as well

* N°9949 - remove dupicated line

* N°9949 - fix log level

* N°9949 - log enhancement in ext mgt

* N°9949 - phpstan code cleanup
2026-08-24 19:35:06 +02:00
jf-cbd
69624116db N°9919 - Check if "To" exists when sendind mail with PHPMail (#1021) 2026-08-24 17:42:05 +02:00
Benjamin Dalsass
97364af71d Feature/8420 transaction id lifetime (#1009) 2026-08-19 08:45:49 +02:00
37 changed files with 377 additions and 77 deletions

View File

@@ -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);
}

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(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) {

View File

@@ -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.',

View File

@@ -42,7 +42,7 @@ css/backoffice/
| vendors/ # Third-party libs, should be either:
| # - Overload of the lib SCSS variables (BEST way, but possible only if the lib exposes them. e.g. Bulma)
| # - Overload of the lib necessary CSS classes only (not great as it duplicates some rules in the browser, which add weight and computation. e.g. dataTables)
| # - Duplicate the lib CSS completly to insert SCSS variables (not great as it will be outdated when updating the lib itself. e.g. jQuery UI)
| # - Duplicate the lib CSS completely to insert SCSS variables (not great as it will be outdated when updating the lib itself. e.g. jQuery UI)
| | _bulma-variables-overload.scss # Bulma CSS framework
| | _jquery-ui.scss # jQuery UI
| ... # Etc…

View File

@@ -12,7 +12,7 @@ class DeletionPlanEntity
public readonly DeletionPlanItem $oDelete;
public readonly DeletionPlanItem $oUpdate;
public readonly DeletionPlanItem $oIssue;
public function __construct()
{
$this->oDelete = new DeletionPlanItem();

View File

@@ -32,9 +32,9 @@
{% UIColumn Standard {} %}
{% for aExtension in aAvailableExtensions[iColumnIndex] %}
{% if aExtension['installed'] %}
{% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source'], aExtension['code']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% else %}
{% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source'], aExtension['code']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% endif %}
{% endfor %}
{% EndUIColumn %}

View File

@@ -12,9 +12,9 @@
{% UIColumn Standard {} %}
{% for aExtension in aAvailableExtensions[iColumnIndex] %}
{% if aExtension['installed'] %}
{% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source'], aExtension['code']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% else %}
{% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source'], aExtension['code']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% endif %}
{% endfor %}
{% EndUIColumn %}

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

@@ -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ů',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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)',

View File

@@ -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)',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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+' => '',

View File

@@ -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+' => '',

View File

@@ -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+' => '要素数',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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+' => 'Количество элементов',

View File

@@ -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~~',

View File

@@ -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ı',

View File

@@ -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)',

View File

@@ -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();

View File

@@ -2,7 +2,7 @@
class ModuleInstallationRepository
{
private static ModuleInstallationRepository $oInstance;
private static ?ModuleInstallationRepository $oInstance;
protected function __construct()
{
@@ -60,6 +60,7 @@ class ModuleInstallationRepository
CMDBSource::InitFromConfig($oConfig);
//read db module installations
$tableWithPrefix = $this->GetTableWithPrefix($oConfig);
$iRootId = CMDBSource::QueryToScalar("SELECT max(parent_id) FROM $tableWithPrefix");
// Get the latest installed modules, without the "root" ones (iTop version and datamodel version)
$sSQL = <<<SQL
@@ -94,10 +95,7 @@ SQL;
public function GetApplicationVersion(Config $oConfig)
{
try {
CMDBSource::InitFromConfig($oConfig);
$tableWithPrefix = $this->GetTableWithPrefix($oConfig);
$sSQLQuery = "SELECT * FROM $tableWithPrefix";
$aSelectInstall = CMDBSource::QueryToArray($sSQLQuery);
$aSelectInstall = $this->ReadFromDB($oConfig);
} catch (MySQLException $e) {
// No database or erroneous information
SetupLog::Error(
@@ -105,8 +103,6 @@ SQL;
null,
[
'host' => $oConfig->Get('db_host'),
'user' => $oConfig->Get('db_user'),
'pwd:' => $oConfig->Get('db_pwd'),
'db name' => $oConfig->Get('db_name'),
'msg' => $e->getMessage(),
]

View File

@@ -645,6 +645,7 @@ class RunTimeEnvironment
$iPrevAccessMode = $oConfig->Get('access_mode');
$oConfig->Set('access_mode', ACCESS_FULL);
$this->InitDataModel($oConfig, true); // load data model and connect to the database
$oParams = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
if (CMDBSource::DBName() == '') {
// In case this has not yet been done
@@ -652,8 +653,9 @@ class RunTimeEnvironment
}
if ($sShortComment === null) {
$sShortComment = 'Done by the setup program';
$sShortComment = $oParams->GetParameter('install_comment', 'Done by the setup program');
}
$oParams->SetParameter('install_comment', null);
$sMainComment = $sShortComment."\nBuilt on ".ITOP_BUILD_DATE;
// Record datamodel version
@@ -748,7 +750,6 @@ class RunTimeEnvironment
}
}
$oParams = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
if (class_exists('DesignerUpdate') && $oParams->GetParameter('return_application') === 'designer') {
// Now keep track of this update
$oLog = new DesignerUpdate();
@@ -1275,14 +1276,14 @@ class RunTimeEnvironment
* - plus the list of modules present in the "extra" directory of the build environment: data/<build_environment>-modules/
*
* @param string $sSourceEnv The name of the source environment to 'imitate'
* @param null $bUseSymLinks Whether to create symbolic links instead of copies
* @param bool|null $bUseSymLinks Whether to create symbolic links instead of copies
* @param array $aAddedExtensions List of additional extensions to add to the build environment
*
* @return string[]
* @throws \ConfigException
* @throws \CoreException
*/
public function CompileFrom($sSourceEnv, $bUseSymLinks = null, $aAddedExtensions = [])
public function CompileFrom($sSourceEnv, ?bool $bUseSymLinks = null, $aAddedExtensions = [])
{
$oConfig = new Config(utils::GetConfigFilePath($sSourceEnv));
$this->InitExtensionMap($oConfig);

View File

@@ -944,7 +944,7 @@ EOF
$sMetadata = '';
if (isset($aChoice['version']) && isset($aChoice['source_label'])) {
$sMetadata = '<span>v'.$aChoice['version'].'</span><span>'.$aChoice['source_label'].'</span>';
$sMetadata = '<span>v'.$aChoice['version'].'</span><span>'.$aChoice['source_label'].'</span><span>'.$aChoice['extension_code'].'</span>';
}
$sChoiceDisabled = $aFlags['disabled'] && !$aFlags['checked'] ? 'choice-disabled' : '';

View File

@@ -158,15 +158,45 @@ HTML
</form>
HTML
);
$oPage->add_ready_script(
<<<JS
if ($this->DisplaySetupShortcutButton()) {
$oPage->add_ready_script(
<<<JS
$('.ibo-setup--wizard--buttons-container tr td:nth-child(1)').before('<td style="text-align:center;"><button class="ibo-button ibo-is-alternative ibo-is-neutral" form="fast_setup"><span class="ibo-button--label">Keep current choices</span></button></td>');
JS
);
);
}
}
}
}
public function DisplaySetupShortcutButton(): bool
{
if ('install' === $this->oWizard->GetParameter('mode', 'install')) {
//fresh install
return false;
}
$oConfig = utils::GetConfig();
$res = ModuleInstallationRepository::GetInstance()->GetApplicationVersion($oConfig);
if (false === $res) {
return false;
}
$sProductName = $res['product_name'] ?? null;
$sProductVersion = $res['product_version'] ?? null;
if (is_null($sProductName) || is_null($sProductVersion)) {
\SetupLog::Error(__METHOD__.": cannot fetch itop version", null, $res);
return false;
}
if (ITOP_VERSION_FULL !== $sProductVersion) {
return false;
}
return (ITOP_APPLICATION === $sProductName);
}
public function CanMoveForward()
{
return $this->bCanMoveForward;

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

@@ -20,7 +20,7 @@ class SymfonyPHPMailTransport extends AbstractTransport
{
$oHeaders = $oRawEmail->getHeaders();
return $oHeaders->get('To')->getBodyAsString();
return $oHeaders->get('To') ? $oHeaders->get('To')->getBodyAsString() : '';
}
/**
@@ -70,6 +70,13 @@ class SymfonyPHPMailTransport extends AbstractTransport
return $sHeaders;
}
public function prepareAdditionalParameters(SentMessage $message): string
{
$sender = $message->getEnvelope()->getSender()->getEncodedAddress();
return '-f'.escapeshellarg($sender);
}
protected function doSend(SentMessage $message): void
{
$oRawEmail = $message->getOriginalMessage();
@@ -82,8 +89,9 @@ class SymfonyPHPMailTransport extends AbstractTransport
$sSubject = $this->prepareSubject($oRawEmail);
$sBody = $this->prepareBody($oRawEmail);
$sHeaders = $this->prepareHeaders($oRawEmail);
$sAdditionalParameters = $this->prepareAdditionalParameters($message);
$success = mail($sTo, $sSubject, $sBody, $sHeaders);
$success = mail($sTo, $sSubject, $sBody, $sHeaders, $sAdditionalParameters);
if (!$success) {
throw new \RuntimeException('The mail() function failed to send the message. Check server mail configuration.');

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");
}
}

View File

@@ -1295,7 +1295,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
<div id="badge--itop-ext-not-installed--to-be-installed" class="ibo-badge ibo-block checked ibo-is-cyan" title="This extension will be installed during the setup." >to be installed</div><div id="badge--itop-ext-not-installed--not-installed" class="ibo-badge ibo-block unchecked ibo-is-blue-grey" title="This extension is not part of the current installation." >not installed</div>
</div>
<div class="ibo-extension-details--information--metadata">
<span>v1.2.3</span><span>Local extensions folder</span>
<span>v1.2.3</span><span>Local extensions folder</span><span>itop-ext-not-installed</span>
</div>
<div class="ibo-extension-details--information--description">
Do something
@@ -1338,7 +1338,7 @@ HTML,
<div id="badge--itop-ext-installed--installed" class="ibo-badge ibo-block checked ibo-is-green" title="This extension is part of the current installation." >installed</div><div id="badge--itop-ext-installed--to-be-uninstalled" class="ibo-badge ibo-block unchecked ibo-is-red" title="This extension will be uninstalled during the setup." >to be uninstalled</div>
</div>
<div class="ibo-extension-details--information--metadata">
<span>v1.2.3</span><span>Local extensions folder</span>
<span>v1.2.3</span><span>Local extensions folder</span><span>itop-ext-installed</span>
</div>
<div class="ibo-extension-details--information--description">
Do something
@@ -1382,7 +1382,7 @@ HTML,
<div id="badge--itop-ext-installed--installed" class="ibo-badge ibo-block checked ibo-is-green" title="This extension is part of the current installation." >installed</div><div id="badge--itop-ext-installed--to-be-uninstalled" class="ibo-badge ibo-block unchecked ibo-is-red" title="This extension will be uninstalled during the setup." >to be uninstalled</div><div id="badge--itop-ext-installed--not-uninstallable" class="ibo-badge ibo-block ibo-is-yellow" title="Once this extension has been installed, it should not be uninstalled." >cannot be uninstalled</div>
</div>
<div class="ibo-extension-details--information--metadata">
<span>v1.2.3</span><span>Local extensions folder</span>
<span>v1.2.3</span><span>Local extensions folder</span><span>itop-ext-installed</span>
</div>
<div class="ibo-extension-details--information--description">
Do something

View File

@@ -0,0 +1,134 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use WizardController;
use ModuleInstallationRepository;
class WizStepWelcomeTest extends ItopDataTestCase
{
protected function setUp(): void
{
parent::setUp(); // TODO: Change the autogenerated stub
require_once(APPROOT.'/setup/wizardsteps_autoload.php');
require_once(APPROOT.'/setup/moduleinstallation/ModuleInstallationRepository.php');
}
protected function tearDown(): void
{
parent::tearDown(); // TODO: Change the autogenerated stub
ModuleInstallationRepository::SetInstance(null);
}
public function testDisplaySetupShortcutButton_NoButtonWhenFreshInstall()
{
$oWizard = $this->createMock(WizardController::class);
$this->GivenSetupMode($oWizard);
$oWiz = new \WizStepWelcome($oWizard, "");
$this->assertEquals(false, $oWiz->DisplaySetupShortcutButton());
}
public function testDisplaySetupShortcutButton_NoButtonWhenNoItopVersionFetched()
{
$oWizard = $this->createMock(WizardController::class);
$this->GivenSetupMode($oWizard, 'upgrade');
$aApplicationVersion = [
'product_name' => ITOP_APPLICATION,
];
$this->GivenGetApplicationVersion($aApplicationVersion);
$oWiz = new \WizStepWelcome($oWizard, "");
$this->assertEquals(false, $oWiz->DisplaySetupShortcutButton());
}
public function testDisplaySetupShortcutButton_NoButtonWhenNoApplicationVersionFetched()
{
$oWizard = $this->createMock(WizardController::class);
$this->GivenSetupMode($oWizard, 'upgrade');
$aApplicationVersion = [
'product_version' => ITOP_VERSION_FULL,
];
$this->GivenGetApplicationVersion($aApplicationVersion);
$oWiz = new \WizStepWelcome($oWizard, "");
$this->assertEquals(false, $oWiz->DisplaySetupShortcutButton());
}
public function testDisplaySetupShortcutButton_NoButtonWhenApplicationChangeDuringUpgrade()
{
$oWizard = $this->createMock(WizardController::class);
$this->GivenSetupMode($oWizard, 'upgrade');
$aApplicationVersion = [
'product_version' => ITOP_VERSION_FULL,
'product_name' => 'toto',
];
$this->GivenGetApplicationVersion($aApplicationVersion);
$oWiz = new \WizStepWelcome($oWizard, "");
$this->assertEquals(false, $oWiz->DisplaySetupShortcutButton());
}
public function testDisplaySetupShortcutButton_NoButtonWhenVersionChangeDuringUpgrade()
{
$oWizard = $this->createMock(WizardController::class);
$this->GivenSetupMode($oWizard, 'upgrade');
$aApplicationVersion = [
'product_version' => '6.6.6',
'product_name' => ITOP_APPLICATION,
];
$this->GivenGetApplicationVersion($aApplicationVersion);
$oWiz = new \WizStepWelcome($oWizard, "");
$this->assertEquals(false, $oWiz->DisplaySetupShortcutButton());
}
public function testDisplaySetupShortcutButton_NoButtonWhenMySQLException()
{
$oWizard = $this->createMock(WizardController::class);
$this->GivenSetupMode($oWizard, 'upgrade');
$this->GivenGetApplicationVersion(false);
$oWiz = new \WizStepWelcome($oWizard, "");
$this->assertEquals(false, $oWiz->DisplaySetupShortcutButton());
}
public function testDisplaySetupShortcutButton_ButtonDisplayed()
{
$oWizard = $this->createMock(WizardController::class);
$this->GivenSetupMode($oWizard, 'upgrade');
$aApplicationVersion = [
'product_version' => ITOP_VERSION_FULL,
'product_name' => ITOP_APPLICATION,
];
$this->GivenGetApplicationVersion($aApplicationVersion);
$oWiz = new \WizStepWelcome($oWizard, "");
$this->assertEquals(true, $oWiz->DisplaySetupShortcutButton());
}
private function GivenSetupMode(WizardController $oWizardMock, $sMode = 'install'): void
{
$oWizardMock->expects($this->once())
->method('GetParameter')
->with('mode')
->willReturn($sMode);
}
private function GivenGetApplicationVersion($sExpectedReturnedValue): void
{
$oModuleInstallationRepository = $this->createMock(ModuleInstallationRepository::class);
ModuleInstallationRepository::SetInstance($oModuleInstallationRepository);
$oModuleInstallationRepository->expects($this->once())
->method('GetApplicationVersion')
->willReturn($sExpectedReturnedValue);
}
}

View File

@@ -0,0 +1,22 @@
<?php
use Combodo\iTop\Core\Email\Transport\SymfonyPHPMailTransport;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Symfony\Component\Mime\Email;
class SymfonyPHPMailTransportTest extends ItopTestCase
{
public function testPrepareMustNotThrowErrorWhenToHeaderIsMissing(): void
{
$oEmail = (new Email())
->from('sender@example.com')
->cc('cc1@example.com', 'cc2@example.com')
->text('Body');
$oTransport = new SymfonyPHPMailTransport();
$oTransport->prepareTo($oEmail);
$this->assertTrue(true); // if no error is thrown, the test passes
}
}