Compare commits

...

10 Commits

Author SHA1 Message Date
Molkobain
175b88de30 N°9144 - Fix typo in English sentence 2026-08-26 19:24:25 +02:00
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
12 changed files with 195 additions and 26 deletions

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

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

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

@@ -70,7 +70,7 @@ $("[data-role=\"setup-collapsable-options--toggler\"").on('click', function() {
$("#force-uninstall").on("click", function() {
let $this = $(this);
let bForceUninstall = $this.prop("checked");
if( bForceUninstall && !confirm('Beware, uninstalling extensions flagged as non uninstallable may result in data corruption and application crashes. Are you sure you want to continue ?')){
if( bForceUninstall && !confirm('Beware, uninstalling extensions flagged as non uninstallable may result in data corruption and application crashes. Are you sure you want to continue?')){
$this.prop("checked",false);
}
});

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

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

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

@@ -8,7 +8,7 @@ class SymfonyPHPMailTransportTest extends ItopTestCase
{
public function testPrepareMustNotThrowErrorWhenToHeaderIsMissing(): void
{
$oEmail = new Email()
$oEmail = (new Email())
->from('sender@example.com')
->cc('cc1@example.com', 'cc2@example.com')
->text('Body');