From 830a74118be15eeb2c2ce0473d1e581330ef4a13 Mon Sep 17 00:00:00 2001 From: Timothee Date: Wed, 22 Oct 2025 10:40:53 +0200 Subject: [PATCH 01/11] =?UTF-8?q?N=C2=B08762=20-=20Allow=20uninstall=20at?= =?UTF-8?q?=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/extensionsmap.class.inc.php | 85 +++++++++++++++++++------- setup/moduleinstallation.class.inc.php | 3 +- setup/runtimeenv.class.inc.php | 1 + setup/wizardsteps.class.inc.php | 8 ++- 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index d0cf51851f..7e89473419 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -110,6 +110,17 @@ public function __construct() $this->bVisible = true; $this->aMissingDependencies = array(); } + + public function IsUninstallable() + { + foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo){ + $bUninstallable = $aModuleInfo['uninstallable'] ? $aModuleInfo['uninstallable'] === 'yes' : false; + if (!$bUninstallable) { + return false; + } + } + return true; + } } /** @@ -253,6 +264,15 @@ protected function AddExtension(iTopExtension $oNewExtension) $this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension; } + public function Get($sExtensionCode):?iTopExtension{ + foreach($this->aExtensions as $oExtension) { + if ($oExtension->sCode == $sExtensionCode) { + return $oExtension; + } + } + return null; + } + /** * Read (recursively) a directory to find if it contains extensions (or modules) * @@ -277,8 +297,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) $aSubDirectories = array(); // First check if there is an extension.xml file in this directory - if (is_readable($sSearchDir.'/extension.xml')) - { + if (is_readable($sSearchDir.'/extension.xml')) { $oXml = new XMLParameters($sSearchDir.'/extension.xml'); $oExtension = new iTopExtension(); $oExtension->sCode = $oXml->Get('extension_code'); @@ -317,11 +336,11 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) // to this extension $sModuleId = $aModuleInfo[1]; list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); - if ($sModuleVersion == '') - { + if ($sModuleVersion == '') { // Provide a default module version since version is mandatory when recording ExtensionInstallation $sModuleVersion = '0.0.1'; } + $aModuleInfo[2]['uninstallable'] ??= 'yes'; if (($sParentExtensionId !== null) && (array_key_exists($sParentExtensionId, $this->aExtensions)) && ($this->aExtensions[$sParentExtensionId] instanceof iTopExtension)) { // Already inside an extension, let's add this module the list of modules belonging to this extension @@ -329,8 +348,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) $this->aExtensions[$sParentExtensionId]->aModuleVersion[$sModuleName] = $sModuleVersion; $this->aExtensions[$sParentExtensionId]->aModuleInfo[$sModuleName] = $aModuleInfo[2]; } - else - { + else { // Not already inside an folder containing an 'extension.xml' file // Ignore non-visible modules and auto-select ones, since these are never prompted @@ -452,6 +470,17 @@ public function MarkAsChosen($sExtensionCode, $bMark = true) } } + + public function MarkAsUninstallable($sExtensionCode, $bMark = true) + { + foreach($this->aExtensions as $oExtension) { + if ($oExtension->sCode == $sExtensionCode) { + $oExtension->bUninstallable = $bMark; + break; + } + } + } + /** * Tells if a given extension(code) is marked as chosen * @param string $sExtensionCode @@ -530,6 +559,10 @@ public function LoadChoicesFromDatabase(Config $oConfig) foreach($aInstalledExtensions as $aDBInfo) { $this->MarkAsChosen($aDBInfo['code']); + $sUninstallable = $aDBInfo['uninstallable'] ?? 'yes'; + $this->MarkAsUninstallable($sUninstallable); + file_put_contents('C:/tmp/install.log', "\nSetInstalledVersion of ".$aDBInfo['code']." to ".$aDBInfo['version'], FILE_APPEND); + $this->SetInstalledVersion($aDBInfo['code'], $aDBInfo['version']); } return true; @@ -572,30 +605,24 @@ public function IsExtensionObsoletedByAnother(iTopExtension $oExtension) public function NormalizeOldExtensions($sInSourceOnly = iTopExtension::SOURCE_MANUAL) { $aSignatures = $this->GetOldExtensionsSignatures(); - foreach($aSignatures as $sExtensionCode => $aExtensionSignatures) - { + foreach($aSignatures as $sExtensionCode => $aExtensionSignatures) { $bFound = false; - foreach($aExtensionSignatures['versions'] as $sVersion => $aModules) - { + foreach($aExtensionSignatures['versions'] as $sVersion => $aModules) { $bInstalled = true; - foreach($aModules as $sModuleId) - { - if(!$this->ModuleIsPresent($sModuleId, $sInSourceOnly)) - { + foreach($aModules as $sModuleId) { + if(!$this->ModuleIsPresent($sModuleId, $sInSourceOnly)) { $bFound = false; break; // One missing module is enough to determine that the extension/version is not present } - else - { - $bInstalled = $bInstalled && (!$this->ModuleIsInstalled($sModuleId, $sInSourceOnly)); + else { + $bInstalled = $bInstalled && $this->ModuleIsInstalled($sModuleId, $sInSourceOnly); $bFound = true; } } if ($bFound) break; // The current version matches the signature } - if ($bFound) - { + if ($bFound) { $oExtension = new iTopExtension(); $oExtension->sCode = $sExtensionCode; $oExtension->sLabel = $aExtensionSignatures['label']; @@ -603,15 +630,15 @@ public function NormalizeOldExtensions($sInSourceOnly = iTopExtension::SOURCE_MA $oExtension->sDescription = $aExtensionSignatures['description']; $oExtension->sVersion = $sVersion; $oExtension->aModules = array(); - if ($bInstalled) - { + if ($bInstalled) { $oExtension->sInstalledVersion = $sVersion; $oExtension->bMarkedAsChosen = true; } - foreach($aModules as $sModuleId) - { + foreach($aModules as $sModuleId) { list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); $oExtension->aModules[] = $sModuleName; + /*NEW!*/ + $oExtension->aModuleInfo[$sModuleName] = $this->aExtensions[$sModuleId]->aModuleInfo[$sModuleName]; } $this->ReplaceModulesByNormalizedExtension($aExtensionSignatures['versions'][$sVersion], $oExtension); } @@ -1316,6 +1343,18 @@ protected function GetOldExtensionsSignatures() ), ), ), + 'combodo-test-old-ext' => + array ( + 'label' => 'Old extension', + 'description' => 'Test retrocompat', + 'versions' => + array ( + '1.0.0' => + array ( + 0 => 'my-test/1.0.0', + ), + ), + ), ); } } diff --git a/setup/moduleinstallation.class.inc.php b/setup/moduleinstallation.class.inc.php index 535f22daf3..823d0283c0 100644 --- a/setup/moduleinstallation.class.inc.php +++ b/setup/moduleinstallation.class.inc.php @@ -48,7 +48,7 @@ public static function Init() MetaModel::Init_AddAttribute(new AttributeDateTime("installed", array("allowed_values" => null, "sql" => "installed", "default_value" => null, "is_null_allowed" => true, "depends_on" => array()))); MetaModel::Init_AddAttribute(new AttributeText("comment", array("allowed_values" => null, "sql" => "comment", "default_value" => null, "is_null_allowed" => true, "depends_on" => array()))); MetaModel::Init_AddAttribute(new AttributeExternalKey("parent_id", array("targetclass" => "ModuleInstallation", "jointype" => "", "allowed_values" => null, "sql" => "parent_id", "is_null_allowed" => true, "on_target_delete" => DEL_MANUAL, "depends_on" => array()))); - + MetaModel::Init_AddAttribute(new AttributeEnum("uninstallable", array("allowed_values"=>new ValueSetEnum('yes,no,maybe'), "sql"=>"uninstallable", "default_value"=>'yes', "is_null_allowed"=>false, "depends_on"=>array()))); // Display lists MetaModel::Init_SetZListItems('details', array('name', 'version', 'installed', 'comment', 'parent_id')); // Attributes to be displayed for the complete details @@ -87,6 +87,7 @@ public static function Init() MetaModel::Init_AddAttribute(new AttributeString("label", array("allowed_values"=>null, "sql"=>"label", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeString("version", array("allowed_values"=>null, "sql"=>"version", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeString("source", array("allowed_values"=>null, "sql"=>"source", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("uninstallable", array("allowed_values"=>new ValueSetEnum('yes,no,maybe'), "sql"=>"uninstallable", "default_value"=>'yes', "is_null_allowed"=>false, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeDateTime("installed", array("allowed_values"=>null, "sql"=>"installed", "default_value"=>'NOW()', "is_null_allowed"=>false, "depends_on"=>array()))); diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 0051ddaaaa..08ff074d99 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -805,6 +805,7 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect $oInstallRec->Set('label', $oExtension->sLabel); $oInstallRec->Set('version', $oExtension->sVersion); $oInstallRec->Set('source', $oExtension->sSource); + $oInstallRec->Set('uninstallable', $oExtension->IsUninstallable() ? 'yes' : 'no'); $oInstallRec->Set('installed', $iInstallationTime); $oInstallRec->DBInsertNoReload(); } diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index d954d4ecd9..963508c95b 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -1931,7 +1931,7 @@ protected function GetStepInfo($idx = null) if (@file_exists($this->GetSourceFilePath())) { - // Found an "installation.xml" file, let's us tis definition for the wizard + // Found an "installation.xml" file, let's use this definition for the wizard $aParams = new XMLParameters($this->GetSourceFilePath()); $aSteps = $aParams->Get('steps', array()); @@ -2044,8 +2044,12 @@ protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDe $sDataId = 'data-id="'.utils::EscapeHtml($aChoice['extension_code']).'"'; $sId = utils::EscapeHtml($aChoice['extension_code']); $bIsDefault = array_key_exists($sChoiceId, $aDefaults); + + $oExtension = $this->oExtensionsMap->Get($aChoice['extension_code']); + + $bIsUninstallable = $oExtension ? $oExtension->IsUninstallable() : true; $bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId); - $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || ($this->bUpgrade && $bIsDefault); + $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $oExtension->sInstalledVersion !== '' && !$bIsUninstallable; $bDisabled = false; if ($bMandatory) { $oPage->add('
 '); From 350deb080c6541185456fffffbcb78d1f24e34c0 Mon Sep 17 00:00:00 2001 From: Timothee Date: Wed, 22 Oct 2025 10:45:31 +0200 Subject: [PATCH 02/11] =?UTF-8?q?N=C2=B08762=20-=20Allow=20uninstall=20at?= =?UTF-8?q?=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/runtimeenv.class.inc.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 08ff074d99..9354080247 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -756,6 +756,7 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect $aModuleData = $aAvailableModules[$sModuleId]; $sName = $sModuleId; $sVersion = $aModuleData['version_code']; + $sUninstallable = $aModuleData['uninstallable'] ?? 'yes'; $aComments = array(); $aComments[] = $sShortComment; if ($aModuleData['mandatory']) { @@ -783,6 +784,7 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect $oInstallRec->Set('comment', $sComment); $oInstallRec->Set('parent_id', $iMainItopRecord); $oInstallRec->Set('installed', $iInstallationTime); + $oInstallRec->Set('uninstallable', $sUninstallable); $oInstallRec->DBInsertNoReload(); } From 233befd7dd0ab066154199ce27830aea2bd956ba Mon Sep 17 00:00:00 2001 From: Timothee Date: Thu, 23 Oct 2025 09:54:49 +0200 Subject: [PATCH 03/11] =?UTF-8?q?N=C2=B08762=20-=20Add=20warning=20for=20n?= =?UTF-8?q?on-uninstallable=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/wizardsteps.class.inc.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 963508c95b..9c55ae0c08 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -1436,6 +1436,7 @@ protected function DisplayStep($oPage) $oPage->add_style("div.choice a { text-decoration:none; font-weight: bold; color: #1C94C4 }"); $oPage->add_style("div.description { margin-left: 2em; }"); $oPage->add_style(".choice-disabled { color: #999; }"); + $oPage->add_style("input.unremovable { accent-color: orangered;}"); $aModules = SetupUtils::AnalyzeInstallation($this->oWizard); $sManualInstallError = SetupUtils::CheckManualInstallDirEmpty($aModules, @@ -2051,17 +2052,17 @@ protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDe $bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId); $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $oExtension->sInstalledVersion !== '' && !$bIsUninstallable; $bDisabled = false; + $sUnremovable = $bIsUninstallable ? '' : 'unremovable'; if ($bMandatory) { $oPage->add('
 '); $bDisabled = true; } else if ($bSelected) { - $oPage->add('
 '); + $oPage->add('
 '); } else { - $oPage->add('
 '); + $oPage->add('
 '); } - $this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled); + $this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled, $bIsUninstallable); $oPage->add('
