mirror of
https://github.com/Combodo/iTop.git
synced 2026-08-27 16:38:19 +02:00
Compare commits
37 Commits
faf/faf-da
...
feature/89
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4c3ab005d | ||
|
|
0c01be9d32 | ||
|
|
899f87f163 | ||
|
|
e0dc1fa571 | ||
|
|
d6466c0bd6 | ||
|
|
eb16e6cbc8 | ||
|
|
42007ffd95 | ||
|
|
6329c157e8 | ||
|
|
9ee0548767 | ||
|
|
620a6b9a51 | ||
|
|
968c56745a | ||
|
|
04a5e5463a | ||
|
|
006dd3d6c2 | ||
|
|
a808e89c30 | ||
|
|
c6e50a0ed7 | ||
|
|
d88687527e | ||
|
|
2cc071b07a | ||
|
|
76a410e00d | ||
|
|
c68887c2bc | ||
|
|
837b433622 | ||
|
|
52fca750b0 | ||
|
|
a5c14e4870 | ||
|
|
0e72b090ee | ||
|
|
d1aedee245 | ||
|
|
1b70b4bf70 | ||
|
|
41f51a2c0c | ||
|
|
73ede13443 | ||
|
|
40dd72ef82 | ||
|
|
c8aea56171 | ||
|
|
041437cbeb | ||
|
|
85d50f31bd | ||
|
|
5715e0484f | ||
|
|
109fdfa06b | ||
|
|
3c301affb3 | ||
|
|
147fae7f8c | ||
|
|
a5aa8c279f | ||
|
|
04f91ab4a2 |
@@ -2,13 +2,17 @@
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'themeVariables': {
|
||||
'git0': 'lawngreen',
|
||||
'git3': 'dodgerblue',
|
||||
'git4': 'grey',
|
||||
'git5': 'grey',
|
||||
'git6': 'grey',
|
||||
'git7': 'grey',
|
||||
'git8': 'grey'
|
||||
'git0': '#7ccf00',
|
||||
'git1': '#7ccf00',
|
||||
'git2': '#99a1af',
|
||||
'git3': '#99a1af',
|
||||
'git4': '#99a1af',
|
||||
'git5': '#99a1af',
|
||||
'git6': '#99a1af',
|
||||
'git7': '#99a1af',
|
||||
'git8': '#99a1af',
|
||||
'git9': '#99a1af',
|
||||
'git10': '#99a1af'
|
||||
}, 'gitGraph': {'showBranches': true,'mainBranchName': 'develop','rotateCommitLabel': true}} }%%
|
||||
gitGraph
|
||||
commit id: "2016-07-06" tag: "2.3.0" type: HIGHLIGHT
|
||||
@@ -106,6 +110,8 @@ gitGraph
|
||||
commit id: "2025-09-25" tag: "2.7.13"
|
||||
checkout support/3.2
|
||||
commit id: "2026-04-27 " tag: "3.2.3"
|
||||
checkout develop
|
||||
commit id: "2026-07-23" tag: "3.3.0-beta1"
|
||||
```
|
||||
|
||||
To learn more, check the [iTop community versions history on the official wiki](https://www.itophub.io/wiki/page?id=latest:release:start).
|
||||
|
||||
@@ -582,7 +582,9 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
*/
|
||||
public function ListProfiles($oUser)
|
||||
{
|
||||
if (count($oUser->ListChanges()) === 0) { // backward compatibility
|
||||
if (!array_key_exists('profile_list', $oUser->ListChanges())) {
|
||||
// Profiles list is not modified on the $oUser object, we can use DBObjectSearch with `all data` capabilities
|
||||
// Note: This is the default behavior
|
||||
$aRet = [];
|
||||
$oSearch = new DBObjectSearch('URP_UserProfile');
|
||||
$oSearch->AllowAllData();
|
||||
@@ -595,6 +597,8 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
|
||||
return $aRet;
|
||||
} else {
|
||||
// Profiles list must be computed with memory changes.
|
||||
// Note: this is a bit tricky because the user object may have been modified in memory (e.g. a profile added or removed) and we need to take that into account
|
||||
$aRet = [];
|
||||
$oProfilesSet = $oUser->Get('profile_list');
|
||||
foreach ($oProfilesSet as $oUserProfile) {
|
||||
|
||||
@@ -1046,7 +1046,7 @@ HTML
|
||||
// Add extra data for markup generation
|
||||
// - Attribute code and AttributeDef. class
|
||||
$val['attcode'] = $sAttCode;
|
||||
$val['atttype'] = $oAttDef->GetType();
|
||||
$val['atttype'] = $oAttDef->GetTypeShortClassName();
|
||||
$val['attlabel'] = $sAttLabel;
|
||||
$val['attflags'] = ($bEditMode) ? $this->GetFormAttributeFlags($sAttCode) : OPT_ATT_READONLY;
|
||||
|
||||
@@ -4507,7 +4507,7 @@ HTML;
|
||||
|
||||
$oDivField = FieldUIBlockFactory::MakeLarge("");
|
||||
// UIContentBlockUIBlockFactory::MakeStandard(null,["field_container field_large"]);
|
||||
$oDivField->AddDataAttribute("attribute-type", $oAttDef->GetType());
|
||||
$oDivField->AddDataAttribute("attribute-type", $oAttDef->GetTypeShortClassName());
|
||||
$oDivField->AddDataAttribute("attribute-label", $sAttMetaDataLabel);
|
||||
$oDivField->AddDataAttribute("attribute-flag-hidden", false);
|
||||
$oDivField->AddDataAttribute("attribute-flag-read-only", false);
|
||||
|
||||
@@ -501,7 +501,7 @@ EOF
|
||||
*/
|
||||
public function Render($oPage, $bEditMode = false, $aExtraParams = [], $bCanEdit = true)
|
||||
{
|
||||
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? null, $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
|
||||
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? $this->GetId(), $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
|
||||
|
||||
/** @var \DashboardLayoutMultiCol $oLayout */
|
||||
$oLayout = new $this->sLayoutClass();
|
||||
|
||||
@@ -730,13 +730,8 @@ abstract class DBObject implements iDisplay
|
||||
public function SetTrim($sAttCode, $sValue)
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
|
||||
$iMaxSize = $oAttDef->GetMaxSize();
|
||||
$sLength = mb_strlen($sValue);
|
||||
if ($iMaxSize && ($sLength > $iMaxSize)) {
|
||||
$sMessage = " -truncated ($sLength chars)";
|
||||
$sValue = mb_substr($sValue, 0, $iMaxSize - mb_strlen($sMessage)).$sMessage;
|
||||
}
|
||||
$this->Set($sAttCode, $sValue);
|
||||
|
||||
$this->Set($sAttCode, $oAttDef->TrimValue($sValue));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2038,7 +2033,7 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
}
|
||||
if (!is_null($iMaxSize = $oAtt->GetMaxSize())) {
|
||||
$iLen = mb_strlen($toCheck);
|
||||
$iLen = $oAtt->GetSize($toCheck);
|
||||
if ($iLen > $iMaxSize) {
|
||||
return "String too long (found $iLen, limited to $iMaxSize)";
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
$ibo-user-rights--padding-x: $ibo-spacing-400 !default;
|
||||
$ibo-user-rights--padding-y: $ibo-spacing-200 !default;
|
||||
$ibo-user-rights--padding-y: $ibo-spacing-100 !default;
|
||||
$ibo-user-rights--border-radius: $ibo-border-radius-400 !default;
|
||||
|
||||
$ibo-user-rights--is-success--background-color: $ibo-color-success-100 !default;
|
||||
@@ -18,6 +18,8 @@ $ibo-user-rights--is-failure--border-color: $ibo-color-danger-500 !default;
|
||||
$ibo-user-rights--is-failure--border: 1px solid $ibo-user-rights--is-failure--border-color !default;
|
||||
|
||||
.ibo-user-rights {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
padding: $ibo-user-rights--padding-y $ibo-user-rights--padding-x;
|
||||
border-radius: $ibo-user-rights--border-radius;
|
||||
&.ibo-is-success {
|
||||
|
||||
@@ -8,70 +8,74 @@
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.help-text{
|
||||
padding: 1px 5px;
|
||||
background-color: #d7e3f8;
|
||||
border: 1px solid #c6e7f5;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.form-error ul{
|
||||
padding: 1px 5px;
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.subform{
|
||||
background-color: #efefef;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.form-buttons{
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.form select{
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form select option{
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.turbo-refreshing{
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.ibo-field legend{
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
collection-entry-element {
|
||||
margin-top: 8px;
|
||||
display: block;
|
||||
padding: 10px 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.ts-control{
|
||||
height: auto;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.ibo-form-actions > .ibo-button > span{
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.ibo-form textarea{
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
// WIP SDK Forms
|
||||
//form[is="itop-form-element"] {
|
||||
//
|
||||
// .help-text{
|
||||
// padding: 1px 5px;
|
||||
// background-color: #d7e3f8;
|
||||
// border: 1px solid #c6e7f5;
|
||||
// border-radius: 5px;
|
||||
// margin: 5px 0;
|
||||
// font-size: 0.9em;
|
||||
// }
|
||||
//
|
||||
// .form-error ul{
|
||||
// padding: 1px 5px;
|
||||
// background-color: #f8d7da;
|
||||
// border: 1px solid #f5c6cb;
|
||||
// border-radius: 5px;
|
||||
// margin: 5px 0;
|
||||
// font-size: 0.9em;
|
||||
// }
|
||||
//
|
||||
// .subform{
|
||||
// background-color: #efefef;
|
||||
// border-radius: 5px;
|
||||
// padding: 10px;
|
||||
// }
|
||||
//
|
||||
// .form-buttons{
|
||||
// margin: 20px 0;
|
||||
// }
|
||||
//
|
||||
// .form select{
|
||||
// padding: 0;
|
||||
// overflow-y: auto;
|
||||
// }
|
||||
//
|
||||
// .form select option{
|
||||
// height: 30px;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// }
|
||||
//
|
||||
// .turbo-refreshing{
|
||||
// opacity: .5;
|
||||
// }
|
||||
//
|
||||
// .ibo-field legend{
|
||||
// margin-top: 24px;
|
||||
// }
|
||||
//
|
||||
// collection-entry-element {
|
||||
// margin-top: 8px;
|
||||
// display: block;
|
||||
// padding: 10px 10px;
|
||||
// background-color: #f5f5f5;
|
||||
// border-radius: 5px;
|
||||
// }
|
||||
// .ts-control{
|
||||
// height: auto;
|
||||
// min-height: 30px;
|
||||
// }
|
||||
//
|
||||
// .ibo-form-actions > .ibo-button > span{
|
||||
// margin-right: 5px;
|
||||
// }
|
||||
//
|
||||
// .ibo-form textarea{
|
||||
// resize: vertical;
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@@ -70,7 +70,7 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
|
||||
}
|
||||
}
|
||||
|
||||
.ck-editor__editable_inline:not(.ck-comment__input *) {
|
||||
.ck-editor__editable_inline:not(.ck-comment__input *), .ck-source-editing-area {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
@@ -84,11 +84,6 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
|
||||
}
|
||||
}
|
||||
|
||||
// N°7552 Allow source editing area to be scrollable in full screen
|
||||
.ck-maximize_editor_main .ck-source-editing-area textarea{
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
/* Mentions */
|
||||
.ck-mentions {
|
||||
.ck-button {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -20,6 +20,7 @@ Dict::Add('EN US', 'English', 'English', [
|
||||
|
||||
'DataFeatureRemoval:Features:Title' => 'Extensions',
|
||||
'DataFeatureRemoval:Result:Title' => 'Modification requested',
|
||||
'DataFeatureRemoval:NoResult:Title' => 'No modification requested',
|
||||
'DataFeatureRemoval:Execution:Title' => 'Deletion Executions',
|
||||
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
|
||||
'DataFeatureRemoval:Analysis:Subtitle' => 'Review all elements requiring attention',
|
||||
@@ -39,6 +40,14 @@ Dict::Add('EN US', 'English', 'English', [
|
||||
'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.',
|
||||
'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.',
|
||||
|
||||
'DataFeatureRemoval:Compile:InProgress' => 'Compilation in progress...',
|
||||
'DataFeatureRemoval:Compile:Success' => 'Compilation successful',
|
||||
'DataFeatureRemoval:Compile:Error' => 'Compilation error',
|
||||
|
||||
'DataFeatureRemoval:RunAudit:InProgress' => 'Analysis in progress...',
|
||||
'DataFeatureRemoval:RunAudit:Success' => 'Analysis completed',
|
||||
'DataFeatureRemoval:RunAudit:Error' => 'Error during analysis',
|
||||
|
||||
'UI:Button:Analyze' => 'Analyze',
|
||||
'UI:Button:ModifyChoices' => 'Change my selection',
|
||||
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
|
||||
|
||||
@@ -20,6 +20,7 @@ Dict::Add('FR FR', 'French', 'Français', [
|
||||
|
||||
'DataFeatureRemoval:Features:Title' => 'Extensions',
|
||||
'DataFeatureRemoval:Result:Title' => 'Modification demandée',
|
||||
'DataFeatureRemoval:NoResult:Title' => 'Aucune modification demandée',
|
||||
'DataFeatureRemoval:Execution:Title' => 'Suppressions',
|
||||
'DataFeatureRemoval:Analysis:Title' => 'Résultat de l’analyse',
|
||||
'DataFeatureRemoval:Analysis:Subtitle' => 'Vérifier les éléments à nettoyer',
|
||||
@@ -39,6 +40,14 @@ Dict::Add('FR FR', 'French', 'Français', [
|
||||
'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.',
|
||||
'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.',
|
||||
|
||||
'DataFeatureRemoval:Compile:InProgress' => 'Compilation en cours...',
|
||||
'DataFeatureRemoval:Compile:Success' => 'Compilation terminée',
|
||||
'DataFeatureRemoval:Compile:Error' => 'Erreur lors de la compilation',
|
||||
|
||||
'DataFeatureRemoval:RunAudit:InProgress' => 'Analyse en cours...',
|
||||
'DataFeatureRemoval:RunAudit:Success' => 'Analyse terminée',
|
||||
'DataFeatureRemoval:RunAudit:Error' => 'Erreur lors de l\'analyse',
|
||||
|
||||
'UI:Button:Analyze' => 'Analyser',
|
||||
'UI:Button:ModifyChoices' => 'Modifier la sélection',
|
||||
'UI:Button:AnalyzeAndSetup' => 'Analyser et ouvrir l’assistant de configuration',
|
||||
|
||||
@@ -37,6 +37,7 @@ class DataFeatureRemovalController extends Controller
|
||||
private array $aCountClassesToCleanup = [];
|
||||
private array $aAnalysisDataTable = [];
|
||||
private array $aDeletionExecutionSummary = [];
|
||||
private ?array $aBasePackageModules = null;
|
||||
|
||||
private int $iCount = 0;
|
||||
private int $iColumnCount = 2;
|
||||
@@ -47,7 +48,11 @@ class DataFeatureRemovalController extends Controller
|
||||
$aParams = [];
|
||||
|
||||
SetupUtils::EraseSetupToken();
|
||||
(new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME))->Erase();
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$oParameters->Erase();
|
||||
Session::Unset('aDeletionExecutionSummary');
|
||||
Session::Set('bForceCompilation', true);
|
||||
$oParameters->SetParameter('return_application', 'DataFeatureRemoval');
|
||||
|
||||
$this->AddAnalyzeParams();
|
||||
$aParams['sTransactionId'] = utils::GetNewTransactionId();
|
||||
@@ -60,7 +65,6 @@ class DataFeatureRemovalController extends Controller
|
||||
$aParams['sSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup';
|
||||
$aParams['iCount'] = $this->iCount;
|
||||
|
||||
Session::Set('bForceCompilation', true);
|
||||
$this->AddLinkedStylesheet(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/css/DataFeatureRemoval.css');
|
||||
$this->AddLinkedScript(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/js/DataFeatureRemoval.js');
|
||||
$this->DisplayPage($aParams);
|
||||
@@ -94,7 +98,7 @@ class DataFeatureRemovalController extends Controller
|
||||
}
|
||||
|
||||
// Display changed extensions
|
||||
$aHiddenInputNames = [
|
||||
$aSetupParameterNames = [
|
||||
'selected_extensions' => '[]',
|
||||
'selected_modules' => '[]',
|
||||
'display_choices' => '',
|
||||
@@ -108,95 +112,38 @@ class DataFeatureRemovalController extends Controller
|
||||
'target_env' => ITOP_DEFAULT_ENV,
|
||||
];
|
||||
|
||||
$aHiddenInputs = [];
|
||||
foreach ($aHiddenInputNames as $sInputName => $defaultValue) {
|
||||
$aHiddenInputs[$sInputName] = utils::ReadPostedParam($sInputName, $defaultValue, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
foreach ($aSetupParameterNames as $sInputName => $defaultValue) {
|
||||
$oParameters->SetParameter($sInputName, $oParameters->GetParameter($sInputName, $defaultValue));
|
||||
}
|
||||
$aParams['aHiddenInputs'] = $aHiddenInputs;
|
||||
|
||||
$aAddedExtensions = json_decode($aHiddenInputs['added_extensions'], true);
|
||||
|
||||
$aRemovedExtensions = json_decode($aHiddenInputs['removed_extensions'], true);
|
||||
if ("[]" === $aHiddenInputs['selected_modules']) {
|
||||
$aAddedExtensions = json_decode($oParameters->GetParameter('added_extensions', '[]'), true);
|
||||
$aRemovedExtensions = json_decode($oParameters->GetParameter('removed_extensions', '[]'), true);
|
||||
if ('[]' === $oParameters->GetParameter('selected_modules', '[]')) {
|
||||
//it does not come from setup
|
||||
// we get extensions from 1st screen uiblocks
|
||||
$this->ReadExtensionsDiff();
|
||||
$aHiddenInputs['force-uninstall'] = $this->bForcedUninstallation ? 'on' : '';
|
||||
$oParameters->SetParameter('force-uninstall', $this->bForcedUninstallation ? 'on' : '');
|
||||
$aAddedExtensions = $this->aExtensionsToCheck['to_be_installed'];
|
||||
$aHiddenInputs['added_extensions'] = $this->ConvertIntoSetupFormat($aAddedExtensions);
|
||||
$oParameters->SetParameter('added_extensions', $this->ConvertIntoSetupFormat($aAddedExtensions));
|
||||
|
||||
$aRemovedExtensions = $this->aExtensionsToCheck['to_be_removed'];
|
||||
$aHiddenInputs['removed_extensions'] = $this->ConvertIntoSetupFormat($aRemovedExtensions);
|
||||
$oParameters->SetParameter('removed_extensions', $this->ConvertIntoSetupFormat($aRemovedExtensions));
|
||||
|
||||
$aExtensionsNotUninstallable = $this->aExtensionsToCheck['extensions_not_uninstallable'];
|
||||
$aHiddenInputs['extensions_not_uninstallable'] = $this->ConvertIntoSetupFormat($aExtensionsNotUninstallable);
|
||||
$oParameters->SetParameter('extensions_not_uninstallable', $this->ConvertIntoSetupFormat($aExtensionsNotUninstallable));
|
||||
}
|
||||
|
||||
$aParams['aAddedExtensions'] = $aAddedExtensions;
|
||||
$aParams['aRemovedExtensions'] = $aRemovedExtensions;
|
||||
|
||||
DataFeatureRemovalLog::Debug(__METHOD__.' Extensions given in parameter', null, [
|
||||
'added_extensions' => $aAddedExtensions,
|
||||
'removed_extensions' => $aRemovedExtensions]);
|
||||
|
||||
$aParams['sTransactionId'] = utils::GetNewTransactionId();
|
||||
$aParams['iColumnCount'] = $this->iColumnCount;
|
||||
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($this->GetExtensionsDiff($aAddedExtensions, $aRemovedExtensions), $this->iColumnCount);
|
||||
|
||||
//to make setup redirection work, we need to pass complex data structures to setup wizards (ie extension/module lists)
|
||||
$sSourceEnv = MetaModel::GetEnvironment();
|
||||
$this->oRuntimeEnvironment = new RunTimeEnvironment($sSourceEnv, false);
|
||||
|
||||
if ('[]' === $aHiddenInputs['selected_modules']) {
|
||||
$oConfig = MetaModel::GetConfig();
|
||||
$aSelectedExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetSelectedExtensions($oConfig, array_keys($aAddedExtensions), array_keys($aRemovedExtensions));
|
||||
$aHiddenInputs['selected_extensions'] = $this->ConvertIntoSetupFormat($aSelectedExtensions);
|
||||
|
||||
$aSelectedModules = []; // keep it to compile method
|
||||
} else {
|
||||
$aSelectedExtensions = json_decode($aHiddenInputs['selected_extensions'], true);
|
||||
$aSelectedModules = json_decode($aHiddenInputs['selected_modules'], true);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->Compile($aSelectedExtensions, array_keys($aRemovedExtensions), $aSelectedModules);
|
||||
$aHiddenInputs['selected_modules'] = $this->ConvertIntoSetupFormat($aSelectedModules);
|
||||
} catch (CoreException $e) {
|
||||
$aParams['DataFeatureRemovalErrorMessage'] = $e->getHtmlDesc();
|
||||
$this->DisplayPage($aParams, 'AnalysisResult');
|
||||
return;
|
||||
} catch (Exception $e) {
|
||||
$aParams['DataFeatureRemovalErrorMessage'] = $e->getMessage();
|
||||
$this->DisplayPage($aParams, 'AnalysisResult');
|
||||
return;
|
||||
}
|
||||
|
||||
$oSetupAudit = new SetupAudit($sSourceEnv);
|
||||
$aGetRemovedClasses = array_keys($oSetupAudit->RunDataAudit());
|
||||
DataFeatureRemovalLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]);
|
||||
|
||||
$aParams['aClasses'] = $aGetRemovedClasses;
|
||||
|
||||
new ContextTag(ContextTag::TAG_SETUP);
|
||||
$aParams['sLaunchSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup/wizard.php';
|
||||
$aParams['aSetupParams'] = [
|
||||
"_class" => "WizStepLandingBeforeAudit",
|
||||
"operation" => "next",
|
||||
];
|
||||
|
||||
foreach ($aHiddenInputs as $sInputName => $sInputValue) {
|
||||
$aParams['aSetupParams'][$sInputName] = $sInputValue;
|
||||
}
|
||||
|
||||
[$aParams['aDeletionPlanSummary'], $aParams['iQueryCount'], $aParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aGetRemovedClasses);
|
||||
[$aParams['aDeletionExecutionSummary'], $aParams['bHasDeletionExecution']] = $this->GetExecutionSummaryTable();
|
||||
$aParams['bDeletionNeeded'] = ($aParams['iQueryCount'] > 0);
|
||||
Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary));
|
||||
|
||||
if (!$aParams['bDeletionNeeded']) {
|
||||
// Erase session setup parameters
|
||||
SetupUtils::CreateSetupToken();
|
||||
}
|
||||
$aAvailableExtensions = $this->GetExtensionsDiff($aAddedExtensions, $aRemovedExtensions);
|
||||
$aParams['aAvailableExtensionsCount'] = count($aAvailableExtensions);
|
||||
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($aAvailableExtensions, $this->iColumnCount);
|
||||
$aParams['sAjaxURL'] = utils::GetAbsoluteUrlModulePage(DataFeatureRemovalHelper::MODULE_NAME, 'index.php');
|
||||
|
||||
$this->DisplayPage($aParams, 'AnalysisResult');
|
||||
}
|
||||
@@ -204,55 +151,124 @@ class DataFeatureRemovalController extends Controller
|
||||
private function ConvertIntoSetupFormat(array $aData): string
|
||||
{
|
||||
$aNewData = [];
|
||||
foreach ($aData as $k => $sVal) {
|
||||
$aNewData[] = sprintf('"%s":"%s"', $k, $sVal);
|
||||
foreach ($aData as $sCode => $sLabel) {
|
||||
$aNewData[] = sprintf('"%s":"%s"', $sCode, $sLabel);
|
||||
}
|
||||
|
||||
return "{".implode(',', $aNewData)."}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aSelectedExtensions
|
||||
* @param array $aRemovedExtensions
|
||||
* @param array $aSelectedModules
|
||||
*
|
||||
* @return void
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
private function Compile(array $aSelectedExtensions, array $aRemovedExtensions, array &$aSelectedModules): void
|
||||
public function OperationAjaxCompile(): void
|
||||
{
|
||||
$aParams = [];
|
||||
//to make setup redirection work, we need to pass complex data structures to setup wizards (ie extension/module lists)
|
||||
$sSourceEnv = MetaModel::GetEnvironment();
|
||||
$sBuildDir = APPROOT."/env-$sSourceEnv-build";
|
||||
if (! is_dir($sBuildDir)) {
|
||||
SetupUtils::builddir($sBuildDir);
|
||||
}
|
||||
$bIsDirEmpty = count(scandir($sBuildDir)) === 2;
|
||||
$bForceCompilation = Session::Get('bForceCompilation', false);
|
||||
$oRuntimeEnvironment = new RunTimeEnvironment($sSourceEnv, false);
|
||||
|
||||
$oConfig = MetaModel::GetConfig();
|
||||
if ($bIsDirEmpty || $bForceCompilation) {
|
||||
Session::Unset('bForceCompilation');
|
||||
$this->oRuntimeEnvironment->CopySetupFiles();
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$aSelectedModules = json_decode($oParameters->GetParameter('selected_modules', '[]'), true);
|
||||
$aSelectedExtensions = json_decode($oParameters->GetParameter('selected_extensions', '[]'), true);
|
||||
$aAddedExtensions = json_decode($oParameters->GetParameter('added_extensions', '[]'), true);
|
||||
$aRemovedExtensions = json_decode($oParameters->GetParameter('removed_extensions', '[]'), true);
|
||||
|
||||
try {
|
||||
$this->ValidateTransactionId();
|
||||
|
||||
$oConfig = MetaModel::GetConfig();
|
||||
if (count($aSelectedModules) === 0) {
|
||||
$aSelectedModules = $this->oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
|
||||
$aSelectedExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetSelectedExtensions($oConfig, array_keys($aAddedExtensions), array_keys($aRemovedExtensions));
|
||||
$oParameters->SetParameter('selected_extensions', $this->ConvertIntoSetupFormat($aSelectedExtensions));
|
||||
}
|
||||
DataFeatureRemovalLog::Debug(
|
||||
__METHOD__,
|
||||
null,
|
||||
['sSourceEnv' => $sSourceEnv, 'sBuildDir' => $sBuildDir, 'bIsDirEmpty' => $bIsDirEmpty, glob("$sBuildDir/*")]
|
||||
);
|
||||
$this->oRuntimeEnvironment->DoCompile($aSelectedExtensions, $aRemovedExtensions, $aSelectedModules, MFCompiler::CanUseSymbolicLinks());
|
||||
} else {
|
||||
if (count($aSelectedModules) === 0) {
|
||||
$aSelectedModules = $this->oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
|
||||
|
||||
$sBuildDir = APPROOT."/env-$sSourceEnv-build";
|
||||
if (! is_dir($sBuildDir)) {
|
||||
SetupUtils::builddir($sBuildDir);
|
||||
}
|
||||
$bIsDirEmpty = count(scandir($sBuildDir)) === 2;
|
||||
$bForceCompilation = Session::Get('bForceCompilation', false);
|
||||
if ($bIsDirEmpty || $bForceCompilation) {
|
||||
$oRuntimeEnvironment->CopySetupFiles();
|
||||
if (count($aSelectedModules) === 0) {
|
||||
$aSelectedModules = $oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
|
||||
}
|
||||
|
||||
DataFeatureRemovalLog::Debug(
|
||||
__METHOD__,
|
||||
null,
|
||||
['sSourceEnv' => $sSourceEnv, 'sBuildDir' => $sBuildDir, 'bIsDirEmpty' => $bIsDirEmpty, glob("$sBuildDir/*")]
|
||||
);
|
||||
$oRuntimeEnvironment->DoCompile($aSelectedExtensions, $aRemovedExtensions, $aSelectedModules, MFCompiler::CanUseSymbolicLinks());
|
||||
Session::Unset('bForceCompilation');
|
||||
} else {
|
||||
if (count($aSelectedModules) === 0) {
|
||||
$aSelectedModules = $oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
|
||||
}
|
||||
}
|
||||
} catch (CoreException $e) {
|
||||
$aParams['error_message'] = $e->getHtmlDesc();
|
||||
} catch (Exception $e) {
|
||||
$aParams['error_message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
$aParams['success_message'] = Dict::S('DataFeatureRemoval:Compile:Success');
|
||||
$aParams['transaction_id'] = utils::GetNewTransactionId();
|
||||
|
||||
$oParameters->SetParameter('selected_modules', json_encode($aSelectedModules));
|
||||
$this->DisplayJSONPage($aParams);
|
||||
}
|
||||
|
||||
public function OperationAjaxRunAudit(): void
|
||||
{
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$aParams = [];
|
||||
$aPageParams = [];
|
||||
try {
|
||||
$this->ValidateTransactionId();
|
||||
|
||||
$sSourceEnv = MetaModel::GetEnvironment();
|
||||
$oSetupAudit = new SetupAudit($sSourceEnv);
|
||||
$aRemovedClasses = array_keys($oSetupAudit->RunDataAudit());
|
||||
$oParameters->SetParameter('classes', $aRemovedClasses);
|
||||
|
||||
new ContextTag(ContextTag::TAG_SETUP);
|
||||
$aPageParams['sLaunchSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup/wizard.php';
|
||||
$aPageParams['aSetupParams'] = [
|
||||
"_class" => "WizStepLandingBeforeAudit",
|
||||
"operation" => "next",
|
||||
];
|
||||
$aPageParams['sTransactionId'] = utils::GetNewTransactionId();
|
||||
|
||||
$aDeletionPlanSummaryEntities = $this->GetDeletionPlanSummaryEntities($aRemovedClasses);
|
||||
[$aPageParams['aDeletionPlanSummary'], $aPageParams['iQueryCount'], $aPageParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aDeletionPlanSummaryEntities);
|
||||
[$aPageParams['aDeletionExecutionSummary'], $aPageParams['bHasDeletionExecution']] = $this->GetExecutionSummaryTable();
|
||||
$aPageParams['bDeletionNeeded'] = ($aPageParams['iQueryCount'] > 0);
|
||||
|
||||
if (!$aPageParams['bDeletionNeeded']) {
|
||||
// Erase session setup parameters
|
||||
SetupUtils::CreateSetupToken();
|
||||
}
|
||||
|
||||
$this->DisplayAjaxPage($aPageParams, 'AjaxRunAudit');
|
||||
return;
|
||||
} catch (CoreException $e) {
|
||||
$aParams['error_message'] = $e->getHtmlDesc();
|
||||
} catch (Exception $e) {
|
||||
$aParams['error_message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
$this->DisplayJSONPage($aParams);
|
||||
}
|
||||
|
||||
private function GetExecutionSummaryTable(): array
|
||||
{
|
||||
$sName = 'ExcutionSummary';
|
||||
$sName = 'ExecutionSummary';
|
||||
|
||||
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
|
||||
|
||||
$aTableData = [];
|
||||
if (count($this->aDeletionExecutionSummary) === 0) {
|
||||
@@ -278,11 +294,15 @@ class DataFeatureRemovalController extends Controller
|
||||
|
||||
}
|
||||
|
||||
private function GetDeletionPlanSummaryTable(array $aRemovedClasses): array
|
||||
private function GetDeletionPlanSummaryEntities(array $aRemovedClasses): array
|
||||
{
|
||||
$oDataCleanupService = new StaticDeletionPlan();
|
||||
return $oDataCleanupService->GetCleanupSummary($aRemovedClasses);
|
||||
}
|
||||
|
||||
private function GetDeletionPlanSummaryTable(array $aDeletionPlanSummaryEntities): array
|
||||
{
|
||||
$sName = 'DeletionPlanSummary';
|
||||
$oDataCleanupService = new StaticDeletionPlan();
|
||||
$aDeletionPlanSummaryEntities = $oDataCleanupService->GetCleanupSummary($aRemovedClasses);
|
||||
$aColumns = ['Class', 'Delete Count' , 'Update Count', 'Issue Count'];
|
||||
$aRows = [];
|
||||
$iQueryCount = 0;
|
||||
@@ -305,9 +325,10 @@ class DataFeatureRemovalController extends Controller
|
||||
{
|
||||
$this->ValidateTransactionId();
|
||||
|
||||
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary'));
|
||||
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
|
||||
Session::Unset('aDeletionExecutionSummary');
|
||||
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$aClasses = $oParameters->GetParameter('classes', []);
|
||||
|
||||
$oDataCleanupService = new DataCleanupService();
|
||||
$aDeletionExecutionSummary = $oDataCleanupService->ExecuteCleanup($aClasses);
|
||||
@@ -322,32 +343,41 @@ class DataFeatureRemovalController extends Controller
|
||||
$oSummary->iTotalUpdateCount += $oExecutionSummary->iUpdateCount;
|
||||
}
|
||||
|
||||
Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary));
|
||||
$this->OperationAnalysisResult();
|
||||
}
|
||||
|
||||
private function GetAvailableExtensions(bool $bIncludePackageExtensions = false): array
|
||||
{
|
||||
$aExtensionsData = [];
|
||||
$oExtensionMap = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap();
|
||||
$aBasePackageModules = $this->GetBasePackageModules();
|
||||
if ($bIncludePackageExtensions) {
|
||||
$aExtensionsRef = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetAllExtensionsWithPreviouslyInstalled();
|
||||
$aExtensionsRef = $oExtensionMap->GetAllExtensionsWithPreviouslyInstalled();
|
||||
} else {
|
||||
$aExtensionsRef = DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions();
|
||||
}
|
||||
|
||||
foreach ($aExtensionsRef as $oExtension) {
|
||||
/** @var \iTopExtension $oExtension */
|
||||
$aMetaData = [$oExtension->sVersion, $oExtension->GetExtensionSourceLabel()];
|
||||
if (SetupUtils::IsIncludedInPackage($oExtensionMap->GetFromExtensionCode($oExtension->sCode), $aBasePackageModules)) {
|
||||
$aMetaData[] = 'Already in package';
|
||||
}
|
||||
|
||||
$aExtensionsData[$oExtension->sCode] = [
|
||||
'version' => $oExtension->sVersion,
|
||||
'label' => $oExtension->sLabel,
|
||||
'code' => $oExtension->sCode,
|
||||
'description' => $oExtension->sDescription,
|
||||
'source' => $oExtension->GetExtensionSourceLabel(),
|
||||
'metadata' => $aMetaData,
|
||||
'installed' => $oExtension->bInstalled,
|
||||
'extra_flags' => [
|
||||
'uninstallable' => $oExtension->CanBeUninstalled(),
|
||||
'remote' => $oExtension->IsRemote(),
|
||||
'missing' => $oExtension->bRemovedFromDisk,
|
||||
'cannot-be-installed' => $oExtension->HasDependencyIssue(),
|
||||
'dependency_issue' => $oExtension->HasDependencyIssue(),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -356,6 +386,26 @@ class DataFeatureRemovalController extends Controller
|
||||
return $aExtensionsData;
|
||||
}
|
||||
|
||||
private function GetBasePackageModules(): array
|
||||
{
|
||||
if ($this->aBasePackageModules !== null) {
|
||||
return $this->aBasePackageModules;
|
||||
}
|
||||
|
||||
try {
|
||||
$oRuntimeEnvironment = new RunTimeEnvironment(MetaModel::GetEnvironment(), false);
|
||||
$aAvailableModules = $oRuntimeEnvironment->AnalyzeInstallation(MetaModel::GetConfig(), [APPROOT]);
|
||||
$this->aBasePackageModules = SetupUtils::GetBasePackageModules($aAvailableModules, APPROOT.'datamodels');
|
||||
|
||||
echo implode(', <br/>', $this->aBasePackageModules);
|
||||
} catch (Exception $e) {
|
||||
DataFeatureRemovalLog::Warning(__METHOD__, null, ['error' => $e->getMessage()]);
|
||||
$this->aBasePackageModules = [];
|
||||
}
|
||||
|
||||
return $this->aBasePackageModules;
|
||||
}
|
||||
|
||||
private function GetExtensionsDiff(array $aAddedExtensions, array $aRemovedExtensions): array
|
||||
{
|
||||
$aExtensions = [];
|
||||
@@ -415,12 +465,11 @@ class DataFeatureRemovalController extends Controller
|
||||
|
||||
/**
|
||||
* Read extensions selected from posted parameters
|
||||
* @return int Number of extensions to be added or removed
|
||||
*/
|
||||
public function ReadExtensionsDiff(): int
|
||||
public function ReadExtensionsDiff(): void
|
||||
{
|
||||
if (!is_null($this->aExtensionsToCheck)) {
|
||||
return count($this->aExtensionsToCheck['to_be_installed']) + count($this->aExtensionsToCheck['to_be_removed']);
|
||||
return;
|
||||
}
|
||||
|
||||
$aAvailableExtensions = $this->GetAvailableExtensions();
|
||||
@@ -442,7 +491,7 @@ class DataFeatureRemovalController extends Controller
|
||||
if (! $this->bForcedUninstallation && $aExtensionData['extra_flags']['uninstallable']) {
|
||||
$this->bForcedUninstallation = true;
|
||||
}
|
||||
if (false === $aExtensionData['extra_flags']['uninstallable']) {
|
||||
if (false === $aExtensionData['extra_flags']['uninstallable'] || true === $aExtensionData['extra_flags']['remote']) {
|
||||
$this->aExtensionsToCheck['extensions_not_uninstallable'][] = $sCode;
|
||||
}
|
||||
} elseif (!$aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] === 'on') {
|
||||
@@ -451,7 +500,6 @@ class DataFeatureRemovalController extends Controller
|
||||
$this->aExtensionsToCheck['to_be_installed'][$sCode] = $sLabel;
|
||||
}
|
||||
}
|
||||
return count($this->aExtensionsToCheck['to_be_installed']) + count($this->aExtensionsToCheck['to_be_removed']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
|
||||
{# @license http://opensource.org/licenses/AGPL-3.0 #}
|
||||
|
||||
{% if bDeletionNeeded %}
|
||||
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:DeletionPlan:Title'|dict_s} %}
|
||||
{% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %}
|
||||
{% EndUIFieldSet %}
|
||||
{% if bDeletionPossible %}
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
{% else %}
|
||||
{% UIAlert ForFailure { sTitle: '', sContent: 'DataFeatureRemoval:DeletionPlan:Error:Issues'|dict_s } %}{% EndUIAlert %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:CleanupComplete:Title'|dict_s, sContent:'DataFeatureRemoval:CompilComplete'|dict_s } %}{% EndUIAlert %}
|
||||
|
||||
{% UIForm Standard {sId:'launch-setup-form', Action:sLaunchSetupUrl, EncType: 'application/x-www-form-urlencoded'} %}
|
||||
{% for sKey, sValue in aSetupParams %}
|
||||
{% UIInput ForHidden { sName:sKey, sValue:sValue } %}
|
||||
{% endfor %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:true} %}
|
||||
{% EndUIForm %}
|
||||
{% endif %}
|
||||
|
||||
{% if bHasDeletionExecution %}
|
||||
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Execution:Title'|dict_s} %}
|
||||
{% UIDataTable ForForm { sRef:'aDeletionExecutionSummary', aColumns:aDeletionExecutionSummary.Columns, aData:aDeletionExecutionSummary.Data} %}{% EndUIDataTable %}
|
||||
{% EndUIFieldSet %}
|
||||
{% endif %}
|
||||
@@ -1,92 +1,50 @@
|
||||
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
|
||||
{# @license http://opensource.org/licenses/AGPL-3.0 #}
|
||||
|
||||
|
||||
|
||||
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:Analysis:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Analysis:Subtitle'|dict_s} %}
|
||||
{% if null != DataFeatureRemovalErrorMessage %}
|
||||
<div id="feature_removal_error_msg_div" style="display:block">
|
||||
{% UIAlert ForFailure { sTitle:'DataFeatureRemoval:Failure:Title'|dict_s, sId: 'feature_removal_error_msg', sContent:DataFeatureRemovalErrorMessage } %}
|
||||
{% EndUIAlert %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% UIAlert ForInformation { sTitle: '', sId: 'ajax_compile_in_progress_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}
|
||||
{{ 'DataFeatureRemoval:Compile:InProgress'|dict_s }}
|
||||
{% UISpinner Standard { } %}
|
||||
{% EndUIAlert %}
|
||||
{% UIAlert ForSuccess { sTitle:'success', sId: 'ajax_compile_success_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
{% UIAlert ForFailure { sTitle:'error', sId: 'ajax_compile_error_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
|
||||
{% UIAlert ForInformation { sTitle: '', sId: 'ajax_run_audit_in_progress_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}
|
||||
{{ 'DataFeatureRemoval:RunAudit:InProgress'|dict_s }}
|
||||
{% UISpinner Standard { } %}
|
||||
{% EndUIAlert %}
|
||||
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:RunAudit:Success'|dict_s, sId: 'ajax_run_audit_success_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
{% UIAlert ForFailure { sTitle:'error', sId: 'ajax_run_audit_error_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
|
||||
{% if aAvailableExtensionsCount == 0 %}
|
||||
{% UITitle Neutral { sTitle:'DataFeatureRemoval:NoResult:Title'|dict_s, iLevel:2 } %}{% EndUITitle %}
|
||||
{% else %}
|
||||
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Result:Title'|dict_s, sSubTitle: '' } %}
|
||||
{% UIMultiColumn Standard {} %}
|
||||
{% for iColumnIndex in 0..iColumnCount-1 %}
|
||||
{% 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['metadata'], 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['metadata'], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% EndUIColumn %}
|
||||
{% endfor %}
|
||||
{% EndUIMultiColumn %}
|
||||
{% EndUIPanel %}
|
||||
{% else %}
|
||||
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Result:Title'|dict_s, sSubTitle: '' } %}
|
||||
{% UIMultiColumn Standard {} %}
|
||||
{% for iColumnIndex in 0..iColumnCount-1 %}
|
||||
{% 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 %}
|
||||
{% else %}
|
||||
{% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% EndUIColumn %}
|
||||
{% endfor %}
|
||||
{% EndUIMultiColumn %}
|
||||
{% EndUIPanel %}
|
||||
|
||||
{% if bDeletionNeeded %}
|
||||
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:DeletionPlan:Title'|dict_s} %}
|
||||
{% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %}
|
||||
{% EndUIFieldSet %}
|
||||
{% if bDeletionPossible %}
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %}
|
||||
{% for sKey, sClass in aClasses %}
|
||||
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
|
||||
{% endfor %}
|
||||
{% for sCode, sLabel in aAddedExtensions %}
|
||||
{% UIInput ForHidden { sName:"aAddedExtensions[" ~ sCode ~ "]", sValue:sLabel } %}
|
||||
{% endfor %}
|
||||
{% for sCode, sLabel in aRemovedExtensions %}
|
||||
{% UIInput ForHidden { sName:"aRemovedExtensions[" ~ sCode ~ "]", sValue:sLabel } %}
|
||||
{% endfor %}
|
||||
{% for sInputName, sValue in aHiddenInputs %}
|
||||
{% UIInput ForHidden { sName:sInputName, sValue:sValue } %}
|
||||
{% endfor %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
{% else %}
|
||||
{% UIAlert ForFailure { sContent: 'DataFeatureRemoval:DeletionPlan:Error:Issues'|dict_s } %}{% EndUIAlert %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:CleanupComplete:Title'|dict_s, sContent:'DataFeatureRemoval:CompilComplete'|dict_s, sId:value } %}{% EndUIAlert %}
|
||||
|
||||
{% UIForm Standard {'sId':'launch-setup-form', Action:sLaunchSetupUrl, 'EncType': 'application/x-www-form-urlencoded'} %}
|
||||
{% for sKey, sValue in aSetupParams %}
|
||||
{% UIInput ForHidden { sName:sKey, sValue:sValue } %}
|
||||
{% endfor %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:true} %}
|
||||
{% EndUIForm %}
|
||||
{% endif %}
|
||||
|
||||
{% if bHasDeletionExecution %}
|
||||
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Execution:Title'|dict_s} %}
|
||||
{% UIDataTable ForForm { sRef:'aDeletionExecutionSummary', aColumns:aDeletionExecutionSummary.Columns, aData:aDeletionExecutionSummary.Data} %}{% EndUIDataTable %}
|
||||
{% EndUIFieldSet %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div id="ajax_run_audit" class="ibo-block"></div>
|
||||
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
|
||||
{# @license http://opensource.org/licenses/AGPL-3.0 #}
|
||||
|
||||
function ajax_run_audit(sTransactionId) {
|
||||
$('#ajax_run_audit_in_progress_msg').removeClass('ibo-is-hidden');
|
||||
$.post(
|
||||
'{{ sAjaxURL|raw }}',
|
||||
{ operation: 'ajax_run_audit', transaction_id: sTransactionId },
|
||||
function (data) {
|
||||
$('#ajax_run_audit_in_progress_msg').addClass('ibo-is-hidden');
|
||||
|
||||
if (data.error_message) {
|
||||
$('#ajax_run_audit_error_msg .ibo-alert--title').html(data.error_message);
|
||||
$('#ajax_run_audit_error_msg').removeClass('ibo-is-hidden');
|
||||
} else {
|
||||
$('#ajax_run_audit_success_msg').removeClass('ibo-is-hidden');
|
||||
$('#ajax_run_audit').html(data);
|
||||
}
|
||||
}
|
||||
)
|
||||
.fail(function() {
|
||||
$('#ajax_run_audit_in_progress_msg').addClass('ibo-is-hidden');
|
||||
$('#ajax_run_audit_error_msg .ibo-alert--title').html('{{ 'DataFeatureRemoval:RunAudit:Error'|dict_s }}');
|
||||
$('#ajax_run_audit_error_msg').removeClass('ibo-is-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
function ajax_compile() {
|
||||
$('#ajax_compile_in_progress_msg').removeClass('ibo-is-hidden');
|
||||
|
||||
$.post(
|
||||
'{{ sAjaxURL|raw }}',
|
||||
{ operation: 'ajax_compile', transaction_id: '{{ sTransactionId }}' },
|
||||
function (data) {
|
||||
$('#ajax_compile_in_progress_msg').addClass('ibo-is-hidden');
|
||||
|
||||
if (data.error_message) {
|
||||
$('#ajax_compile_error_msg .ibo-alert--title').html(data.error_message);
|
||||
$('#ajax_compile_error_msg').removeClass('ibo-is-hidden');
|
||||
} else {
|
||||
$('#ajax_compile_success_msg .ibo-alert--title').html(data.success_message);
|
||||
$('#ajax_compile_success_msg').removeClass('ibo-is-hidden');
|
||||
|
||||
ajax_run_audit(data.transaction_id);
|
||||
}
|
||||
},
|
||||
'json'
|
||||
)
|
||||
.fail(function() {
|
||||
$('#ajax_compile_in_progress_msg').addClass('ibo-is-hidden');
|
||||
$('#ajax_compile_error_msg .ibo-alert--title').html('{{ 'DataFeatureRemoval:Compile:Error'|dict_s }}');
|
||||
$('#ajax_compile_error_msg').removeClass('ibo-is-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
ajax_compile();
|
||||
@@ -5,7 +5,6 @@
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden {sName:'operation', sValue:'AnalysisResult'} %}
|
||||
{% UIInput ForHidden {sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden {sName:'return_application', sValue:'itop'} %}
|
||||
|
||||
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Features:Title'|dict_s, sSubTitle: '' } %}
|
||||
{% UIMultiColumn Standard {} %}
|
||||
@@ -13,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['metadata'], 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['metadata'], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% EndUIColumn %}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -17,4 +17,8 @@ $ipb-vendors-ckeditor--ck-content--text-color: $ipb-color-grey-900 !default;
|
||||
|
||||
.ipb-is-html-content {
|
||||
@extend .ck-content;
|
||||
}
|
||||
|
||||
.ck-source-editing-area {
|
||||
height: 180px;
|
||||
}
|
||||
@@ -689,7 +689,7 @@ class ManageBrickController extends BrickController
|
||||
$sValue = $oAttDef->GetAsHTML($oCurrentRow->Get($sItemAttr));
|
||||
$sSortValue = $oCurrentRow->Get($sItemAttr);
|
||||
}
|
||||
$sAttType = $oAttDef->GetType();
|
||||
$sAttType = $oAttDef->GetTypeShortClassName();
|
||||
unset($oAttDef);
|
||||
|
||||
// For simple fields, we get the raw (stored) value as well
|
||||
|
||||
@@ -1385,7 +1385,7 @@ class ObjectController extends BrickController
|
||||
'object_id' => $oNewLink->GetKey(),
|
||||
'prefix' => 'lnk__',
|
||||
'attribute_code' => $sAttCode,
|
||||
'attribute_type' => $oAttDef->GetType(),
|
||||
'attribute_type' => $oAttDef->GetTypeShortClassName(),
|
||||
'value_html' => $sValue,
|
||||
];
|
||||
|
||||
@@ -1442,7 +1442,7 @@ class ObjectController extends BrickController
|
||||
'object_class' => $sObjectClass,
|
||||
'object_id' => $oObject->GetKey(),
|
||||
'attribute_code' => $oAttDef->GetCode(),
|
||||
'attribute_type' => $oAttDef->GetType(),
|
||||
'attribute_type' => $oAttDef->GetTypeShortClassName(),
|
||||
];
|
||||
|
||||
// - Value raw
|
||||
|
||||
@@ -324,7 +324,7 @@ class BrowseBrickHelper
|
||||
'object_class' => $sCurrentObjectClass,
|
||||
'object_id' => $sCurrentObjectId,
|
||||
'attribute_code' => $sNameAttCode,
|
||||
'attribute_type' => $sNameAttDef->GetType(),
|
||||
'attribute_type' => $sNameAttDef->GetTypeShortClassName(),
|
||||
'value_raw' => $value->Get($sNameAttCode),
|
||||
],
|
||||
];
|
||||
@@ -415,7 +415,7 @@ class BrowseBrickHelper
|
||||
'object_class' => $sCurrentObjectClass,
|
||||
'object_id' => $sCurrentObjectId,
|
||||
'attribute_code' => $aField['code'],
|
||||
'attribute_type' => $oAttDef->GetType(),
|
||||
'attribute_type' => $oAttDef->GetTypeShortClassName(),
|
||||
'value_raw' => $attValueRaw,
|
||||
'value_html' => $sHtmlForFieldValue,
|
||||
];
|
||||
|
||||
2
node_modules/.package-lock.json
generated
vendored
2
node_modules/.package-lock.json
generated
vendored
@@ -104,7 +104,7 @@
|
||||
},
|
||||
"node_modules/ckeditor5-itop-build": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "git+ssh://git@github.com/Combodo/ckeditor5-itop-build.git#21b6edc3348d3f1804e3ae8aab1567ac888a2f30",
|
||||
"resolved": "git+ssh://git@github.com/Combodo/ckeditor5-itop-build.git#8d8e90d315cd8627c85f501835b8d60bc0ee72d5",
|
||||
"license": "SEE LICENSE IN LICENSE.md"
|
||||
},
|
||||
"node_modules/clipboard": {
|
||||
|
||||
7
node_modules/ckeditor5-itop-build/build/styles/compiled-theme.scss
generated
vendored
7
node_modules/ckeditor5-itop-build/build/styles/compiled-theme.scss
generated
vendored
@@ -7525,9 +7525,14 @@ div.ck.ck-balloon-panel.ck-mention-balloon {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.ck-fullscreen__main-wrapper .ck-fullscreen__editable .ck.ck-editor__editable:not(.ck-editor__nested-editable) {
|
||||
.ck-fullscreen__main-wrapper .ck-fullscreen__editable .ck.ck-editor__editable:not(.ck-editor__nested-editable),
|
||||
.ck-fullscreen__main-wrapper .ck-fullscreen__editable .ck-source-editing-area{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ck-source-editing-area textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -141,7 +141,7 @@
|
||||
},
|
||||
"node_modules/ckeditor5-itop-build": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "git+ssh://git@github.com/Combodo/ckeditor5-itop-build.git#21b6edc3348d3f1804e3ae8aab1567ac888a2f30",
|
||||
"resolved": "git+ssh://git@github.com/Combodo/ckeditor5-itop-build.git#8d8e90d315cd8627c85f501835b8d60bc0ee72d5",
|
||||
"license": "SEE LICENSE IN LICENSE.md"
|
||||
},
|
||||
"node_modules/clipboard": {
|
||||
@@ -437,8 +437,8 @@
|
||||
}
|
||||
},
|
||||
"ckeditor5-itop-build": {
|
||||
"version": "git+ssh://git@github.com/Combodo/ckeditor5-itop-build.git#21b6edc3348d3f1804e3ae8aab1567ac888a2f30",
|
||||
"from": "ckeditor5-itop-build@github:Combodo/ckeditor5-itop-build"
|
||||
"version": "git+ssh://git@github.com/Combodo/ckeditor5-itop-build.git#8d8e90d315cd8627c85f501835b8d60bc0ee72d5",
|
||||
"from": "ckeditor5-itop-build@https://github.com/Combodo/ckeditor5-itop-build.git"
|
||||
},
|
||||
"clipboard": {
|
||||
"version": "2.0.11",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -1707,6 +1707,69 @@ JS
|
||||
return array_key_exists('itsm-designer-connector', $aModules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map of module IDs that belong to the package base scope.
|
||||
*
|
||||
* @param array $aAnalyzeInstallationModules Output from AnalyzeInstallation method
|
||||
* @param string $sSourceDir Base directory used as package scope
|
||||
*
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
public static function GetBasePackageModules(array $aAnalyzeInstallationModules, string $sSourceDir): array
|
||||
{
|
||||
$aBasePackageModules = [];
|
||||
$sNormalizedSourceDir = self::NormalizePathForComparison($sSourceDir);
|
||||
if ($sNormalizedSourceDir === '') {
|
||||
return $aBasePackageModules;
|
||||
}
|
||||
|
||||
foreach ($aAnalyzeInstallationModules as $sModuleId => $aModuleInfo) {
|
||||
if ($sModuleId === ROOT_MODULE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sRootDir = $aModuleInfo['root_dir'] ?? '';
|
||||
if ($sRootDir === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sModuleRootDir = self::NormalizePathForComparison($sRootDir);
|
||||
if (utils::StartsWith($sModuleRootDir, $sNormalizedSourceDir)) {
|
||||
$aBasePackageModules[$sModuleId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $aBasePackageModules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when all modules of a non-package extension are already included in base package modules.
|
||||
*/
|
||||
public static function IsIncludedInPackage(?iTopExtension $oExtension, array $aBasePackageModules): bool
|
||||
{
|
||||
if (($oExtension === null) || ($oExtension->sSource === iTopExtension::SOURCE_WIZARD)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$aModules = $oExtension->aModules ?? [];
|
||||
if (!is_array($aModules) || empty($aModules)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($aModules as $sModuleId) {
|
||||
if (!array_key_exists($sModuleId, $aBasePackageModules)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function NormalizePathForComparison(string $sPath): string
|
||||
{
|
||||
return rtrim(str_replace('\\', '/', $sPath), '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aModules List of available module codes
|
||||
*
|
||||
@@ -2226,7 +2289,7 @@ JS
|
||||
$sButtonLabel = '';
|
||||
if ($sReturnApplication !== '') {
|
||||
switch ($sReturnApplication) {
|
||||
case 'itop':
|
||||
case 'DataFeatureRemoval':
|
||||
$sButtonUrl = utils::GetAbsoluteUrlModulePage('combodo-data-feature-removal', 'index.php');
|
||||
$sButtonLabel = 'Back to application';
|
||||
break;
|
||||
|
||||
@@ -32,9 +32,6 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
|
||||
$oWizard->SetParameter('db_tls_ca', $this->oConfig->Get('db_tls.ca') ?? '');
|
||||
$oWizard->SetParameter('display_choices', '');
|
||||
|
||||
$oWizard->SaveParameter('extensions_not_uninstallable', '[]');
|
||||
$oWizard->SaveParameter('use_symbolic_links', MFCompiler::UseSymbolicLinks());
|
||||
$oWizard->SaveParameter('force-uninstall', false);
|
||||
$oWizard->SaveParameter('skip_wizard', false);
|
||||
|
||||
// should be done at the end
|
||||
@@ -64,16 +61,7 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
|
||||
$adModulesFromDatabase = ModuleInstallationRepository::GetInstance()->ReadComputeInstalledModules($oConfig);
|
||||
$this->oWizard->SetParameter('selected_modules', json_encode(array_keys($adModulesFromDatabase)));
|
||||
} else {
|
||||
$this->oWizard->SavePostedParameter('selected_modules');
|
||||
$this->oWizard->SavePostedParameter('selected_extensions');
|
||||
$this->oWizard->SavePostedParameter('added_extensions');
|
||||
$this->oWizard->SavePostedParameter('removed_extensions');
|
||||
$this->oWizard->SavePostedParameter('extensions_not_uninstallable');
|
||||
$this->oWizard->SavePostedParameter('copy_setup_files', '1');
|
||||
$this->oWizard->SavePostedParameter('force-uninstall');
|
||||
$this->oWizard->SavePostedParameter('use_symbolic_links');
|
||||
$this->oWizard->SavePostedParameter('return_application');
|
||||
$this->oWizard->SavePostedParameter('target_env');
|
||||
}
|
||||
|
||||
$aWizardSteps = $this->oWizard->GetParameter('_steps', null);
|
||||
|
||||
@@ -49,6 +49,7 @@ class WizStepModulesChoice extends AbstractWizStepInstall
|
||||
protected bool $bChoicesFromDatabase;
|
||||
|
||||
private array $aAnalyzeInstallationModules = [];
|
||||
private ?array $aBasePackageModules = null;
|
||||
private ?MissingDependencyException $oMissingDependencyException = null;
|
||||
|
||||
public function __construct(WizardController $oWizard, $sCurrentState, bool $bOverWriteConfig = true)
|
||||
@@ -167,6 +168,33 @@ class WizStepModulesChoice extends AbstractWizStepInstall
|
||||
return new WizardState(WizStepModulesChoice::class, (string)($index - 1));
|
||||
}
|
||||
|
||||
|
||||
public function GetAllSelectedModulesUntilNow(): array
|
||||
{
|
||||
$aModules = [];
|
||||
$aSelectedChoices = json_decode($this->oWizard->GetParameter('selected_components', '{}'), true);
|
||||
$iNextStep = $this->GetStepIndex();
|
||||
$index = $iNextStep;
|
||||
while(isset($aSelectedChoices[$iNextStep])){
|
||||
//Let's empty the next steps, we only want what has been chosen before
|
||||
$aSelectedChoices[$iNextStep] = [];
|
||||
$iNextStep++;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $index; $i++) {
|
||||
$aStepInfo = $this->GetStepInfo($i);
|
||||
$this->GetSelectedModules($aStepInfo, $aSelectedChoices[$i], $aModules );
|
||||
}
|
||||
|
||||
/*
|
||||
echo "===GetAllSelectedModulesUntilNow : ===<br/>";
|
||||
echo implode(', <br/>',array_keys($aModules));
|
||||
echo "<br/>=======<br/><hr/>";
|
||||
*/
|
||||
|
||||
return $aModules;
|
||||
}
|
||||
|
||||
public function GetWizardSteps(): array
|
||||
{
|
||||
$aSteps = [
|
||||
@@ -570,6 +598,7 @@ EOF
|
||||
foreach ($this->aAnalyzeInstallationModules as $sModuleId => $aModule) {
|
||||
if (($sModuleId != ROOT_MODULE) && !isset($aModules[$sModuleId])) {
|
||||
if (($aModule['category'] == 'authentication') || (!$aModule['visible'] && !isset($aModule['auto_select']))) {
|
||||
//echo "Set module ".$sModuleId." to selected1<br/>";
|
||||
$aModules[$sModuleId] = true;
|
||||
$sDisplayChoices .= '<li><i>'.$aModule['label'].' (hidden)</i></li>';
|
||||
}
|
||||
@@ -612,6 +641,7 @@ EOF
|
||||
}
|
||||
}
|
||||
if ($bSelected) {
|
||||
//echo "Set module ".$sModuleId." to selected 2<br/>";
|
||||
$aModules[$sModuleId] = true; // store the Id of the selected module
|
||||
SetupInfo::SetSelectedModules($aModules);
|
||||
}
|
||||
@@ -742,28 +772,45 @@ EOF
|
||||
return $this->aSteps[$index] ?? null;
|
||||
}
|
||||
|
||||
public function ComputeChoiceFlags(array $aChoice, string $sChoiceId, array $aSelectedComponents, bool $bAllDisabled, bool $bDisableUninstallCheck, bool $bUpgradeMode)
|
||||
public function ExtensionIsAlreadyIncludedInPreviousChoices(?iTopExtension $oITopExtension): bool
|
||||
{
|
||||
if(is_null($oITopExtension) || empty($oITopExtension->aModules)){
|
||||
return false;
|
||||
}
|
||||
$aAllPreviousChoicesModules = $this->GetAllSelectedModulesUntilNow();
|
||||
|
||||
foreach($oITopExtension->aModules as $sModuleId) {
|
||||
if (!isset($aAllPreviousChoicesModules[$sModuleId])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function ComputeChoiceFlags(array $aChoice, string $sChoiceId, array $aSelectedComponents, bool $bAllDisabled, bool $bDisableUninstallCheck, bool $bUpgradeMode) {
|
||||
$oITopExtension = $this->oExtensionsMap->GetFromExtensionCode($aChoice['extension_code']);
|
||||
//If the extension is missing from disk, it won't exist in the ExtensionsMap, thus returning null
|
||||
$bCanBeUninstalled = isset($aChoice['uninstallable']) ? $aChoice['uninstallable'] === true || $aChoice['uninstallable'] === 'yes' : $oITopExtension->CanBeUninstalled();
|
||||
$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] === $sChoiceId);
|
||||
$bMissingFromDisk = isset($aChoice['missing']) && $aChoice['missing'] === true;
|
||||
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']);
|
||||
$bInstalled = $bMissingFromDisk || $oITopExtension->bInstalled;
|
||||
$bDependencyIssue = $oITopExtension->HasDependencyIssue();
|
||||
$bInstalled = $bMissingFromDisk || $oITopExtension?->bInstalled ?? false;
|
||||
$bDependencyIssue = $oITopExtension?->HasDependencyIssue() ?? false;
|
||||
$bAlreadyIncluded = $this->ExtensionIsAlreadyIncludedInPreviousChoices($oITopExtension);
|
||||
|
||||
$bChecked = $bSelected;
|
||||
$bDisabled = false;
|
||||
if ($bMissingFromDisk) {
|
||||
$bDisabled = true;
|
||||
$bChecked = false;
|
||||
} elseif ($bDependencyIssue) {
|
||||
$bDisabled = true;
|
||||
$bChecked = false;
|
||||
} elseif ($bMandatory) {
|
||||
}elseif($bAlreadyIncluded){
|
||||
$bDisabled = true;
|
||||
$bChecked = true;
|
||||
}elseif ($bMandatory) {
|
||||
$bDisabled = true;
|
||||
$bChecked = true;
|
||||
} elseif ($bDependencyIssue) {
|
||||
$bDisabled = !$bDisableUninstallCheck;
|
||||
} elseif ($bInstalled && !$bCanBeUninstalled && !$bDisableUninstallCheck) {
|
||||
$bChecked = true;
|
||||
$bDisabled = true;
|
||||
@@ -791,6 +838,7 @@ EOF
|
||||
|
||||
return [
|
||||
'uninstallable' => $bCanBeUninstalled,
|
||||
'already_included' => $bAlreadyIncluded,
|
||||
'dependency_issue' => $bDependencyIssue,
|
||||
'mandatory' => $bMandatory,
|
||||
'missing' => $bMissingFromDisk,
|
||||
@@ -813,6 +861,9 @@ EOF
|
||||
|
||||
if ($aFlags['disabled'] && !$aFlags['checked'] && !$bDisableUninstallCheck && (!$aFlags['uninstallable'] || $aFlags['mandatory'])) {
|
||||
$this->bCanMoveForward = false;//Disable "Next"
|
||||
} elseif (!$bDisableUninstallCheck && $aFlags['dependency_issue']) {
|
||||
//If there is a dependency issue, the user cannot move forward without forced uninstall
|
||||
$this->bCanMoveForward = false;
|
||||
}
|
||||
|
||||
$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $sChoiceId, $aFlags);
|
||||
@@ -866,7 +917,7 @@ EOF
|
||||
<a class="setup--wizard-choice--more-info" target="_blank" href="'.$aChoice['more_info'].'">
|
||||
<i class="setup-extension--icon fas fa-external-link-alt" title="More information"></i>
|
||||
</a>' : '';
|
||||
$sDescription = isset($aChoice['description']) ? utils::EscapeHtml($aChoice['description']) : '';
|
||||
$sDescription = isset($aChoice['description']) ? trim(utils::EscapeHtml($aChoice['description'])) : '';
|
||||
$sId = utils::EscapeHtml($aChoice['extension_code']);
|
||||
$sDataId = 'data-id="'.utils::EscapeHtml($aChoice['extension_code']).'"';
|
||||
$sDisabled = $aFlags['disabled'] ? ' disabled data-disabled="disabled"' : '';
|
||||
@@ -891,11 +942,18 @@ EOF
|
||||
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>';
|
||||
}
|
||||
if ($aFlags['already_included']) {
|
||||
if(mb_strlen($sDescription) > 0){
|
||||
$sDescription .= '<br/>';
|
||||
}
|
||||
$sDescription .= '<b>All the modules included in this extension are also included in the package. To uninstall this extension, you have to delete its folder.</b>';
|
||||
}
|
||||
|
||||
$sMetadata = '';
|
||||
if (isset($aChoice['version']) && isset($aChoice['source_label'])) {
|
||||
$sMetadata = '<span>v'.$aChoice['version'].'</span><span>'.$aChoice['source_label'].'</span>';
|
||||
}
|
||||
|
||||
$sChoiceDisabled = $aFlags['disabled'] && !$aFlags['checked'] ? 'choice-disabled' : '';
|
||||
|
||||
$oPage->add('
|
||||
@@ -935,6 +993,7 @@ EOF
|
||||
return $sSourceDir.'/installation.xml';
|
||||
}
|
||||
|
||||
|
||||
public function CanMoveForward()
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -555,7 +555,7 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
|
||||
'object_class' => $sClassName,
|
||||
'class_alias' => $sClassAlias,
|
||||
'attribute_code' => $sAttCode,
|
||||
'attribute_type' => $oAttDef->GetType(),
|
||||
'attribute_type' => $oAttDef->GetTypeShortClassName(),
|
||||
'attribute_label' => $sAttLabel,
|
||||
'render' => $oAttDef->GetRenderForDataTable($sClassAlias),
|
||||
];
|
||||
@@ -737,7 +737,7 @@ JS;
|
||||
'object_class' => $sClassName,
|
||||
'class_alias' => $sClassAlias,
|
||||
'attribute_code' => $sAttCode,
|
||||
'attribute_type' => $oAttDef->GetType(),
|
||||
'attribute_type' => $oAttDef->GetTypeShortClassName(),
|
||||
'attribute_label' => $sAttLabel,
|
||||
];
|
||||
$aColumnDefinition["data"] = $sClassAlias."/".$sAttCode;
|
||||
|
||||
@@ -136,6 +136,12 @@ abstract class AttributeDefinition
|
||||
return Dict::S('Core:'.$oClass->getShortName());
|
||||
}
|
||||
|
||||
public function GetTypeShortClassName()
|
||||
{
|
||||
$oClass = new ReflectionClass(get_class($this));
|
||||
return $oClass->getShortName();
|
||||
}
|
||||
|
||||
public function GetTypeDesc()
|
||||
{
|
||||
$oClass = new ReflectionClass(get_class($this));
|
||||
@@ -894,6 +900,60 @@ abstract class AttributeDefinition
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the size of $value, expressed in the same unit as {@see static::GetMaxSize()} for this attribute class.
|
||||
*
|
||||
* Default unit is a number of **characters**, matching MySQL VARCHAR(M) semantics for VARCHAR-based attributes.
|
||||
* Byte-based attributes (e.g. {@see AttributeText}, stored as MySQL TEXT which is limited to 65535 **bytes**,
|
||||
* not characters) MUST override both this method and {@see TrimValue()} consistently.
|
||||
*
|
||||
* @param string|null $sValue
|
||||
*
|
||||
* @return int Size of $value in the unit of GetMaxSize() (characters by default)
|
||||
* @since 3.2.3-2 3.2.4 3.3.0 N°9759
|
||||
*/
|
||||
public function GetSize(?string $sValue)
|
||||
{
|
||||
// If the value is null, we return 0
|
||||
if ($sValue === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return mb_strlen($sValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to set a value that fits the attribute max size
|
||||
*
|
||||
* Truncation is performed in the same unit as GetMaxSize() / {@see GetSize()}: a number of **characters**
|
||||
* by default (VARCHAR-based attributes). When truncated, a " -truncated (N chars)" suffix is appended and
|
||||
* the returned value (suffix included) still fits within GetMaxSize().
|
||||
*
|
||||
* Default behavior is what DBObject::SetTrim used to do, now delegated to AttributeDefinition
|
||||
*
|
||||
* @param string|null $sValue
|
||||
*
|
||||
* @return string $sValue truncated so that it fits within {@see GetMaxSize()}.
|
||||
* @since 3.2.3-2 3.2.4 3.3.0 N°9759
|
||||
*/
|
||||
public function TrimValue(?string $sValue)
|
||||
{
|
||||
// If the value is null, we return an empty string
|
||||
if ($sValue === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$iMaxSize = $this->GetMaxSize();
|
||||
$iLength = mb_strlen($sValue);
|
||||
if ($iMaxSize && ($iLength > $iMaxSize)) {
|
||||
$sMessage = " -truncated ($iLength chars)";
|
||||
|
||||
return mb_substr($sValue, 0, $iMaxSize - mb_strlen($sMessage)).$sMessage;
|
||||
}
|
||||
|
||||
return $sValue;
|
||||
}
|
||||
|
||||
abstract public function GetDefaultValue(?DBObject $oHostObject = null);
|
||||
|
||||
//
|
||||
@@ -1095,7 +1155,7 @@ abstract class AttributeDefinition
|
||||
|
||||
// Metadata
|
||||
$oFormField->AddMetadata('attribute-code', $this->GetCode());
|
||||
$oFormField->AddMetadata('attribute-type', $this->GetType());
|
||||
$oFormField->AddMetadata('attribute-type', $this->GetTypeShortClassName());
|
||||
$oFormField->AddMetadata('attribute-label', $this->GetLabel());
|
||||
// - Attribute flags
|
||||
$aPossibleAttFlags = MetaModel::EnumPossibleAttributeFlags();
|
||||
|
||||
@@ -58,6 +58,51 @@ class AttributeText extends AttributeString
|
||||
return "TEXT".CMDBSource::GetSqlStringColumnDefinition();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* Unlike the default implementation, the size is expressed in **bytes**: MySQL TEXT columns are limited
|
||||
* in bytes (65535), not in characters, and {@see static::GetMaxSize()} for this class returns a number of bytes.
|
||||
*/
|
||||
public function GetSize(?string $sValue)
|
||||
{
|
||||
// If the value is null, we return 0
|
||||
if ($sValue === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return strlen($sValue);
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* Truncation is performed on a **byte** budget (MySQL TEXT limit) without ever cutting through a multibyte
|
||||
* UTF-8 sequence: the returned value is always valid UTF-8 and never exceeds GetMaxSize() bytes,
|
||||
* truncation suffix included.
|
||||
*/
|
||||
public function TrimValue(?string $sValue)
|
||||
{
|
||||
// If the value is null, we return an empty string
|
||||
if ($sValue === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$iMaxSize = $this->GetMaxSize();
|
||||
$iLength = strlen($sValue);
|
||||
$iLengthChar = mb_strlen($sValue);
|
||||
if ($iMaxSize && ($iLength > $iMaxSize)) {
|
||||
$sMessage = " -truncated ($iLengthChar chars)";
|
||||
$iTruncatedValueMaxSize = $iMaxSize - strlen($sMessage);
|
||||
// mb_strcut cuts on a byte budget but moves the cut point back to a character boundary,
|
||||
// so it never returns a broken multibyte sequence at the end of the value
|
||||
$sTruncatedValue = mb_strcut($sValue, 0, $iTruncatedValueMaxSize, 'UTF-8');
|
||||
|
||||
return $sTruncatedValue.$sMessage;
|
||||
}
|
||||
|
||||
return $sValue;
|
||||
}
|
||||
|
||||
public function GetSQLColumns($bFullSpec = false)
|
||||
{
|
||||
$aColumns = [];
|
||||
|
||||
@@ -852,7 +852,7 @@ JS
|
||||
'object_class' => $sClass,
|
||||
'object_id' => $oItem->GetKey(),
|
||||
'attribute_code' => $sAttCode,
|
||||
'attribute_type' => $oAttDef->GetType(),
|
||||
'attribute_type' => $oAttDef->GetTypeShortClassName(),
|
||||
];
|
||||
// - Value raw
|
||||
// For simple fields, we get the raw (stored) value as well
|
||||
|
||||
@@ -45,8 +45,10 @@ class SessionParameters
|
||||
*/
|
||||
public function SetParameter($sParamCode, $value): void
|
||||
{
|
||||
$this->aParameters[$sParamCode] = $value;
|
||||
$this->Save();
|
||||
if (!array_key_exists($sParamCode, $this->aParameters) || $this->aParameters[$sParamCode] !== $value) {
|
||||
$this->aParameters[$sParamCode] = $value;
|
||||
$this->Save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3002,8 +3002,6 @@ class SynchroExecution
|
||||
* </ul>
|
||||
*/
|
||||
protected $m_oLastFullLoadStartDate = null;
|
||||
/** @var bool true if the caller script gave the datetime before import phase was launched */
|
||||
protected $m_bIsImportPhaseDateKnown;
|
||||
|
||||
/** @var \CMDBChange */
|
||||
protected $m_oChange = null;
|
||||
@@ -3028,10 +3026,7 @@ class SynchroExecution
|
||||
public function __construct($oDataSource, $oImportPhaseStartDate = null)
|
||||
{
|
||||
$this->m_oDataSource = $oDataSource;
|
||||
|
||||
$this->m_bIsImportPhaseDateKnown = ($oImportPhaseStartDate != null);
|
||||
$this->m_oImportPhaseStartDate = $oImportPhaseStartDate;
|
||||
|
||||
$this->m_oCtx = new ContextTag(ContextTag::TAG_SYNCHRO);
|
||||
$this->m_oCtx1 = new ContextTag('Synchro:'.$oDataSource->GetRawName()); // More precise context information
|
||||
}
|
||||
@@ -3108,7 +3103,7 @@ class SynchroExecution
|
||||
$this->m_oStatLog->Set('stats_nb_replica_total', $this->m_iCountAllReplicas);
|
||||
|
||||
$this->m_oStatLog->DBInsert();
|
||||
$sLastFullLoad = ($this->m_bIsImportPhaseDateKnown) ? $this->m_oImportPhaseStartDate->format('Y-m-d H:i:s') : 'not specified';
|
||||
$sLastFullLoad = (is_null($this->m_oImportPhaseStartDate)) ? 'not specified' : $this->m_oImportPhaseStartDate->format('Y-m-d H:i:s');
|
||||
$this->m_oStatLog->AddTrace("###### STARTING SYNCHRONIZATION ##### Total: {$this->m_iCountAllReplicas} replica(s). Last full load: '$sLastFullLoad' ");
|
||||
$sSql = 'SELECT NOW();';
|
||||
$sDBNow = CMDBSource::QueryToScalar($sSql);
|
||||
@@ -3212,21 +3207,18 @@ class SynchroExecution
|
||||
// Compute and keep track of the limit date taken into account for obsoleting replicas
|
||||
//
|
||||
$iFullLoadInterval = $this->m_oDataSource->Get('full_load_periodicity'); // Duration in seconds
|
||||
if ($this->m_bIsImportPhaseDateKnown) {
|
||||
$oLimitDate = clone $this->m_oImportPhaseStartDate;
|
||||
$sInterval = "-$iFullLoadInterval seconds";
|
||||
$oLimitDate->Modify($sInterval);
|
||||
} else {
|
||||
if (is_null($this->m_oImportPhaseStartDate)) {
|
||||
if ($iFullLoadInterval <= 0) {
|
||||
// we are doing exec phase alone, and the full load interval is set to 0 => we should not update/delete replicas !!
|
||||
// This will prevent actions in DoJob1() method
|
||||
$oLimitDate = new DateTime('1970-01-01');
|
||||
} else {
|
||||
$oLimitDate = self::GetDataBaseCurrentDateTime();
|
||||
$sInterval = "-$iFullLoadInterval seconds";
|
||||
$oLimitDate->Modify($sInterval);
|
||||
}
|
||||
} else {
|
||||
$oLimitDate = clone $this->m_oImportPhaseStartDate;
|
||||
}
|
||||
$this->ExactlySubtractSeconds($oLimitDate, $iFullLoadInterval);
|
||||
$this->m_oLastFullLoadStartDate = $oLimitDate;
|
||||
if ($bFirstPass) {
|
||||
$this->m_oStatLog->AddTrace('Limit Date: '.$this->m_oLastFullLoadStartDate->Format('Y-m-d H:i:s'));
|
||||
@@ -3346,10 +3338,10 @@ class SynchroExecution
|
||||
$aArguments['log'] = $this->m_oStatLog->GetKey();
|
||||
$aArguments['change'] = $this->m_oChange->GetKey();
|
||||
$aArguments['chunk'] = $iMaxChunkSize;
|
||||
if ($this->m_bIsImportPhaseDateKnown) {
|
||||
$aArguments['last_full_load'] = $this->m_oImportPhaseStartDate->Format('Y-m-d H:i:s');
|
||||
} else {
|
||||
if (is_null($this->m_oImportPhaseStartDate)) {
|
||||
$aArguments['last_full_load'] = '';
|
||||
} else {
|
||||
$aArguments['last_full_load'] = $this->m_oImportPhaseStartDate->Format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$this->m_oStatLog->DBUpdate();
|
||||
@@ -3701,13 +3693,11 @@ class SynchroExecution
|
||||
|
||||
// Get all the replicas that are to be deleted
|
||||
//
|
||||
$oDeletionDate = $this->m_oLastFullLoadStartDate;
|
||||
$oDeletionDate = clone $this->m_oLastFullLoadStartDate;
|
||||
$iDeleteRetention = $this->m_oDataSource->Get('delete_policy_retention'); // Duration in seconds
|
||||
if ($iDeleteRetention > 0) {
|
||||
$sInterval = "-$iDeleteRetention seconds";
|
||||
$oDeletionDate->Modify($sInterval);
|
||||
}
|
||||
$this->ExactlySubtractSeconds($oDeletionDate, $iDeleteRetention);
|
||||
$sDeletionDate = $oDeletionDate->Format('Y-m-d H:i:s');
|
||||
|
||||
if ($bFirstPass) {
|
||||
$this->m_oStatLog->AddTrace("Deletion date: $sDeletionDate");
|
||||
}
|
||||
@@ -3757,4 +3747,21 @@ class SynchroExecution
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Take into account timechange to apply date difference operation
|
||||
* @param \DateTime $oDate
|
||||
* @param $iDurationInSeconds
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function ExactlySubtractSeconds(DateTime $oDate, $iDurationInSeconds): void
|
||||
{
|
||||
if ($iDurationInSeconds > 0) {
|
||||
$oDate->setTimezone(new DateTimeZone('UTC'));
|
||||
$sInterval = "-$iDurationInSeconds seconds";
|
||||
$oDate->Modify($sInterval);
|
||||
$sTimezone = MetaModel::GetConfig()->Get('timezone');
|
||||
$oDate->setTimezone(new DateTimeZone($sTimezone));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1364,19 +1364,21 @@ class DBObjectTest extends ItopDataTestCase
|
||||
{
|
||||
return [
|
||||
// UserRequest.title is an AttributeString (maxsize = 255)
|
||||
'title 250 chars' => ['title', 250],
|
||||
'title 254 chars' => ['title', 254],
|
||||
'title 255 chars' => ['title', 255],
|
||||
'title 256 chars' => ['title', 256],
|
||||
'title 300 chars' => ['title', 300],
|
||||
'title 250 chars' => ['title', 250, 250, true],
|
||||
'title 254 chars' => ['title', 254, 254, true],
|
||||
'title 255 chars' => ['title', 255, 255, true],
|
||||
'title 256 chars' => ['title', 256, 255, false],
|
||||
'title 300 chars' => ['title', 300, 255, false],
|
||||
|
||||
// UserRequest.pending_reason is an AttributeText (maxsize=65535) with format=text
|
||||
'pending_reason 250 chars' => ['pending_reason', 250],
|
||||
'pending_reason 60000 chars' => ['pending_reason', 60000],
|
||||
'pending_reason 65534 chars' => ['pending_reason', 65534],
|
||||
'pending_reason 65535 chars' => ['pending_reason', 65535],
|
||||
'pending_reason 65536 chars' => ['pending_reason', 65536],
|
||||
'pending_reason 70000 chars' => ['pending_reason', 70000],
|
||||
'pending_reason 250 chars' => ['pending_reason', 250, 250, true],
|
||||
'pending_reason 65534 chars' => ['pending_reason', 65534, 16403, false],
|
||||
'pending_reason 65535 chars' => ['pending_reason', 65535, 16403, false],
|
||||
'pending_reason 65536 chars' => ['pending_reason', 65536, 16403, false],
|
||||
'pending_reason 16385 chars' => ['pending_reason', 16385, 16403, false],
|
||||
'pending_reason 16384 chars' => ['pending_reason', 16384, 16384, true],
|
||||
'pending_reason 16383 chars' => ['pending_reason', 16383, 16383, true],
|
||||
'pending_reason 16382 chars' => ['pending_reason', 16382, 16382, true],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1389,7 +1391,7 @@ class DBObjectTest extends ItopDataTestCase
|
||||
*
|
||||
* @since 3.1.2 N°3448 - Framework field size check not correctly implemented for multi-bytes languages/strings
|
||||
*/
|
||||
public function testCheckLongValueInAttribute(string $sAttrCode, int $iValueLength)
|
||||
public function testCheckLongValueInAttribute(string $sAttrCode, int $iValueLength, int $iExpectedLength, bool $bIsValueToSetBelowAttrMaxSize): void
|
||||
{
|
||||
$sPrefix = 'a'; // just a small prefix so that the emoji bytes won't have a power of 2 (we want a non even value)
|
||||
$sEmojiToRepeat = '😎'; // this emoji is 4 bytes long
|
||||
@@ -1410,17 +1412,18 @@ class DBObjectTest extends ItopDataTestCase
|
||||
|
||||
$oAttDef = MetaModel::GetAttributeDef(UserRequest::class, $sAttrCode);
|
||||
$iAttrMaxSize = $oAttDef->GetMaxSize();
|
||||
$bIsValueToSetBelowAttrMaxSize = ($iValueLength <= $iAttrMaxSize);
|
||||
$bExpectedStatus = ($oAttDef->GetSize($sValueToSet) <= $iAttrMaxSize);
|
||||
$this->assertSame($bExpectedStatus, $bIsValueToSetBelowAttrMaxSize, 'The data provider must stay aligned with the attribute max size logic.');
|
||||
/** @noinspection PhpUnusedLocalVariableInspection */
|
||||
[$bCheckStatus, $aCheckIssues, $bSecurityIssue] = $oTicket->CheckToWrite();
|
||||
$this->assertEquals($bIsValueToSetBelowAttrMaxSize, $bCheckStatus, "CheckResult result:".var_export($aCheckIssues, true));
|
||||
|
||||
$oTicket->SetTrim($sAttrCode, $sValueToSet);
|
||||
$sValueInObject = $oTicket->Get($sAttrCode);
|
||||
$this->assertEquals($iExpectedLength, mb_strlen($sValueInObject), 'Should match expected resulting value length.');
|
||||
if ($bIsValueToSetBelowAttrMaxSize) {
|
||||
$this->assertEquals($sValueToSet, $sValueInObject, 'Should not alter string that is already shorter than attribute max length');
|
||||
} else {
|
||||
$this->assertEquals($iAttrMaxSize, mb_strlen($sValueInObject), 'Should truncate at the same length than attribute max length');
|
||||
$sLastCharsOfValueInObject = mb_substr($sValueInObject, -30);
|
||||
$this->assertStringContainsString(' -truncated', $sLastCharsOfValueInObject, 'Should end with "truncated" comment');
|
||||
}
|
||||
@@ -1453,6 +1456,61 @@ class DBObjectTest extends ItopDataTestCase
|
||||
$this->assertEquals($sResult, $oOrganisation->Get('name'), 'SetTrim must limit string to 255 characters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that DBObject::SetTrim doesn't cut through multibytes characters
|
||||
*
|
||||
* @covers DBObject::SetTrim
|
||||
* @dataProvider SetTrimAttributeTextProvider
|
||||
*/
|
||||
public function testSetTrimOnAttributeTextKeepsUtf8Validity(string $sChar, int $iRepeatCount, bool $bExpectExactByteFill): void
|
||||
{
|
||||
$oTicket = MetaModel::NewObject('UserRequest', [
|
||||
'ref' => 'Test Ticket',
|
||||
'title' => 'Create OK',
|
||||
'description' => 'Create OK',
|
||||
'caller_id' => 15,
|
||||
'org_id' => 3,
|
||||
]);
|
||||
|
||||
$sValueToSet = str_repeat($sChar, $iRepeatCount);
|
||||
$oTicket->SetTrim('pending_reason', $sValueToSet);
|
||||
$sValueInObject = $oTicket->Get('pending_reason');
|
||||
|
||||
$oAttDef = MetaModel::GetAttributeDef('UserRequest', 'pending_reason');
|
||||
$iAttrMaxSize = $oAttDef->GetMaxSize();
|
||||
$iOriginalCharLength = mb_strlen($sValueToSet);
|
||||
$sMessage = " -truncated ($iOriginalCharLength chars)";
|
||||
|
||||
$this->assertStringEndsWith($sMessage, $sValueInObject, 'Trimmed value should keep the expected truncation suffix.');
|
||||
$this->assertTrue(mb_check_encoding($sValueInObject, 'UTF-8'), 'Trimmed value should stay valid UTF-8.');
|
||||
$this->assertLessThanOrEqual($iAttrMaxSize, strlen($sValueInObject), 'Trimmed value should never exceed attribute byte max size.');
|
||||
|
||||
if ($bExpectExactByteFill) {
|
||||
$this->assertSame($iAttrMaxSize, strlen($sValueInObject), 'When byte cut lands on a character boundary, SetTrim should use all available bytes.');
|
||||
}
|
||||
}
|
||||
|
||||
public function SetTrimAttributeTextProvider()
|
||||
{
|
||||
return [
|
||||
// 2-byte UTF-8 chars: truncation payload size is byte-aligned and should fill the max size exactly.
|
||||
'pending_reason 2-byte chars on byte boundary' => ["\xC3\xA9", 32768, true],
|
||||
// 4-byte UTF-8 chars: truncation payload size is not byte-aligned and must backtrack to valid UTF-8.
|
||||
'pending_reason 4-byte chars with mid-character byte cut' => ['💃', 16385, false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers DBObject::SetTrim
|
||||
*/
|
||||
public function testSetTrimOnNonStringAttributeDoesNotTrim()
|
||||
{
|
||||
$oTicket = MetaModel::NewObject(UserRequest::class);
|
||||
$oTicket->SetTrim('caller_id', '15');
|
||||
|
||||
$this->assertEquals(15, $oTicket->Get('caller_id'), 'SetTrim should keep non-string attributes untouched before regular Set conversion');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers DBObject::SetComputedDate
|
||||
* @return void
|
||||
|
||||
@@ -37,6 +37,7 @@ use Dict;
|
||||
use MetaModel;
|
||||
use UserLocal;
|
||||
use UserRights;
|
||||
use UserRightsProfile;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
@@ -155,6 +156,129 @@ class UserRightsTest extends ItopDataTestCase
|
||||
return $oUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure profiles are obtained with persisted user when profiles list has NOT been changed.
|
||||
* @see N°9723 - IsActionAllowed should work with non instantiated users objects
|
||||
* @covers UserRightsProfile::ListProfiles
|
||||
*/
|
||||
public function testListProfilesUsesPersistedProfilesWhenOnlyNonProfileChangesExist(): void
|
||||
{
|
||||
// Create a user with a set of profiles and save it
|
||||
$oUserInDataBase = $this->GivenUserWithProfiles('test1', [
|
||||
self::$aURP_Profiles['Configuration Manager'],
|
||||
self::$aURP_Profiles['Support Agent'],
|
||||
self::$aURP_Profiles['Service Manager'],
|
||||
]);
|
||||
$oUserInDataBase->DBInsert();
|
||||
|
||||
// User to check profiles (give the persisted user id)
|
||||
$oUser = new class ($oUserInDataBase) {
|
||||
private $oUserInDataBase;
|
||||
|
||||
public function __construct($oUserInDataBase)
|
||||
{
|
||||
$this->oUserInDataBase = $oUserInDataBase;
|
||||
}
|
||||
|
||||
public function ListChanges(): array
|
||||
{
|
||||
return ['login' => 'changed'];
|
||||
}
|
||||
|
||||
public function GetKey(): int
|
||||
{
|
||||
return $this->oUserInDataBase->GetKey();
|
||||
}
|
||||
|
||||
public function Get(string $sAttCode)
|
||||
{
|
||||
if ($sAttCode === 'profile_list') {
|
||||
throw new \LogicException('profile_list should not be read when it is not part of ListChanges');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// List the profiles (persisted)
|
||||
$oUserRights = new UserRightsProfile();
|
||||
$aProfiles = $oUserRights->ListProfiles($oUser);
|
||||
|
||||
// Asserts
|
||||
$this->assertCount(3, $aProfiles);
|
||||
$this->assertArrayHasKey(3, $aProfiles);
|
||||
$this->assertArrayHasKey(5, $aProfiles);
|
||||
$this->assertArrayHasKey(10, $aProfiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure profiles are obtained with object in memory when profiles list has been changed.
|
||||
* @see N°9723 - IsActionAllowed should work with non instantiated users objects
|
||||
* @covers UserRightsProfile::ListProfiles
|
||||
*/
|
||||
public function testListProfilesUsesInMemoryProfilesWhenProfileListChanged(): void
|
||||
{
|
||||
// First profile
|
||||
$oUserProfile1 = new class () {
|
||||
public function Get(string $sAttCode)
|
||||
{
|
||||
if ($sAttCode === 'profileid') {
|
||||
return 3;
|
||||
}
|
||||
if ($sAttCode === 'profileid_friendlyname') {
|
||||
return 'Configuration Manager';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Second profile
|
||||
$oUserProfile2 = new class () {
|
||||
public function Get(string $sAttCode)
|
||||
{
|
||||
if ($sAttCode === 'profileid') {
|
||||
return 12;
|
||||
}
|
||||
if ($sAttCode === 'profileid_friendlyname') {
|
||||
return 'Portal power user';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// User in memory with first and second profile
|
||||
$oUser = new class ($oUserProfile1, $oUserProfile2) {
|
||||
private $aProfileList;
|
||||
|
||||
public function __construct($oUserProfile1, $oUserProfile2)
|
||||
{
|
||||
$this->aProfileList = [$oUserProfile1, $oUserProfile2];
|
||||
}
|
||||
|
||||
public function ListChanges(): array
|
||||
{
|
||||
return ['profile_list' => true, 'login' => 'changed'];
|
||||
}
|
||||
|
||||
public function Get(string $sAttCode)
|
||||
{
|
||||
if ($sAttCode === 'profile_list') {
|
||||
return $this->aProfileList;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// List the profiles (memory)
|
||||
$oUserRights = new UserRightsProfile();
|
||||
$aProfiles = $oUserRights->ListProfiles($oUser);
|
||||
|
||||
// Asserts
|
||||
$this->assertSame([
|
||||
3 => 'Configuration Manager',
|
||||
12 => 'Portal power user',
|
||||
], $aProfiles);
|
||||
}
|
||||
|
||||
public function testIsLoggedIn()
|
||||
{
|
||||
$this->assertFalse(UserRights::IsLoggedIn());
|
||||
|
||||
@@ -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,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => false,
|
||||
'checked' => false,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
@@ -72,15 +73,13 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => false,
|
||||
'checked' => true,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'A missing extension should be disabled and unchecked' => [
|
||||
'aExtensionsOnDiskOrDb' => [
|
||||
'itop-ext1' => [
|
||||
'installed' => false,
|
||||
],
|
||||
],
|
||||
'aWizardStepDefinition' => [
|
||||
'extension_code' => 'itop-ext1',
|
||||
@@ -96,15 +95,13 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => false,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'A missing extension should always be disabled and unchecked, even when mandatory' => [
|
||||
'aExtensionsOnDiskOrDb' => [
|
||||
'itop-ext1' => [
|
||||
'installed' => false,
|
||||
],
|
||||
],
|
||||
'aWizardStepDefinition' => [
|
||||
'extension_code' => 'itop-ext1',
|
||||
@@ -120,15 +117,13 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => false,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => true,
|
||||
],
|
||||
],
|
||||
'A missing extension should always be disabled and unchecked, even when non-uninstallable' => [
|
||||
'aExtensionsOnDiskOrDb' => [
|
||||
'itop-ext1' => [
|
||||
'installed' => false,
|
||||
],
|
||||
],
|
||||
'aWizardStepDefinition' => [
|
||||
'extension_code' => 'itop-ext1',
|
||||
@@ -144,6 +139,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => false,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => true,
|
||||
],
|
||||
@@ -167,6 +163,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => false,
|
||||
'checked' => false,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
@@ -190,6 +187,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
@@ -213,6 +211,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => false,
|
||||
'checked' => true,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
@@ -236,6 +235,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => true,
|
||||
],
|
||||
@@ -259,6 +259,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => true,
|
||||
],
|
||||
@@ -294,6 +295,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => false,
|
||||
'checked' => false,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
@@ -329,6 +331,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
@@ -364,6 +367,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
@@ -399,11 +403,12 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => false,
|
||||
'checked' => false,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => false,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'A non installed extension with missing dependencies should be not checked and disabled' => [
|
||||
'A non installed and non mandatory extension with missing dependencies and without force uninstall should be not checked and disabled' => [
|
||||
'aExtensionsOnDiskOrDb' => [
|
||||
'itop-ext1' => [
|
||||
'installed' => false,
|
||||
@@ -426,11 +431,12 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => false,
|
||||
'disabled' => true,
|
||||
'checked' => false,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => true,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'An installed extension with missing dependencies should be not checked and disabled' => [
|
||||
'An installed non mandatory extension with missing dependencies and without force uninstall should be not checked and disabled' => [
|
||||
'aExtensionsOnDiskOrDb' => [
|
||||
'itop-ext1' => [
|
||||
'installed' => true,
|
||||
@@ -453,10 +459,123 @@ class WizStepModulesChoiceTest extends ItopTestCase
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => false,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => true,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'A non installed and non mandatory extension with missing dependencies and force uninstall should be not checked and enabled' => [
|
||||
'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' => true,
|
||||
'aExpectedFlags' => [
|
||||
'uninstallable' => true,
|
||||
'missing' => false,
|
||||
'installed' => false,
|
||||
'disabled' => false,
|
||||
'checked' => false,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => true,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'An installed non mandatory extension with missing dependencies and force uninstall should be not checked and enabled' => [
|
||||
'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' => true,
|
||||
'aExpectedFlags' => [
|
||||
'uninstallable' => true,
|
||||
'missing' => false,
|
||||
'installed' => true,
|
||||
'disabled' => false,
|
||||
'checked' => false,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => true,
|
||||
'mandatory' => false,
|
||||
],
|
||||
],
|
||||
'An installed mandatory extension with missing dependencies and without force uninstall should be checked and disabled' => [
|
||||
'aExtensionsOnDiskOrDb' => [
|
||||
'itop-ext1' => [
|
||||
'installed' => true,
|
||||
'missing_dependencies' => [
|
||||
'itop-ext1-1',
|
||||
],
|
||||
],
|
||||
],
|
||||
'aWizardStepDefinition' => [
|
||||
'extension_code' => 'itop-ext1',
|
||||
'mandatory' => true,
|
||||
'uninstallable' => true,
|
||||
'missing_dependencies' => true,
|
||||
],
|
||||
'bCurrentSelected' => false,
|
||||
'bDisableUninstallChecks' => true,
|
||||
'aExpectedFlags' => [
|
||||
'uninstallable' => true,
|
||||
'missing' => false,
|
||||
'installed' => true,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => true,
|
||||
'mandatory' => true,
|
||||
],
|
||||
],
|
||||
'An non installed mandatory extension with missing dependencies and without force uninstall should be checked and disabled' => [
|
||||
'aExtensionsOnDiskOrDb' => [
|
||||
'itop-ext1' => [
|
||||
'installed' => false,
|
||||
'missing_dependencies' => [
|
||||
'itop-ext1-1',
|
||||
],
|
||||
],
|
||||
],
|
||||
'aWizardStepDefinition' => [
|
||||
'extension_code' => 'itop-ext1',
|
||||
'mandatory' => true,
|
||||
'uninstallable' => true,
|
||||
'missing_dependencies' => true,
|
||||
],
|
||||
'bCurrentSelected' => false,
|
||||
'bDisableUninstallChecks' => true,
|
||||
'aExpectedFlags' => [
|
||||
'uninstallable' => true,
|
||||
'missing' => false,
|
||||
'installed' => false,
|
||||
'disabled' => true,
|
||||
'checked' => true,
|
||||
'already_included' => false,
|
||||
'dependency_issue' => true,
|
||||
'mandatory' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
|
||||
class SynchroExecutionTest extends ItopDataTestCase
|
||||
{
|
||||
private function InitAndGetTZ($sTZ = null): string
|
||||
{
|
||||
$sTZ = $sTZ ?? 'Europe/Paris';
|
||||
MetaModel::GetConfig()->Set('timezone', $sTZ);
|
||||
return $sTZ;
|
||||
}
|
||||
|
||||
public function testGetDateMinusRetentionWithTimeChange()
|
||||
{
|
||||
$sTZ = $this->InitAndGetTZ();
|
||||
$oObj = new SynchroExecution($this->createMock(SynchroDataSource::class));
|
||||
$oDate = DateTime::createFromFormat('Y-m-d H:i:s', "2026-03-29 03:30:01", new DateTimeZone($sTZ));
|
||||
|
||||
$oObj->ExactlySubtractSeconds($oDate, 3600);
|
||||
//03h30 minus 1hours => 03h minus 30mn => 03h
|
||||
// => 03h with timechange => 2h
|
||||
// => 02 minus 30mn => 01h30
|
||||
$this->assertEquals("2026-03-29 01:30:01", $oDate->Format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public function testGetDateMinusRetentioLinuxTimeZero()
|
||||
{
|
||||
$oObj = new SynchroExecution($this->createMock(SynchroDataSource::class));
|
||||
$oDate = new DateTime('1970-01-01');
|
||||
|
||||
$oObj->ExactlySubtractSeconds($oDate, 3600);
|
||||
$this->assertEquals("1969-12-31 23:00:00", $oDate->Format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public function testGetDateMinusRetentionWithTimeChangeAndUTC()
|
||||
{
|
||||
$sTZ = $this->InitAndGetTZ('UTC');
|
||||
$oObj = new SynchroExecution($this->createMock(SynchroDataSource::class));
|
||||
$oDate = DateTime::createFromFormat('Y-m-d H:i:s', "2026-03-29 03:30:01", new DateTimeZone($sTZ));
|
||||
|
||||
$oObj->ExactlySubtractSeconds($oDate, 3600);
|
||||
$this->assertEquals("2026-03-29 02:30:01", $oDate->Format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public function testGetDateMinusRetention()
|
||||
{
|
||||
$sTZ = $this->InitAndGetTZ();
|
||||
$oObj = new SynchroExecution($this->createMock(SynchroDataSource::class));
|
||||
$oDate = DateTime::createFromFormat('Y-m-d H:i:s', "2026-04-01 03:30:01", new DateTimeZone($sTZ));
|
||||
|
||||
$oObj->ExactlySubtractSeconds($oDate, 3600);
|
||||
$this->assertEquals("2026-04-01 02:30:01", $oDate->Format('Y-m-d H:i:s'));
|
||||
}
|
||||
}
|
||||
@@ -277,25 +277,13 @@ abstract class WebServicesBase
|
||||
$oLog->Set('userinfo', UserRights::GetUser());
|
||||
$oLog->Set('verb', $sVerb);
|
||||
$oLog->Set('result', $oRes->IsOk());
|
||||
$this->TrimAndSetValue($oLog, 'log_info', (string)$oRes->GetInfoAsText());
|
||||
$this->TrimAndSetValue($oLog, 'log_warning', (string)$oRes->GetWarningsAsText());
|
||||
$this->TrimAndSetValue($oLog, 'log_error', (string)$oRes->GetErrorsAsText());
|
||||
$this->TrimAndSetValue($oLog, 'data', (string)$oRes->GetReturnedDataAsText());
|
||||
$oLog->SetTrim('log_info', (string)$oRes->GetInfoAsText());
|
||||
$oLog->SetTrim('log_warning', (string)$oRes->GetWarningsAsText());
|
||||
$oLog->SetTrim('log_error', (string)$oRes->GetErrorsAsText());
|
||||
$oLog->SetTrim('data', (string)$oRes->GetReturnedDataAsText());
|
||||
$oLog->DBInsertNoReload();
|
||||
}
|
||||
|
||||
protected function TrimAndSetValue($oLog, $sAttCode, $sValue)
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oLog), $sAttCode);
|
||||
if (is_object($oAttDef)) {
|
||||
$iMaxSize = $oAttDef->GetMaxSize();
|
||||
if ($iMaxSize && (mb_strlen($sValue) > $iMaxSize)) {
|
||||
$sValue = mb_substr($sValue, 0, $iMaxSize);
|
||||
}
|
||||
$oLog->Set($sAttCode, $sValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to set a scalar attribute
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user