Skip to content
83 changes: 53 additions & 30 deletions setup/extensionsmap.class.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ public function __construct()
$this->bVisible = true;
$this->aMissingDependencies = array();
}

/**
* @since 3.3.0
* @return bool
*/
public function CanBeUninstalled()
{
foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) {
$bUninstallable = $aModuleInfo['uninstallable'] === 'yes';
if (!$bUninstallable) {
Comment on lines +121 to +122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$bUninstallable = $aModuleInfo['uninstallable'] === 'yes';
if (!$bUninstallable) {
if (!filter_var($aModuleInfo['uninstallable'], FILTER_VALIDATE_BOOLEAN)) {

return false;
}
}
return true;
}
}

/**
Expand Down Expand Up @@ -253,6 +268,22 @@ protected function AddExtension(iTopExtension $oNewExtension)
$this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension;
}

/**
* @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) {
return $oExtension;
}
}
return null;
}

/**
* Read (recursively) a directory to find if it contains extensions (or modules)
*
Expand All @@ -277,8 +308,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');
Expand Down Expand Up @@ -315,44 +345,43 @@ 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 == '')
{
if ($sModuleVersion == '') {
// Provide a default module version since version is mandatory when recording ExtensionInstallation
$sModuleVersion = '0.0.1';
}
$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
{
else {
// Not already inside an folder containing an 'extension.xml' file

// 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[ModuleFileReader::MODULE_INFO_CONFIG]['auto_select']))
{
$bVisible = false;
}

// 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);
Expand Down Expand Up @@ -452,6 +481,7 @@ public function MarkAsChosen($sExtensionCode, $bMark = true)
}
}


/**
* Tells if a given extension(code) is marked as chosen
* @param string $sExtensionCode
Expand Down Expand Up @@ -572,46 +602,39 @@ 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'];
$oExtension->sSource = $sInSourceOnly;
$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;
$oExtension->aModuleInfo[$sModuleName] = $this->aExtensions[$sModuleId]->aModuleInfo[$sModuleName];
}
$this->ReplaceModulesByNormalizedExtension($aExtensionSignatures['versions'][$sVersion], $oExtension);
}
Expand Down
2 changes: 1 addition & 1 deletion setup/modulediscovery.class.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
12 changes: 8 additions & 4 deletions setup/modulediscovery/ModuleFileReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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"
];
Expand Down Expand Up @@ -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];
}
}

Expand Down Expand Up @@ -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,
];
}

Expand Down
3 changes: 2 additions & 1 deletion setup/moduleinstallation.class.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())));


Expand Down
3 changes: 3 additions & 0 deletions setup/runtimeenv.class.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']) {
Expand Down Expand Up @@ -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();
}

Expand All @@ -805,6 +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->CanBeUninstalled() ? 'yes' : 'no');
$oInstallRec->Set('installed', $iInstallationTime);
$oInstallRec->DBInsertNoReload();
}
Expand Down
Loading