'); - $index++; } $sChoiceName = null; $sDisabled = ''; @@ -2127,12 +2128,14 @@ protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDe } } - protected function DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled = false) + protected function DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled = false, $bUninstallable = true) { $sMoreInfo = (isset($aChoice['more_info']) && ($aChoice['more_info'] != '')) ? 'More information' : ''; $sSourceLabel = isset($aChoice['source_label']) ? $aChoice['source_label'] : ''; $sId = utils::EscapeHtml($aChoice['extension_code']); - $oPage->add(' '.$sMoreInfo); + $sUninstallationWarning = $bUninstallable ? '' : '(!)'; + + $oPage->add(' '.$sUninstallationWarning.' '.$sMoreInfo.''); $sDescription = isset($aChoice['description']) ? utils::EscapeHtml($aChoice['description']) : ''; $oPage->add('
'.$sDescription.''); if (isset($aChoice['sub_options'])) { From 8a23a326aacaac45cdb340d6dc7f0a13c03b6b99 Mon Sep 17 00:00:00 2001 From: Timothee Date: Thu, 23 Oct 2025 09:57:45 +0200 Subject: [PATCH 04/11] =?UTF-8?q?N=C2=B08762=20-=20Remove=20trace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/extensionsmap.class.inc.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 7e89473419..0b1cae89fa 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -561,8 +561,6 @@ public function LoadChoicesFromDatabase(Config $oConfig) $this->MarkAsChosen($aDBInfo['code']); $sUninstallable = $aDBInfo['uninstallable'] ?? 'yes'; $this->MarkAsUninstallable($sUninstallable); - file_put_contents('C:/tmp/install.log', "\nSetInstalledVersion of ".$aDBInfo['code']." to ".$aDBInfo['version'], FILE_APPEND); - $this->SetInstalledVersion($aDBInfo['code'], $aDBInfo['version']); } return true; From 7ab588328fd64b6d9f771183168cb5372b49f579 Mon Sep 17 00:00:00 2001 From: Timothee Date: Thu, 23 Oct 2025 09:58:53 +0200 Subject: [PATCH 05/11] =?UTF-8?q?N=C2=B08762=20-=20Remove=20temporary=20te?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/extensionsmap.class.inc.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 0b1cae89fa..75818a2951 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -1341,18 +1341,6 @@ protected function GetOldExtensionsSignatures() ), ), ), - 'combodo-test-old-ext' => - array ( - 'label' => 'Old extension', - 'description' => 'Test retrocompat', - 'versions' => - array ( - '1.0.0' => - array ( - 0 => 'my-test/1.0.0', - ), - ), - ), ); } } From ada63ab615ce4e91a57e72897d5006525dd122e4 Mon Sep 17 00:00:00 2001 From: Timothee Date: Fri, 24 Oct 2025 14:08:21 +0200 Subject: [PATCH 06/11] =?UTF-8?q?N=C2=B08762=20-=20Force=20uninstallation?= =?UTF-8?q?=20flag=20(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/wizardsteps.class.inc.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 9c55ae0c08..8d3f6fe5a7 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -997,6 +997,26 @@ final protected function AddUseSymlinksFlagOption(WebPage $oPage): void ); } } + + final protected function AddForceUninstallFlagOption(WebPage $oPage): void + { + $sChecked = $this->oWizard->GetParameter('force-uninstall', false) ? ' checked ' : ''; + $oPage->add('
'); + $oPage->add('Advanced parameters'); + $oPage->p('
'); + + $oPage->add_ready_script(<<<'JS' +$("#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 ?')){ + $this.prop("checked",false); + } +}); +JS + ); + } } @@ -1181,6 +1201,7 @@ public function ProcessParams($bMoveForward = true) { $this->oWizard->SaveParameter('application_url', ''); $this->oWizard->SaveParameter('graphviz_path', ''); + $this->oWizard->SaveParameter('force-uninstall', false); return array('class' => 'WizStepModulesChoice', 'state' => 'start_upgrade'); } @@ -1223,6 +1244,7 @@ public function Display(WebPage $oPage) ); $this->AddUseSymlinksFlagOption($oPage); + $this->AddForceUninstallFlagOption($oPage); } public function AsyncAction(WebPage $oPage, $sCode, $aParameters) From 76731f7d41c048f901876df00f3192f240e5ee53 Mon Sep 17 00:00:00 2001 From: Timothee Date: Fri, 24 Oct 2025 14:44:22 +0200 Subject: [PATCH 07/11] =?UTF-8?q?N=C2=B08762=20-=20Force=20uninstallation?= =?UTF-8?q?=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/wizardsteps.class.inc.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 8d3f6fe5a7..f7752c2d9d 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -2072,7 +2072,8 @@ protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDe $bIsUninstallable = $oExtension ? $oExtension->IsUninstallable() : true; $bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId); - $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $oExtension->sInstalledVersion !== '' && !$bIsUninstallable; + $bDisableUninstallCheck = (bool)$this->oWizard->GetParameter('force-uninstall', false); + $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $oExtension->sInstalledVersion !== '' && !$bIsUninstallable && !$bDisableUninstallCheck;; $bDisabled = false; $sUnremovable = $bIsUninstallable ? '' : 'unremovable'; if ($bMandatory) { From 95c81390ad760ea5b4e8931808cbcca77ec470c5 Mon Sep 17 00:00:00 2001 From: Timothee Date: Thu, 30 Oct 2025 11:47:12 +0100 Subject: [PATCH 08/11] =?UTF-8?q?N=C2=B08762=20-=20Code=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/extensionsmap.class.inc.php | 8 ++++---- setup/wizardsteps.class.inc.php | 25 +++++++++---------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 75818a2951..8c9a8fa02c 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -113,8 +113,8 @@ public function __construct() public function IsUninstallable() { - foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo){ - $bUninstallable = $aModuleInfo['uninstallable'] ? $aModuleInfo['uninstallable'] === 'yes' : false; + foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) { + $bUninstallable = $aModuleInfo['uninstallable'] === 'yes'; if (!$bUninstallable) { return false; } @@ -264,7 +264,8 @@ protected function AddExtension(iTopExtension $oNewExtension) $this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension; } - public function Get($sExtensionCode):?iTopExtension{ + public function Get($sExtensionCode):?iTopExtension + { foreach($this->aExtensions as $oExtension) { if ($oExtension->sCode == $sExtensionCode) { return $oExtension; @@ -635,7 +636,6 @@ public function NormalizeOldExtensions($sInSourceOnly = iTopExtension::SOURCE_MA foreach($aModules as $sModuleId) { list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); $oExtension->aModules[] = $sModuleName; - /*NEW!*/ $oExtension->aModuleInfo[$sModuleName] = $this->aExtensions[$sModuleId]->aModuleInfo[$sModuleName]; } $this->ReplaceModulesByNormalizedExtension($aExtensionSignatures['versions'][$sVersion], $oExtension); diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index f7752c2d9d..39ff41ec31 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -2060,6 +2060,7 @@ protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDe if ($bAllDisabled) { $sAllDisabled = 'disabled data-disabled="disabled" '; } + $bDisableUninstallCheck = (bool)$this->oWizard->GetParameter('force-uninstall', false); foreach ($aOptions as $index => $aChoice) { $sAttributes = ''; @@ -2068,12 +2069,10 @@ protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDe $sId = utils::EscapeHtml($aChoice['extension_code']); $bIsDefault = array_key_exists($sChoiceId, $aDefaults); - $oExtension = $this->oExtensionsMap->Get($aChoice['extension_code']); - - $bIsUninstallable = $oExtension ? $oExtension->IsUninstallable() : true; + $bIsUninstallable = $this->oExtensionsMap->Get($aChoice['extension_code'])->IsUninstallable(); $bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId); - $bDisableUninstallCheck = (bool)$this->oWizard->GetParameter('force-uninstall', false); - $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $oExtension->sInstalledVersion !== '' && !$bIsUninstallable && !$bDisableUninstallCheck;; + + $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $bIsDefault && !$bIsUninstallable && !$bDisableUninstallCheck;; $bDisabled = false; $sUnremovable = $bIsUninstallable ? '' : 'unremovable'; if ($bMandatory) { @@ -2091,23 +2090,19 @@ protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDe $sDisabled = ''; $bDisabled = false; $sChoiceIdNone = null; - foreach($aAlternatives as $index => $aChoice) - { + foreach($aAlternatives as $index => $aChoice) { $sChoiceId = $sParentId.self::$SEP.$index; - if ($sChoiceName == null) - { + if ($sChoiceName == null) { $sChoiceName = $sChoiceId; // All radios share the same name } $bIsDefault = array_key_exists($sChoiceName, $aDefaults) && ($aDefaults[$sChoiceName] == $sChoiceId); $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || ($this->bUpgrade && $bIsDefault); - if ($bMandatory || $bAllDisabled) - { + if ($bMandatory || $bAllDisabled) { // One choice is mandatory, all alternatives are disabled $sDisabled = ' disabled data-disabled="disabled"'; $bDisabled = true; } - if ( (!isset($aChoice['sub_options']) || (count($aChoice['sub_options']) == 0)) && (!isset($aChoice['modules']) || (count($aChoice['modules']) == 0)) ) - { + if ( (!isset($aChoice['sub_options']) || (count($aChoice['sub_options']) == 0)) && (!isset($aChoice['modules']) || (count($aChoice['modules']) == 0)) ) { $sChoiceIdNone = $sChoiceId; // the "None" / empty choice } } @@ -2139,15 +2134,13 @@ protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDe $sAttributes = ' checked '; } $sHidden = ''; - if ($bMandatory && $bDisabled) - { + if ($bMandatory && $bDisabled) { $sAttributes = ' checked '; $sHidden = ''; } $oPage->add('
'.$sHidden.' '); $this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled && !$bSelected); $oPage->add('
'); - $index++; } } From 36a52842cb84c5ca51836132300b509f2f631dfb Mon Sep 17 00:00:00 2001 From: Timothee Date: Thu, 30 Oct 2025 16:59:12 +0100 Subject: [PATCH 09/11] =?UTF-8?q?N=C2=B08762=20-=20Small=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/wizardsteps.class.inc.php | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 39ff41ec31..96068a9dc5 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -2054,16 +2054,10 @@ protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDe { $aOptions = isset($aStepInfo['options']) ? $aStepInfo['options'] : array(); $aAlternatives = isset($aStepInfo['alternatives']) ? $aStepInfo['alternatives'] : array(); - $index = 0; - $sAllDisabled = ''; - if ($bAllDisabled) { - $sAllDisabled = 'disabled data-disabled="disabled" '; - } $bDisableUninstallCheck = (bool)$this->oWizard->GetParameter('force-uninstall', false); foreach ($aOptions as $index => $aChoice) { - $sAttributes = ''; $sChoiceId = $sParentId.self::$SEP.$index; $sDataId = 'data-id="'.utils::EscapeHtml($aChoice['extension_code']).'"'; $sId = utils::EscapeHtml($aChoice['extension_code']); @@ -2071,18 +2065,14 @@ protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDe $bIsUninstallable = $this->oExtensionsMap->Get($aChoice['extension_code'])->IsUninstallable(); $bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId); - $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $bIsDefault && !$bIsUninstallable && !$bDisableUninstallCheck;; - $bDisabled = false; - $sUnremovable = $bIsUninstallable ? '' : 'unremovable'; - if ($bMandatory) { - $oPage->add('
 '); - $bDisabled = true; - } else if ($bSelected) { - $oPage->add('
 '); - } else { - $oPage->add('
 '); - } + $bDisabled = $bMandatory || $bAllDisabled; + $bChecked = $bMandatory || $bSelected; + $sChecked = $bChecked ? ' checked ' : ''; + $sDisabled = $bDisabled ? ' disabled data-disabled="disabled" ' : ''; + $sUnremovable = !$bIsUninstallable ? ' unremovable ' : ''; + $sHiddenInput = $bDisabled && $bChecked ? '' : ''; + $oPage->add('
'.$sHiddenInput.' '); $this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled, $bIsUninstallable); $oPage->add('
'); } From a850ea8fad7da46f5545db6b3a9f8d55f93ca462 Mon Sep 17 00:00:00 2001 From: Timothee Date: Mon, 3 Nov 2025 15:54:49 +0100 Subject: [PATCH 10/11] =?UTF-8?q?N=C2=B08762=20-=20Remove=20unused=20flag,?= =?UTF-8?q?=20add=20@since=20to=20new=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/extensionsmap.class.inc.php | 44 +++++++++++----------- setup/modulediscovery.class.inc.php | 2 +- setup/modulediscovery/ModuleFileReader.php | 12 ++++-- setup/runtimeenv.class.inc.php | 2 +- setup/wizardsteps.class.inc.php | 8 ++-- 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 8c9a8fa02c..0def7ae058 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -111,7 +111,11 @@ public function __construct() $this->aMissingDependencies = array(); } - public function IsUninstallable() + /** + * @since 3.3.0 + * @return bool + */ + public function CanBeUninstalled() { foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) { $bUninstallable = $aModuleInfo['uninstallable'] === 'yes'; @@ -264,10 +268,16 @@ protected function AddExtension(iTopExtension $oNewExtension) $this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension; } - public function Get($sExtensionCode):?iTopExtension + /** + * @since 3.3.0 + * @param string $sExtensionCode + * + * @return \iTopExtension|null + */ + public function Get(string $sExtensionCode):?iTopExtension { foreach($this->aExtensions as $oExtension) { - if ($oExtension->sCode == $sExtensionCode) { + if ($oExtension->sCode === $sExtensionCode) { return $oExtension; } } @@ -335,19 +345,19 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) // If we are not already inside a formal extension, then the module itself is considered // as an extension, otherwise, the module is just added to the list of modules belonging // to this extension - $sModuleId = $aModuleInfo[1]; + $sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID]; list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); if ($sModuleVersion == '') { // Provide a default module version since version is mandatory when recording ExtensionInstallation $sModuleVersion = '0.0.1'; } - $aModuleInfo[2]['uninstallable'] ??= 'yes'; + $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]['uninstallable'] ??= 'yes'; if (($sParentExtensionId !== null) && (array_key_exists($sParentExtensionId, $this->aExtensions)) && ($this->aExtensions[$sParentExtensionId] instanceof iTopExtension)) { // Already inside an extension, let's add this module the list of modules belonging to this extension $this->aExtensions[$sParentExtensionId]->aModules[] = $sModuleName; $this->aExtensions[$sParentExtensionId]->aModuleVersion[$sModuleName] = $sModuleVersion; - $this->aExtensions[$sParentExtensionId]->aModuleInfo[$sModuleName] = $aModuleInfo[2]; + $this->aExtensions[$sParentExtensionId]->aModuleInfo[$sModuleName] = $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]; } else { // Not already inside an folder containing an 'extension.xml' file @@ -355,7 +365,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) // Ignore non-visible modules and auto-select ones, since these are never prompted // as a choice to the end-user $bVisible = true; - if (!$aModuleInfo[2]['visible'] || isset($aModuleInfo[2]['auto_select'])) + if (!$aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]['visible'] || isset($aModuleInfo[2]['auto_select'])) { $bVisible = false; } @@ -363,15 +373,15 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) // Let's create a "fake" extension from this module (containing just this module) for backwards compatibility $oExtension = new iTopExtension(); $oExtension->sCode = $sModuleName; - $oExtension->sLabel = $aModuleInfo[2]['label']; + $oExtension->sLabel = $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]['label']; $oExtension->sDescription = ''; $oExtension->sVersion = $sModuleVersion; $oExtension->sSource = $sSource; - $oExtension->bMandatory = $aModuleInfo[2]['mandatory']; - $oExtension->sMoreInfoUrl = $aModuleInfo[2]['doc.more_information']; + $oExtension->bMandatory = $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]['mandatory']; + $oExtension->sMoreInfoUrl = $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]['doc.more_information']; $oExtension->aModules = array($sModuleName); $oExtension->aModuleVersion[$sModuleName] = $sModuleVersion; - $oExtension->aModuleInfo[$sModuleName] = $aModuleInfo[2]; + $oExtension->aModuleInfo[$sModuleName] = $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]; $oExtension->sSourceDir = $sSearchDir; $oExtension->bVisible = $bVisible; $this->AddExtension($oExtension); @@ -472,16 +482,6 @@ public function MarkAsChosen($sExtensionCode, $bMark = true) } - public function MarkAsUninstallable($sExtensionCode, $bMark = true) - { - foreach($this->aExtensions as $oExtension) { - if ($oExtension->sCode == $sExtensionCode) { - $oExtension->bUninstallable = $bMark; - break; - } - } - } - /** * Tells if a given extension(code) is marked as chosen * @param string $sExtensionCode @@ -560,8 +560,6 @@ public function LoadChoicesFromDatabase(Config $oConfig) foreach($aInstalledExtensions as $aDBInfo) { $this->MarkAsChosen($aDBInfo['code']); - $sUninstallable = $aDBInfo['uninstallable'] ?? 'yes'; - $this->MarkAsUninstallable($sUninstallable); $this->SetInstalledVersion($aDBInfo['code'], $aDBInfo['version']); } return true; diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 6340f6654c..89cc864401 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -520,7 +520,7 @@ protected static function ListModuleFiles($sRelDir, $sRootDir) $sModuleFilePath = $sDirectory.'/'.$sFile; try { $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sDirectory.'/'.$sFile); - SetupWebPage::AddModule($sModuleFilePath, $aModuleInfo[1], $aModuleInfo[2]); + SetupWebPage::AddModule($sModuleFilePath, $aModuleInfo[ModuleFileReader::MODULE_INFO_ID], $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]); } catch(ModuleFileReaderException $e){ continue; } diff --git a/setup/modulediscovery/ModuleFileReader.php b/setup/modulediscovery/ModuleFileReader.php index c5a7412a30..b09d3ef3e7 100644 --- a/setup/modulediscovery/ModuleFileReader.php +++ b/setup/modulediscovery/ModuleFileReader.php @@ -32,6 +32,10 @@ class ModuleFileReader { "method_exists" ]; + const MODULE_INFO_PATH = 0; + const MODULE_INFO_ID = 1; + const MODULE_INFO_CONFIG = 2; + const STATIC_CALLWHITELIST=[ "utils::GetItopVersionWikiSyntax" ]; @@ -168,7 +172,7 @@ public function ReadModuleFileInformationUnsafe(string $sModuleFilePath) : array private function CompleteModuleInfoWithFilePath(array &$aModuleInfo) { if (count($aModuleInfo)==3) { - $aModuleInfo[2]['module_file_path'] = $aModuleInfo[0]; + $aModuleInfo[static::MODULE_INFO_CONFIG]['module_file_path'] = $aModuleInfo[static::MODULE_INFO_PATH]; } } @@ -255,9 +259,9 @@ private function GetModuleInformationFromAddModuleCall(string $sModuleFilePath, } return [ - $sModuleFilePath, - $sModuleId, - $aModuleConfig, + static::MODULE_INFO_PATH => $sModuleFilePath, + static::MODULE_INFO_ID => $sModuleId, + static::MODULE_INFO_CONFIG => $aModuleConfig, ]; } diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 9354080247..e8052c434a 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -807,7 +807,7 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect $oInstallRec->Set('label', $oExtension->sLabel); $oInstallRec->Set('version', $oExtension->sVersion); $oInstallRec->Set('source', $oExtension->sSource); - $oInstallRec->Set('uninstallable', $oExtension->IsUninstallable() ? 'yes' : 'no'); + $oInstallRec->Set('uninstallable', $oExtension->CanBeUninstalled() ? 'yes' : 'no'); $oInstallRec->Set('installed', $iInstallationTime); $oInstallRec->DBInsertNoReload(); } diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 96068a9dc5..88c290c113 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -2063,17 +2063,17 @@ protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDe $sId = utils::EscapeHtml($aChoice['extension_code']); $bIsDefault = array_key_exists($sChoiceId, $aDefaults); - $bIsUninstallable = $this->oExtensionsMap->Get($aChoice['extension_code'])->IsUninstallable(); + $bCanBeUninstalled = $this->oExtensionsMap->Get($aChoice['extension_code'])->CanBeUninstalled(); $bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId); - $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $bIsDefault && !$bIsUninstallable && !$bDisableUninstallCheck;; + $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $bIsDefault && !$bCanBeUninstalled && !$bDisableUninstallCheck;; $bDisabled = $bMandatory || $bAllDisabled; $bChecked = $bMandatory || $bSelected; $sChecked = $bChecked ? ' checked ' : ''; $sDisabled = $bDisabled ? ' disabled data-disabled="disabled" ' : ''; - $sUnremovable = !$bIsUninstallable ? ' unremovable ' : ''; + $sUnremovable = !$bCanBeUninstalled ? ' unremovable ' : ''; $sHiddenInput = $bDisabled && $bChecked ? '' : ''; $oPage->add('
'.$sHiddenInput.' '); - $this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled, $bIsUninstallable); + $this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled, $bCanBeUninstalled); $oPage->add('
'); } $sChoiceName = null; From c4ca0bc2200a7702d218a5483f8879a8919f828c Mon Sep 17 00:00:00 2001 From: Timmy38 <101416770+Timmy38@users.noreply.github.com> Date: Wed, 5 Nov 2025 08:42:30 +0100 Subject: [PATCH 11/11] =?UTF-8?q?N=C2=B08762=20Apply=20suggestion=20from?= =?UTF-8?q?=20@Hipska?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Thomas Casteleyn --- setup/extensionsmap.class.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 0def7ae058..0e8fca1ebd 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -365,7 +365,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) // Ignore non-visible modules and auto-select ones, since these are never prompted // as a choice to the end-user $bVisible = true; - if (!$aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]['visible'] || isset($aModuleInfo[2]['auto_select'])) + if (!$aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]['visible'] || isset($aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]['auto_select'])) { $bVisible = false; }