mirror of
https://github.com/Combodo/iTop.git
synced 2026-07-31 10:58:19 +02:00
Compare commits
14 Commits
feature/88
...
3.3.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6e50a0ed7 | ||
|
|
76a410e00d | ||
|
|
c68887c2bc | ||
|
|
837b433622 | ||
|
|
52fca750b0 | ||
|
|
d1aedee245 | ||
|
|
85d50f31bd | ||
|
|
3c301affb3 | ||
|
|
147fae7f8c | ||
|
|
a5aa8c279f | ||
|
|
04f91ab4a2 | ||
|
|
8e0246601a | ||
|
|
e6cdcd0a5a | ||
|
|
2ab3081926 |
@@ -8,13 +8,6 @@
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalField;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalKey;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeFriendlyName;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeHierarchicalKey;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeLinkedSet;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeStopWatch;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeSubItem;
|
||||
|
||||
/**
|
||||
* Bulk export: Tabular export: abstract base class for all "tabular" exports.
|
||||
@@ -98,8 +91,8 @@ abstract class TabularBulkExport extends BulkExport
|
||||
{
|
||||
$aResult = [];
|
||||
switch (get_class($oAttDef)) {
|
||||
case AttributeExternalKey::class:
|
||||
case AttributeHierarchicalKey::class:
|
||||
case 'AttributeExternalKey':
|
||||
case 'AttributeHierarchicalKey':
|
||||
|
||||
$bAddFriendlyName = true;
|
||||
$oKeyAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
|
||||
@@ -149,7 +142,7 @@ abstract class TabularBulkExport extends BulkExport
|
||||
}
|
||||
break;
|
||||
|
||||
case AttributeStopWatch::class:
|
||||
case 'AttributeStopWatch':
|
||||
foreach (MetaModel::ListAttributeDefs($sClass) as $sSubAttCode => $oSubAttDef) {
|
||||
if ($oSubAttDef instanceof AttributeSubItem) {
|
||||
if ($oSubAttDef->GetParentAttCode() == $sAttCode) {
|
||||
|
||||
@@ -127,6 +127,9 @@ class DataFeatureRemovalController extends Controller
|
||||
|
||||
$aRemovedExtensions = $this->aExtensionsToCheck['to_be_removed'];
|
||||
$aHiddenInputs['removed_extensions'] = $this->ConvertIntoSetupFormat($aRemovedExtensions);
|
||||
|
||||
$aExtensionsNotUninstallable = $this->aExtensionsToCheck['extensions_not_uninstallable'];
|
||||
$aHiddenInputs['extensions_not_uninstallable'] = $this->ConvertIntoSetupFormat($aExtensionsNotUninstallable);
|
||||
}
|
||||
|
||||
$aParams['aAddedExtensions'] = $aAddedExtensions;
|
||||
@@ -344,7 +347,7 @@ class DataFeatureRemovalController extends Controller
|
||||
'uninstallable' => $oExtension->CanBeUninstalled(),
|
||||
'remote' => $oExtension->IsRemote(),
|
||||
'missing' => $oExtension->bRemovedFromDisk,
|
||||
'cannot-be-installed' => count($oExtension->aMissingDependencies) > 0,
|
||||
'dependency_issue' => $oExtension->HasDependencyIssue(),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -425,6 +428,7 @@ class DataFeatureRemovalController extends Controller
|
||||
$this->aExtensionsToCheck = [
|
||||
'to_be_installed' => [],
|
||||
'to_be_removed' => [],
|
||||
'extensions_not_uninstallable' => [],
|
||||
];
|
||||
foreach ($aAvailableExtensions as $sCode => &$aExtensionData) {
|
||||
if (!isset($aSelectedExtensionsFromUI[$sCode])) {
|
||||
@@ -438,6 +442,9 @@ class DataFeatureRemovalController extends Controller
|
||||
if (! $this->bForcedUninstallation && $aExtensionData['extra_flags']['uninstallable']) {
|
||||
$this->bForcedUninstallation = true;
|
||||
}
|
||||
if (false === $aExtensionData['extra_flags']['uninstallable'] || true === $aExtensionData['extra_flags']['remote']) {
|
||||
$this->aExtensionsToCheck['extensions_not_uninstallable'][] = $sCode;
|
||||
}
|
||||
} elseif (!$aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] === 'on') {
|
||||
$aExtensionData['extra_flags']['selected'] = true;
|
||||
$sLabel = $aAvailableExtensions[$sCode]['label'];
|
||||
|
||||
@@ -134,10 +134,6 @@ class DataCleanupService
|
||||
/** @var DBObject $oDependentObj */
|
||||
while ($oDependentObj = $oSet->Fetch()) {
|
||||
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
|
||||
if ($iDeletePropagationOption == DEL_MANUAL) {
|
||||
$this->oObjectService->SetIssue(get_class($oDependentObj));
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($oExtKeyAttDef->IsNullAllowed()) {
|
||||
// Optional external key, list to reset
|
||||
@@ -152,6 +148,12 @@ class DataCleanupService
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Mandatory external key
|
||||
if ($iDeletePropagationOption == DEL_MANUAL) {
|
||||
// Cannot be deleted automatically, must be handled manually
|
||||
$this->oObjectService->SetIssue(get_class($oDependentObj));
|
||||
continue;
|
||||
}
|
||||
// Propagate deletion only if not visited
|
||||
if ($this->IsVisited($oDependentObj)) {
|
||||
continue;
|
||||
|
||||
@@ -59,9 +59,11 @@ class StaticDeletionPlan
|
||||
{
|
||||
foreach ($aClasses as $sClass) {
|
||||
$oDeletionPlanItem = $this->GetInitialClassDeletionPlan($sClass);
|
||||
$oDeletionPlanEntity = new DeletionPlanEntity();
|
||||
$oDeletionPlanEntity->oDelete->Merge($oDeletionPlanItem);
|
||||
$this->aDeletionPlan[$sClass] = $oDeletionPlanEntity;
|
||||
// N°9831 Do not overwrite existing entity as it may already exist for this class if a previously processed class references it (issues/updates already accumulated must be kept)
|
||||
if (false === array_key_exists($sClass, $this->aDeletionPlan)) {
|
||||
$this->aDeletionPlan[$sClass] = new DeletionPlanEntity();
|
||||
}
|
||||
$this->aDeletionPlan[$sClass]->oDelete->Merge($oDeletionPlanItem);
|
||||
|
||||
$this->DeletionPlanForReferencingClasses($sClass);
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ function DoInstall(WebPage $oPage)
|
||||
|
||||
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {
|
||||
if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) {
|
||||
if (count($oExtension->aMissingDependencies) > 0) {
|
||||
if ($oExtension->HasDependencyIssue()) {
|
||||
$oPage->add('<div class="choice">');
|
||||
$oPage->add('<input type="checkbox" disabled> ');
|
||||
$sTitle = Dict::Format('iTopHub:InstallationEffect:MissingDependencies_Details', implode(', ', $oExtension->aMissingDependencies));
|
||||
|
||||
@@ -545,7 +545,7 @@ class iTopExtensionsMap
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $bKeepExtensionsHavingMissingDependencies && count($oExtension->aMissingDependencies) > 0) {
|
||||
if (! $bKeepExtensionsHavingMissingDependencies && $oExtension->HasDependencyIssue()) {
|
||||
//skip extensions with dependency issues
|
||||
continue;
|
||||
}
|
||||
@@ -562,19 +562,20 @@ class iTopExtensionsMap
|
||||
public function GetAllExtensionsOptionInfo(bool $bRemoteExtensionsShouldBeMandatory = true): array
|
||||
{
|
||||
$aRes = [];
|
||||
foreach ($this->GetAllExtensionsToDisplayInSetup(false, $bRemoteExtensionsShouldBeMandatory) as $sCode => $oExtension) {
|
||||
foreach ($this->GetAllExtensionsToDisplayInSetup(true, $bRemoteExtensionsShouldBeMandatory) as $sCode => $oExtension) {
|
||||
$aRes[] = [
|
||||
'extension_code' => $oExtension->sCode,
|
||||
'title' => $oExtension->sLabel,
|
||||
'description' => $oExtension->sDescription,
|
||||
'more_info' => $oExtension->sMoreInfoUrl,
|
||||
'default' => true, // by default offer to install all modules
|
||||
'modules' => $oExtension->aModules,
|
||||
'mandatory' => $oExtension->bMandatory,
|
||||
'source_label' => $oExtension->GetExtensionSourceLabel(),
|
||||
'uninstallable' => $oExtension->CanBeUninstalled(),
|
||||
'missing' => $oExtension->bRemovedFromDisk,
|
||||
'version' => $oExtension->sVersion,
|
||||
'extension_code' => $oExtension->sCode,
|
||||
'title' => $oExtension->sLabel,
|
||||
'description' => $oExtension->sDescription,
|
||||
'more_info' => $oExtension->sMoreInfoUrl,
|
||||
'default' => true, // by default offer to install all modules
|
||||
'modules' => $oExtension->aModules,
|
||||
'mandatory' => $oExtension->bMandatory,
|
||||
'dependency_issue' => $oExtension->HasDependencyIssue(),
|
||||
'source_label' => $oExtension->GetExtensionSourceLabel(),
|
||||
'uninstallable' => $oExtension->CanBeUninstalled(),
|
||||
'missing' => $oExtension->bRemovedFromDisk,
|
||||
'version' => $oExtension->sVersion,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use DBObjectSet;
|
||||
use IssueLog;
|
||||
use MetaModel;
|
||||
use SetupLog;
|
||||
use TagSetFieldData;
|
||||
|
||||
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
|
||||
|
||||
@@ -68,6 +69,10 @@ abstract class AbstractSetupAudit
|
||||
continue;
|
||||
}
|
||||
|
||||
if (MetaModel::IsSameFamily($sClass, TagSetFieldData::class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!MetaModel::IsStandaloneClass($sClass)) {
|
||||
$iCount = $this->Count($sClass);
|
||||
$this->aFinalClassesToCleanup[$sClass] = $iCount;
|
||||
|
||||
@@ -176,7 +176,7 @@ class iTopExtension
|
||||
};
|
||||
}
|
||||
|
||||
public function IsRemote(): string
|
||||
public function IsRemote(): bool
|
||||
{
|
||||
return $this->sSource === self::SOURCE_REMOTE;
|
||||
}
|
||||
@@ -200,4 +200,9 @@ class iTopExtension
|
||||
{
|
||||
return $this->sLabel !== '';
|
||||
}
|
||||
|
||||
public function HasDependencyIssue(): bool
|
||||
{
|
||||
return count($this->aMissingDependencies) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -933,6 +933,7 @@ class RunTimeEnvironment
|
||||
@chmod($sFinalConfig, 0440); // Read-only for owner and group, nothing for others
|
||||
|
||||
SetupUtils::rrmdir(dirname($sBuildConfig)); // Cleanup the temporary build dir if empty
|
||||
MetaModel::ResetAllCaches($this->sBuildEnv);
|
||||
MetaModel::ResetAllCaches($this->sFinalEnv);
|
||||
|
||||
if (! isset($_SESSION)) {
|
||||
|
||||
@@ -1661,11 +1661,14 @@ JS
|
||||
|
||||
$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, $aDirsToScan, $bAbortOnMissingDependency, $aModulesToLoad);
|
||||
|
||||
foreach ($aAvailableModules as $key => $aModule) {
|
||||
$bIsExtra = (array_key_exists('root_dir', $aModule) && (strpos($aModule['root_dir'], $sExtraDir) !== false)); // Some modules (root, datamodel) have no 'root_dir'
|
||||
if ($bIsExtra) {
|
||||
// Modules in data/production-modules/ are considered as mandatory and always installed
|
||||
$aAvailableModules[$key]['visible'] = false;
|
||||
$bRemoteExtensionsShouldBeMandatory = !$oWizard->GetParameter('force-uninstall', false);
|
||||
if ($bRemoteExtensionsShouldBeMandatory) {
|
||||
foreach ($aAvailableModules as $key => $aModule) {
|
||||
$bIsExtra = (array_key_exists('root_dir', $aModule) && (strpos($aModule['root_dir'], $sExtraDir) !== false)); // Some modules (root, datamodel) have no 'root_dir'
|
||||
if ($bIsExtra) {
|
||||
// Modules in data/production-modules/ are considered as mandatory and always installed
|
||||
$aAvailableModules[$key]['visible'] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ class InstallationFileService
|
||||
continue;
|
||||
}
|
||||
if ($this->CanChooseUnpackageExtension($oExtension)) {
|
||||
if (($oExtension->bVisible) && (count($oExtension->aMissingDependencies) === 0)) {
|
||||
if ($oExtension->bVisible && !$oExtension->HasDependencyIssue()) {
|
||||
$aCurrentModules = [];
|
||||
$aUnselectableModules = [];
|
||||
$bIsExtensionSelectable = true;
|
||||
|
||||
@@ -112,6 +112,22 @@ JS);
|
||||
return;
|
||||
}
|
||||
|
||||
// When the setup reach this step, it already checked whether extensions were uninstallable (during WizStepModulesChoice or combodo-data-feature-removal). We only need to log what has been done.
|
||||
if ($this->oWizard->GetParameter('force-uninstall', false)) {
|
||||
SetupLog::Warning("User disabled uninstallation checks");
|
||||
}
|
||||
$aExtensionsRemoved = json_decode($this->oWizard->GetParameter('removed_extensions'), true) ?? [];
|
||||
$aExtensionsNotUninstallable = json_decode($this->oWizard->GetParameter('extensions_not_uninstallable'), true) ?? [];
|
||||
$aExtensionsForceUninstalled = [];
|
||||
foreach ($aExtensionsRemoved as $sExtensionCode => $sLabel) {
|
||||
if (in_array($sExtensionCode, $aExtensionsNotUninstallable)) {
|
||||
$aExtensionsForceUninstalled[] = $sExtensionCode;
|
||||
}
|
||||
}
|
||||
if (count($aExtensionsForceUninstalled)) {
|
||||
SetupLog::Warning("Extensions uninstalled forcefully : ".implode(',', $aExtensionsForceUninstalled));
|
||||
}
|
||||
|
||||
$oPage->add_ready_script(<<<JS
|
||||
$("#wiz_form").data("installation_status", "not started");
|
||||
ExecuteStep("");
|
||||
|
||||
@@ -31,8 +31,8 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
|
||||
$oWizard->SetParameter('db_tls_enabled', $this->oConfig->Get('db_tls.enabled'));
|
||||
$oWizard->SetParameter('db_tls_ca', $this->oConfig->Get('db_tls.ca') ?? '');
|
||||
$oWizard->SetParameter('display_choices', '');
|
||||
$oWizard->SetParameter('extensions_not_uninstallable', '[]');
|
||||
|
||||
$oWizard->SaveParameter('extensions_not_uninstallable', '[]');
|
||||
$oWizard->SaveParameter('use_symbolic_links', MFCompiler::UseSymbolicLinks());
|
||||
$oWizard->SaveParameter('force-uninstall', false);
|
||||
$oWizard->SaveParameter('skip_wizard', false);
|
||||
|
||||
@@ -587,7 +587,7 @@ EOF
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((isset($aChoice['mandatory']) && $aChoice['mandatory']) ||
|
||||
if ((isset($aChoice['mandatory']) && $aChoice['mandatory'] && (!isset($aChoice['dependency_issue']) || !$aChoice['dependency_issue'])) ||
|
||||
(isset($aSelectedChoices[$sChoiceId]) && ($aSelectedChoices[$sChoiceId] == $sChoiceId))) {
|
||||
$sDisplayChoices .= '<li>'.$aChoice['title'].'</li>';
|
||||
if (isset($aChoice['modules'])) {
|
||||
@@ -637,7 +637,7 @@ EOF
|
||||
if ($sChoiceName == null) {
|
||||
$sChoiceName = $sChoiceId;
|
||||
}
|
||||
if ((isset($aChoice['mandatory']) && $aChoice['mandatory']) ||
|
||||
if ((isset($aChoice['mandatory']) && $aChoice['mandatory'] && (!isset($aChoice['dependency_issue']) || !$aChoice['dependency_issue'])) ||
|
||||
(isset($aSelectedChoices[$sChoiceName]) && ($aSelectedChoices[$sChoiceName] == $sChoiceId))) {
|
||||
$sDisplayChoices .= '<li>'.$aChoice['title'].'</li>';
|
||||
if ($aSelectedExtensions !== null) {
|
||||
@@ -750,13 +750,17 @@ EOF
|
||||
$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] === $sChoiceId);
|
||||
$bMissingFromDisk = isset($aChoice['missing']) && $aChoice['missing'] === true;
|
||||
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']);
|
||||
$bInstalled = $bMissingFromDisk || $oITopExtension->bInstalled;
|
||||
$bInstalled = $bMissingFromDisk || $oITopExtension?->bInstalled ?? false;
|
||||
$bDependencyIssue = $oITopExtension?->HasDependencyIssue() ?? false;
|
||||
|
||||
$bChecked = $bSelected;
|
||||
$bDisabled = false;
|
||||
if ($bMissingFromDisk) {
|
||||
$bDisabled = true;
|
||||
$bChecked = false;
|
||||
} elseif ($bDependencyIssue) {
|
||||
$bDisabled = true;
|
||||
$bChecked = false;
|
||||
} elseif ($bMandatory) {
|
||||
$bDisabled = true;
|
||||
$bChecked = true;
|
||||
@@ -787,6 +791,8 @@ EOF
|
||||
|
||||
return [
|
||||
'uninstallable' => $bCanBeUninstalled,
|
||||
'dependency_issue' => $bDependencyIssue,
|
||||
'mandatory' => $bMandatory,
|
||||
'missing' => $bMissingFromDisk,
|
||||
'installed' => $bInstalled,
|
||||
'disabled' => $bDisabled,
|
||||
@@ -805,7 +811,7 @@ EOF
|
||||
$sChoiceId = $sParentId.self::$SEP.$index;
|
||||
$aFlags = $this->ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $this->bUpgrade);
|
||||
|
||||
if ($aFlags['disabled'] && !$aFlags['checked'] && !$aFlags['uninstallable'] && !$bDisableUninstallCheck) {
|
||||
if ($aFlags['disabled'] && !$aFlags['checked'] && !$bDisableUninstallCheck && (!$aFlags['uninstallable'] || $aFlags['mandatory'])) {
|
||||
$this->bCanMoveForward = false;//Disable "Next"
|
||||
}
|
||||
|
||||
@@ -882,6 +888,9 @@ EOF
|
||||
if (!$aFlags['uninstallable']) {
|
||||
$sTooltip .= '<div id="badge--'.$sId.'--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>';
|
||||
}
|
||||
if ($aFlags['dependency_issue']) {
|
||||
$sTooltip .= '<div id="badge--'.$sId.'--cannot-be-installed" class="ibo-badge ibo-block ibo-is-orange" title="This extension cannot be installed because one or more dependencies are not satisfied." >cannot be installed</div>';
|
||||
}
|
||||
|
||||
$sMetadata = '';
|
||||
if (isset($aChoice['version']) && isset($aChoice['source_label'])) {
|
||||
@@ -939,10 +948,6 @@ EOF
|
||||
|
||||
public function GetNextButtonLabel()
|
||||
{
|
||||
if (!$this->bCanMoveForward) {
|
||||
return 'Non-uninstallable extension missing';
|
||||
}
|
||||
|
||||
if ($this->GetStepInfo(1 + $this->GetStepIndex()) === null) {
|
||||
return 'Check compatibility';
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ class WizStepSummary extends AbstractWizStepInstall
|
||||
$oPage->add('<div class="closed"><a class="title ibo-setup-summary-title" href="#" aria-label="Extensions to be uninstalled">Extensions to be uninstalled</a>');
|
||||
|
||||
$aExtensionsRemoved = json_decode($this->oWizard->GetParameter('removed_extensions'), true) ?? [];
|
||||
$aExtensionsNotUninstallable = json_decode($this->oWizard->GetParameter('extensions_not_uninstallable')) ?? [];
|
||||
$aExtensionsNotUninstallable = json_decode($this->oWizard->GetParameter('extensions_not_uninstallable'), true) ?? [];
|
||||
if (count($aExtensionsRemoved) > 0) {
|
||||
$sExtensionsRemoved = '<ul>';
|
||||
foreach ($aExtensionsRemoved as $sExtensionCode => $sLabel) {
|
||||
|
||||
@@ -26,11 +26,11 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
|
||||
$aBadges = [];
|
||||
$bUninstallable = $aExtraFlags['uninstallable'] ?? true;
|
||||
$bMissingFromDisk = $aExtraFlags['missing'] ?? false;
|
||||
$bCannotBeInstalled = $aExtraFlags['cannot-be-installed'] ?? false;
|
||||
$bDependencyIssue = $aExtraFlags['dependency_issue'] ?? false;
|
||||
$bSelected = $aExtraFlags['selected'] ?? true;
|
||||
$bDisabled = $aExtraFlags['disabled'] ?? false;
|
||||
$bRemote = $aExtraFlags['remote'] ?? false;
|
||||
self::AddExtraBadges($aBadges, $bUninstallable, $bMissingFromDisk, $bCannotBeInstalled, $sCode);
|
||||
self::AddExtraBadges($aBadges, $bUninstallable, $bMissingFromDisk, $bDependencyIssue, $sCode);
|
||||
$oBadgeInstalled = BadgeUIBlockFactory::MakeGreen(
|
||||
Dict::S('UI:Layout:ExtensionsDetails:BadgeInstalled'),
|
||||
Dict::S('UI:Layout:ExtensionsDetails:BadgeInstalled+'),
|
||||
@@ -48,7 +48,7 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
|
||||
|
||||
$oExtensionDetails = new ExtensionDetails($sCode, $sLabel, $sDescription, $aMetaData, $aBadges, $sAbout);
|
||||
$oExtensionDetails->GetToggler()->SetIsToggled(true);
|
||||
if ($bMissingFromDisk || $bCannotBeInstalled) {
|
||||
if ($bMissingFromDisk || $bDependencyIssue) {
|
||||
$oExtensionDetails->GetToggler()->SetIsToggled(false);
|
||||
$oExtensionDetails->GetToggler()->SetIsDisabled(true);
|
||||
} elseif ((!$bUninstallable || $bRemote) && !$bDisabled) {
|
||||
@@ -71,10 +71,10 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
|
||||
{
|
||||
$aBadges = [];
|
||||
$bUninstallable = $aExtraFlags['uninstallable'] ?? true;
|
||||
$bCannotBeInstalled = $aExtraFlags['cannot-be-installed'] ?? false;
|
||||
$bDependencyIssue = $aExtraFlags['dependency_issue'] ?? false;
|
||||
$bSelected = $aExtraFlags['selected'] ?? false;
|
||||
$bDisabled = $aExtraFlags['disabled'] ?? false;
|
||||
self::AddExtraBadges($aBadges, $bUninstallable, false, $bCannotBeInstalled, $sCode);
|
||||
self::AddExtraBadges($aBadges, $bUninstallable, false, $bDependencyIssue, $sCode);
|
||||
$oBadgeInstalled = BadgeUIBlockFactory::MakeGrey(
|
||||
Dict::S('UI:Layout:ExtensionsDetails:BadgeNotInstalled'),
|
||||
Dict::S('UI:Layout:ExtensionsDetails:BadgeNotInstalled+'),
|
||||
@@ -91,7 +91,7 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
|
||||
$aBadges[] = $oBadgeToBeUninstalled;
|
||||
$oExtensionDetails = new ExtensionDetails($sCode, $sLabel, $sDescription, $aMetaData, $aBadges, $sAbout);
|
||||
|
||||
if ($bCannotBeInstalled) {
|
||||
if ($bDependencyIssue) {
|
||||
$oExtensionDetails->GetToggler()->SetIsToggled(false);
|
||||
$oExtensionDetails->GetToggler()->SetIsDisabled(true);
|
||||
}
|
||||
@@ -107,7 +107,7 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
|
||||
return $oExtensionDetails;
|
||||
}
|
||||
|
||||
private static function AddExtraBadges(array &$aBadges, bool $bUninstallable, bool $bMissingFromDisk, bool $bCannotBeInstalled, string $sCode)
|
||||
private static function AddExtraBadges(array &$aBadges, bool $bUninstallable, bool $bMissingFromDisk, bool $bDependencyIssue, string $sCode)
|
||||
{
|
||||
if (!$bUninstallable) {
|
||||
$aBadges[] = BadgeUIBlockFactory::MakeYellow(
|
||||
@@ -123,7 +123,7 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
|
||||
self::GetBadgeId($sCode, self::BADGE_ID_MISSING_FROM_DISK)
|
||||
);
|
||||
}
|
||||
if ($bCannotBeInstalled) {
|
||||
if ($bDependencyIssue) {
|
||||
$aBadges[] = BadgeUIBlockFactory::MakeOrange(
|
||||
Dict::S('UI:Layout:ExtensionsDetails:BadgeCannotBeInstalled'),
|
||||
Dict::S('UI:Layout:ExtensionsDetails:BadgeCannotBeInstalled+'),
|
||||
|
||||
@@ -163,6 +163,58 @@ class DataCleanupServiceTest extends \AbstractCleanup
|
||||
$this->AssertSummaryEquals($aExpected, $aRes);
|
||||
}
|
||||
|
||||
/**
|
||||
* N°9831
|
||||
*
|
||||
* A nullable external key configured as DEL_MANUAL must NOT be treated as an issue: it is simply
|
||||
* reset (set to null), exactly like core DBObject::MakeDeletionPlan does. DEL_MANUAL only blocks
|
||||
* for mandatory keys. This mirrors the real "Hypervisor.farm_id -> Farm" case (nullable + DEL_MANUAL).
|
||||
*
|
||||
* Regression guard for the ordering of the IsNullAllowed()/DEL_MANUAL checks in RecursiveDeletion:
|
||||
* reverting that change would make ExecuteCleanup throw here instead of resetting the object.
|
||||
*/
|
||||
public function testExecuteCleanup_NullableManualShouldResetAndNotThrow()
|
||||
{
|
||||
$this->GivenDFRTreeInDB(<<<EOF
|
||||
DFRToRemoveLeaf_1 <- DFRManualNullable_1
|
||||
EOF);
|
||||
|
||||
$aClasses = [ 'DFRToRemoveLeaf' ];
|
||||
$oService = new DataCleanupService();
|
||||
// Must NOT throw: the nullable external key is reset, not reported as an issue
|
||||
$aRes = $oService->ExecuteCleanup($aClasses);
|
||||
|
||||
$aExpected = [
|
||||
['DFRManualNullable', 1, 0 ],
|
||||
['DFRToRemoveLeaf', 0, 1 ],
|
||||
];
|
||||
$this->AssertSummaryEquals($aExpected, $aRes);
|
||||
}
|
||||
|
||||
/**
|
||||
* N°9831
|
||||
*
|
||||
* Summary counterpart of the test above: a nullable DEL_MANUAL external key is reported as an
|
||||
* update (reset), with an issue count of 0. Reverting the RecursiveDeletion change would report
|
||||
* it as an issue instead.
|
||||
*/
|
||||
public function testGetCleanupSummary_NullableManualShouldBeUpdateNotIssue()
|
||||
{
|
||||
$this->GivenDFRTreeInDB(<<<EOF
|
||||
DFRToRemoveLeaf_1 <- DFRManualNullable_1
|
||||
EOF);
|
||||
|
||||
$aClasses = [ 'DFRToRemoveLeaf' ];
|
||||
$oService = new DataCleanupService();
|
||||
$aRes = $oService->GetCleanupSummary($aClasses);
|
||||
|
||||
$aExpected = [
|
||||
['DFRManualNullable', 1, 0, 0 ],
|
||||
['DFRToRemoveLeaf', 0, 1, 0 ],
|
||||
];
|
||||
$this->AssertSummaryEquals($aExpected, $aRes);
|
||||
}
|
||||
|
||||
private function AssertSummaryEquals(array $expected, $actual, $sMessage = '')
|
||||
{
|
||||
$aExpected = [];
|
||||
|
||||
@@ -9,7 +9,6 @@ namespace Combodo\iTop\Test\UnitTest\Module\DataFeatureRemoval;
|
||||
|
||||
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
|
||||
use Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan;
|
||||
use MetaModel;
|
||||
|
||||
require_once __DIR__."/AbstractCleanup.php";
|
||||
class StaticDeletionPlanTest extends \AbstractCleanup
|
||||
@@ -150,6 +149,66 @@ EOF);
|
||||
$this->AssertSummaryEquals($aExpected, $aRes);
|
||||
}
|
||||
|
||||
/**
|
||||
* N°9831
|
||||
*
|
||||
* A DEL_MANUAL issue must still be reported when the referencing class (DFRManual) is itself part
|
||||
* of the classes to remove AND is processed AFTER the class it points to (DFRToRemoveLeaf).
|
||||
*
|
||||
* This mirrors the real "VirtualMachine -> VirtualHost" case: the setup audit sorts the removed
|
||||
* classes alphabetically and drops abstract classes, so a referencing class such as VirtualMachine
|
||||
* ends up processed after its concrete targets (Farm/Hypervisor/Cloud). Here DFRToRemove is abstract
|
||||
* and DFRToRemoveLeaf is its concrete child, while DFRManual points to DFRToRemove with a mandatory
|
||||
* DEL_MANUAL external key.
|
||||
*
|
||||
* Regression: the plan entity accumulating the issue used to be overwritten when the referencing
|
||||
* class was processed as a top-level class, silently dropping the issue (no blocking message on the
|
||||
* summary page, then a crash at execution time).
|
||||
*/
|
||||
public function testGetCleanupSummary_IssueKeptWhenReferencingClassListedAfterTarget(): void
|
||||
{
|
||||
$this->GivenDFRObjectsInDB(<<<EOF
|
||||
create DFRToRemoveLeaf (name = DFRToRemoveLeaf_1)
|
||||
create DFRManual (name = DFRManual_1, extkey_id = DFRToRemoveLeaf_1)
|
||||
EOF);
|
||||
|
||||
// Target listed BEFORE the referencing class, as produced by the alphabetically sorted audit
|
||||
$aClasses = ['DFRToRemoveLeaf', 'DFRManual'];
|
||||
$oService = new StaticDeletionPlan();
|
||||
$aRes = $oService->GetCleanupSummary($aClasses);
|
||||
|
||||
$aExpected = [
|
||||
'DFRToRemoveLeaf' => ['iDeleteCount' => 1],
|
||||
'DFRManual' => ['iDeleteCount' => 1, 'iIssueCount' => 1],
|
||||
];
|
||||
$this->AssertSummaryEquals($aExpected, $aRes);
|
||||
}
|
||||
|
||||
/**
|
||||
* N°9831
|
||||
*
|
||||
* Control case: the same issue must also be reported when the referencing class is listed BEFORE
|
||||
* its target (this order already worked, e.g. "Enclosure -> Rack"). Together with the test above,
|
||||
* this locks in that issue detection is independent of the processing order.
|
||||
*/
|
||||
public function testGetCleanupSummary_IssueKeptWhenReferencingClassListedBeforeTarget(): void
|
||||
{
|
||||
$this->GivenDFRObjectsInDB(<<<EOF
|
||||
create DFRToRemoveLeaf (name = DFRToRemoveLeaf_1)
|
||||
create DFRManual (name = DFRManual_1, extkey_id = DFRToRemoveLeaf_1)
|
||||
EOF);
|
||||
|
||||
$aClasses = ['DFRManual', 'DFRToRemoveLeaf'];
|
||||
$oService = new StaticDeletionPlan();
|
||||
$aRes = $oService->GetCleanupSummary($aClasses);
|
||||
|
||||
$aExpected = [
|
||||
'DFRToRemoveLeaf' => ['iDeleteCount' => 1],
|
||||
'DFRManual' => ['iDeleteCount' => 1, 'iIssueCount' => 1],
|
||||
];
|
||||
$this->AssertSummaryEquals($aExpected, $aRes);
|
||||
}
|
||||
|
||||
public function testCircularRefsShouldNotRunInfinitely()
|
||||
{
|
||||
$this->GivenDFRObjectsInDB(<<<EOF
|
||||
|
||||
@@ -319,6 +319,58 @@
|
||||
</presentation>
|
||||
<parent>cmdbAbstractObject</parent>
|
||||
</class>
|
||||
<class id="DFRManualNullable" _created_in="itop-structure" _delta="define">
|
||||
<properties>
|
||||
<category>bizmodel,searchable</category>
|
||||
<abstract>false</abstract>
|
||||
<db_table>dfrmanualnullable</db_table>
|
||||
<naming>
|
||||
<attributes/>
|
||||
</naming>
|
||||
<reconciliation>
|
||||
<attributes/>
|
||||
</reconciliation>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="name" xsi:type="AttributeString">
|
||||
<sql>name</sql>
|
||||
<default_value/>
|
||||
<is_null_allowed>true</is_null_allowed>
|
||||
<validation_pattern/>
|
||||
<dependencies/>
|
||||
<tracking_level>all</tracking_level>
|
||||
</field>
|
||||
<field id="extkey_id" xsi:type="AttributeExternalKey">
|
||||
<sql>extkey_id</sql>
|
||||
<filter/>
|
||||
<dependencies/>
|
||||
<is_null_allowed>true</is_null_allowed>
|
||||
<target_class>DFRToRemove</target_class>
|
||||
<on_target_delete>DEL_MANUAL</on_target_delete>
|
||||
<tracking_level>all</tracking_level>
|
||||
</field>
|
||||
</fields>
|
||||
<methods/>
|
||||
<presentation>
|
||||
<list>
|
||||
<items/>
|
||||
</list>
|
||||
<search>
|
||||
<items/>
|
||||
</search>
|
||||
<details>
|
||||
<items>
|
||||
<item id="name">
|
||||
<rank>10</rank>
|
||||
</item>
|
||||
<item id="extkey_id">
|
||||
<rank>20</rank>
|
||||
</item>
|
||||
</items>
|
||||
</details>
|
||||
</presentation>
|
||||
<parent>cmdbAbstractObject</parent>
|
||||
</class>
|
||||
<class id="DFRLeafNotToRemove" _created_in="itop-structure" _delta="define">
|
||||
<properties>
|
||||
<category>bizmodel,searchable</category>
|
||||
@@ -718,6 +770,14 @@
|
||||
<entry id="Class:DFRManual/Attribute:name+" _delta="define"><![CDATA[]]></entry>
|
||||
<entry id="Class:DFRManual/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
|
||||
<entry id="Class:DFRManual/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
|
||||
<entry id="Class:DFRManualNullable/Name" _delta="define"><![CDATA[]]></entry>
|
||||
<entry id="Class:DFRManualNullable/ComplementaryName" _delta="define"><![CDATA[]]></entry>
|
||||
<entry id="Class:DFRManualNullable" _delta="define"><![CDATA[DFRManualNullable]]></entry>
|
||||
<entry id="Class:DFRManualNullable+" _delta="define"><![CDATA[]]></entry>
|
||||
<entry id="Class:DFRManualNullable/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
|
||||
<entry id="Class:DFRManualNullable/Attribute:name+" _delta="define"><![CDATA[]]></entry>
|
||||
<entry id="Class:DFRManualNullable/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
|
||||
<entry id="Class:DFRManualNullable/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
|
||||
</entries>
|
||||
</dictionary>
|
||||
</dictionaries>
|
||||
|
||||
@@ -49,6 +49,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => false,
|
||||
'checked' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'A selected but not installed extension should be checked and enabled' => [
|
||||
@@ -70,6 +72,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => false,
|
||||
'checked' => true,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'A missing extension should be disabled and unchecked' => [
|
||||
@@ -89,6 +93,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'A missing extension should always be disabled and unchecked, even when mandatory' => [
|
||||
@@ -108,6 +114,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => true,
|
||||
],
|
||||
],
|
||||
'A missing extension should always be disabled and unchecked, even when non-uninstallable' => [
|
||||
@@ -127,6 +135,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => true,
|
||||
],
|
||||
],
|
||||
'An installed but not selected extension should not be checked and be enabled' => [
|
||||
@@ -148,6 +158,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => false,
|
||||
'checked' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'An installed non uninstallable extension should be checked and disabled' => [
|
||||
@@ -169,6 +181,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'An installed non uninstallable extension should be enabled if the "disable uninstallation check" flag is set' => [
|
||||
@@ -190,6 +204,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => false,
|
||||
'checked' => true,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'A mandatory extension should be checked and disabled' => [
|
||||
@@ -211,6 +227,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => true,
|
||||
],
|
||||
],
|
||||
'A mandatory extension should be checked and disabled even if the "disable uninstallation check" flag is set' => [
|
||||
@@ -232,6 +250,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => true,
|
||||
],
|
||||
],
|
||||
'An optional sub extension should not force its parent flags' => [
|
||||
@@ -265,6 +285,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => false,
|
||||
'checked' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'A mandatory sub extension should force its parent to be checked and disabled' => [
|
||||
@@ -298,6 +320,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'An installed non uninstallable sub extension should force its parent to be checked and disabled' => [
|
||||
@@ -331,6 +355,8 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'A non installed non uninstallable sub extension should not force its parent flags' => [
|
||||
@@ -364,6 +390,62 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => false,
|
||||
'checked' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'A non installed extension with missing dependencies should be not checked and disabled' => [
|
||||
'aExtensionsOnDiskOrDb' => [
|
||||
'itop-ext1' => [
|
||||
'installed' => false,
|
||||
'missing_dependencies' => [
|
||||
'itop-ext1-1',
|
||||
],
|
||||
],
|
||||
],
|
||||
'aWizardStepDefinition' => [
|
||||
'extension_code' => 'itop-ext1',
|
||||
'mandatory' => false,
|
||||
'uninstallable' => true,
|
||||
'missing_dependencies' => true,
|
||||
],
|
||||
'bCurrentSelected' => false,
|
||||
'bDisableUninstallChecks' => false,
|
||||
'aExpectedFlags' => [
|
||||
'uninstallable' => true,
|
||||
'missing' => false,
|
||||
'installed' => false,
|
||||
'disabled' => true,
|
||||
'checked' => false,
|
||||
'dependency_issue' => true,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'An installed extension with missing dependencies should be not checked and disabled' => [
|
||||
'aExtensionsOnDiskOrDb' => [
|
||||
'itop-ext1' => [
|
||||
'installed' => true,
|
||||
'missing_dependencies' => [
|
||||
'itop-ext1-1',
|
||||
],
|
||||
],
|
||||
],
|
||||
'aWizardStepDefinition' => [
|
||||
'extension_code' => 'itop-ext1',
|
||||
'mandatory' => false,
|
||||
'uninstallable' => true,
|
||||
'missing_dependencies' => true,
|
||||
],
|
||||
'bCurrentSelected' => false,
|
||||
'bDisableUninstallChecks' => false,
|
||||
'aExpectedFlags' => [
|
||||
'uninstallable' => true,
|
||||
'missing' => false,
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => false,
|
||||
'dependency_issue' => true,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -79,6 +79,7 @@ class SetupAuditTest extends ItopCustomDatamodelTestCase
|
||||
"Feature1Module1MyClass",
|
||||
"FinalClassFeature2Module1MyClass",
|
||||
"FinalClassFeature2Module1MyFinalClassFromLocation",
|
||||
"TagSetFieldDataFor_FinalClassFeature2Module1MyFinalClassFromLocation__domains",
|
||||
];
|
||||
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetRemovedClasses());
|
||||
|
||||
|
||||
@@ -64,6 +64,12 @@
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
<validation_pattern/>
|
||||
</field>
|
||||
<field id="domains" xsi:type="AttributeTagSet">
|
||||
<sql>domains</sql>
|
||||
<is_null_allowed>true</is_null_allowed>
|
||||
<tracking_level>all</tracking_level>
|
||||
<max_items>12</max_items>
|
||||
</field>
|
||||
</fields>
|
||||
<methods/>
|
||||
<presentation/>
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
<default_value/>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
</field>
|
||||
<field id="domains" xsi:type="AttributeTagSet">
|
||||
<sql>domains</sql>
|
||||
<is_null_allowed>true</is_null_allowed>
|
||||
<tracking_level>all</tracking_level>
|
||||
<max_items>12</max_items>
|
||||
</field>
|
||||
</fields>
|
||||
<methods/>
|
||||
<presentation/>
|
||||
|
||||
@@ -23,6 +23,7 @@ class iTopExtensionsMapFake extends iTopExtensionsMap
|
||||
$oExtension->sVersion = $aExtension['version'] ?? '1.0.0';
|
||||
$oExtension->sSource = $aExtension['source'] ?? iTopExtension::SOURCE_MANUAL;
|
||||
$oExtension->aModuleInfo = $aExtension['module_info'] ?? [];
|
||||
$oExtension->aMissingDependencies = $aExtension['missing_dependencies'] ?? [];
|
||||
$oMap->AddExtension($oExtension);
|
||||
}
|
||||
return $oMap;
|
||||
|
||||
Reference in New Issue
Block a user