Compare commits

..

4 Commits

Author SHA1 Message Date
Timothee
8aac64cc19 N°8864 Added tests 2026-01-05 10:34:42 +01:00
Timothee
f668d94bd9 N°8864 Style fix 2025-12-23 16:20:33 +01:00
Timothee
00e071228e N°8864 List forced uninstall 2025-12-23 16:20:33 +01:00
Timothee
19b7f851f7 N°8864 List extensions added or removed in recap 2025-12-23 16:20:33 +01:00
165 changed files with 5021 additions and 11212 deletions

View File

@@ -16,5 +16,5 @@ require_once(APPROOT.'/application/audit.category.class.inc.php');
require_once(APPROOT.'/application/audit.domain.class.inc.php');
require_once(APPROOT.'/application/audit.rule.class.inc.php');
require_once(APPROOT.'/application/query.class.inc.php');
require_once(APPROOT.'/setup/moduleinstallation/moduleinstallation.class.inc.php');
require_once(APPROOT.'/setup/moduleinstallation.class.inc.php');
require_once(APPROOT.'/application/utils.inc.php');

View File

@@ -24,7 +24,7 @@ MetaModel::IncludeModule('application/user.dashboard.class.inc.php');
MetaModel::IncludeModule('application/audit.rule.class.inc.php');
MetaModel::IncludeModule('application/audit.domain.class.inc.php');
MetaModel::IncludeModule('application/query.class.inc.php');
MetaModel::IncludeModule('setup/moduleinstallation/moduleinstallation.class.inc.php');
MetaModel::IncludeModule('setup/moduleinstallation.class.inc.php');
MetaModel::IncludeModule('core/event.class.inc.php');
MetaModel::IncludeModule('core/action.class.inc.php');

View File

@@ -2685,13 +2685,14 @@ class Config
*
* @param array $aParamValues
* @param ?string $sModulesDir
* @param bool $bPreserveModuleSettings
*
* @return void The current object is modified directly
*
* @throws \Exception
* @throws \CoreException
*/
public function UpdateFromParams($aParamValues, $sModulesDir = null)
public function UpdateFromParams($aParamValues, $sModulesDir = null, $bPreserveModuleSettings = false)
{
if (isset($aParamValues['application_path'])) {
$this->Set('app_root_url', $aParamValues['application_path']);
@@ -2739,10 +2740,7 @@ class Config
} else {
$aSelectedModules = null;
}
if (! is_null($sModulesDir)) {
$this->UpdateIncludes($sModulesDir, $aSelectedModules);
}
$this->UpdateIncludes($sModulesDir, $aSelectedModules);
if (isset($aParamValues['source_dir'])) {
$this->Set('source_dir', $aParamValues['source_dir']);
@@ -2760,13 +2758,17 @@ class Config
*
* @throws Exception
*/
public function UpdateIncludes(string $sModulesDir, $aSelectedModules = null)
public function UpdateIncludes($sModulesDir, $aSelectedModules = null)
{
if ($sModulesDir === null) {
return;
}
// Initialize the arrays below with default values for the application...
$oEmptyConfig = new Config('dummy_file', false); // Do NOT load any config file, just set the default values
$aAddOns = $oEmptyConfig->GetAddOns();
$aModules = ModuleDiscovery::GetModulesOrderedByDependencies([APPROOT.$sModulesDir]);
$aModules = ModuleDiscovery::GetAvailableModules([APPROOT.$sModulesDir]);
foreach ($aModules as $sModuleId => $aModuleInfo) {
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
if (is_null($aSelectedModules) || in_array($sModuleName, $aSelectedModules)) {

View File

@@ -22,8 +22,6 @@ use Combodo\iTop\Application\EventRegister\ApplicationEvents;
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Setup\ModuleDependency\Module;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
require_once APPROOT.'core/modulehandler.class.inc.php';
require_once APPROOT.'core/querymodifier.class.inc.php';
@@ -470,35 +468,11 @@ abstract class MetaModel
* @return string
* @throws \CoreException
*/
final public static function GetModuleName($sClass)
final public static function GetCreatedIn($sClass)
{
try {
$oReflectionClass = new ReflectionClass($sClass);
$sDir = realpath(dirname($oReflectionClass->getFileName()));
$sApproot = realpath(APPROOT);
while (($sDir !== $sApproot) && (str_contains($sDir, $sApproot))) {
$aFiles = glob("$sDir/module.*.php");
if (count($aFiles) > 1) {
return 'core';
}
self::_check_subclass($sClass);
if (count($aFiles) == 0) {
$sDir = realpath(dirname($sDir));
continue;
}
$sModuleFilePath = $aFiles[0];
$aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath);
$sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID];
list($sModuleName, ) = ModuleDiscovery::GetModuleName($sModuleId);
return $sModuleName;
}
} catch (\Exception $e) {
throw new CoreException("Cannot find class module", ['class' => $sClass], '', $e);
}
return 'core';
return self::$m_aClassParams[$sClass]["created_in"] ?? "";
}
/**
@@ -3184,6 +3158,7 @@ abstract class MetaModel
$aMandatParams = [
"category" => "group classes by modules defining their visibility in the UI",
"key_type" => "autoincrement | string",
//"created_in" => "module_name where class is defined",
"name_attcode" => "define which attribute is the class name, may be an array of attributes (format specified in the dictionary as 'Class:myclass/Name' => '%1\$s %2\$s...'",
"state_attcode" => "define which attribute is representing the state (object lifecycle)",
"reconc_keys" => "define the attributes that will 'almost uniquely' identify an object in batch processes",

View File

@@ -22,5 +22,4 @@
@import "medallion-with-blocklist";
@import "field-badge-within-datatable";
@import "jquery-blockui-within-dialog";
@import "jquery-blockui-within-datatable";
@import "badge-with-badge";
@import "jquery-blockui-within-datatable";

View File

@@ -1,10 +0,0 @@
/*
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
$ibo-badge--spacing-left--with-same-block: $ibo-spacing-200 !default;
.ibo-badge + .ibo-badge {
margin-left: $ibo-badge--spacing-left--with-same-block;
}

View File

@@ -33,5 +33,4 @@
@import "field-badge";
@import "file-select";
@import "medallion-icon";
@import "toast";
@import "badge";
@import "toast";

View File

@@ -1,41 +0,0 @@
$ibo-badge--padding-x : $ibo-spacing-200 !default;
$ibo-badge--padding-y : $ibo-spacing-100 !default;
$ibo-badge--border-radius : $ibo-border-radius-400 !default;
$ibo-badge-colors: (
'primary': ($ibo-color-primary-100, $ibo-color-primary-900),
'secondary': ($ibo-color-secondary-100, $ibo-color-secondary-900),
'neutral': ($ibo-color-secondary-100, $ibo-color-secondary-900),
'information': ($ibo-color-information-100, $ibo-color-information-900),
'success': ($ibo-color-success-100, $ibo-color-success-900),
'failure': ($ibo-color-danger-100, $ibo-color-danger-900),
'warning': ($ibo-color-warning-100,$ibo-color-warning-900),
'danger': ($ibo-color-danger-100,$ibo-color-danger-900),
'grey' : ($ibo-color-grey-100, $ibo-color-grey-900),
'blue-grey': ($ibo-color-blue-grey-100, $ibo-color-blue-grey-900),
'blue': ($ibo-color-blue-100, $ibo-color-blue-900),
'cyan': ($ibo-color-cyan-100, $ibo-color-cyan-900),
'green': ($ibo-color-green-100, $ibo-color-green-900),
'orange' : ($ibo-color-orange-100, $ibo-color-orange-900),
'red': ($ibo-color-red-100, $ibo-color-red-900),
'pink': ($ibo-color-pink-100, $ibo-color-pink-900),
) !default;
.ibo-badge {
display: inline-block;
white-space: nowrap;
padding : $ibo-badge--padding-y $ibo-badge--padding-x;
border-radius : $ibo-badge--border-radius;
@extend %ibo-font-ral-med-50;
@each $sColor, $aColorValues in $ibo-badge-colors {
$bg-color: nth($aColorValues, 1);
$text-color: nth($aColorValues, 2);
&.ibo-is-#{$sColor} {
background-color: $bg-color;
color: $text-color;
}
}
}

View File

@@ -8,7 +8,6 @@ $ibo-toggler--wrapper--height: 20px !default;
$ibo-toggler--slider--border-radius: $ibo-border-radius-900 !default;
$ibo-toggler--slider--background-color: $ibo-color-secondary-600 !default;
$ibo-toggler--slider--disabled--background-color: $ibo-color-secondary-200 !default;
$ibo-toggler--slider--before--left: 3px !default;
$ibo-toggler--slider--before--bottom: 3px !default;
@@ -18,7 +17,6 @@ $ibo-toggler--slider--before--border-radius: $ibo-border-radius-full !default;
$ibo-toggler--slider--before--background-color: $ibo-color-grey-100 !default;
$ibo-toggler--slider--checked--background-color: $ibo-color-primary-600 !default;
$ibo-toggler--slider--checked-disabled--background-color: $ibo-color-primary-200 !default;
$ibo-toggler--slider--focus--box-shadow: 0 0 1px $ibo-color-primary-600 !default;
$ibo-toggler--label--margin-left: 4px !default;
@@ -63,13 +61,6 @@ $ibo-toggler--label--margin-left: 4px !default;
background-color: $ibo-toggler--slider--checked--background-color;
}
.ibo-toggler--wrapper input:disabled + .ibo-toggler--slider {
background-color: $ibo-toggler--slider--disabled--background-color;
}
.ibo-toggler--wrapper input:checked:disabled + .ibo-toggler--slider {
background-color: $ibo-toggler--slider--checked-disabled--background-color;
}
input:focus + .ibo-toggler--slider {
box-shadow: $ibo-toggler--slider--focus--box-shadow;
}

View File

@@ -15,4 +15,3 @@
@import "wizard-container/wizard-container";
@import "object/all";
@import "activity-panel/all";
@import "extension/all";

View File

@@ -1 +0,0 @@
@import "extension-details";

View File

@@ -1,54 +0,0 @@
$ibo-extension-details--information--metadata--padding: $ibo-spacing-200 !default;
$ibo-extension-details--information--metadata--delimiter: "-" !default;
$ibo-extension-details--information--metadata--color: $ibo-color-grey-700 !default;
$ibo-extension-details--actions--button--padding-y: 3px !default;
$ibo-extension-details--actions--button--padding-x: $ibo-button--padding-x !default;
.ibo-extension-details {
display: inline-flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
}
.ibo-extension-details--information {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.ibo-extension-details--actions {
display: flex;
}
.ibo-extension-details--information--label {
@extend %ibo-font-ral-med-150;
}
.ibo-extension-details--information--metadata {
@extend %ibo-font-ral-med-100;
color: $ibo-extension-details--information--metadata--color;
}
.ibo-extension-details--information--description {
@extend %ibo-font-ral-med-100;
}
.ibo-extension-details--information--metadata span + span:before {
content: $ibo-extension-details--information--metadata--delimiter;
padding-left: $ibo-extension-details--information--metadata--padding;
padding-right: $ibo-extension-details--information--metadata--padding;
}
.ibo-extension-details:has(input:checked) .ibo-badge.unchecked, .ibo-extension-details:has(input:not(:checked)) .ibo-badge.checked {
display: none;
}
.ibo-extension-details--actions > button {
padding: $ibo-extension-details--actions--button--padding-y $ibo-extension-details--actions--button--padding-x;
}
.ibo-extension-details--actions:has(.toggler-install:not(:disabled)) .ibo-popover-menu--section a[data-resource-id="force_uninstall"] {
display: none;
}

File diff suppressed because one or more lines are too long

View File

@@ -328,10 +328,10 @@ fieldset {
}
}
}
&:not(:checked) ~ label .checked{
&:not(:checked) ~ label .setup-extension-tag.checked{
display:none;
}
&:checked ~ label .unchecked{
&:checked ~ label .setup-extension-tag.unchecked{
display:none;
}
}
@@ -522,12 +522,10 @@ body {
}
}
.ibo-setup-summary-title, .ibo-setup-summary-title:visited, .ibo-setup-summary-title:hover {
font-size: $ibo-font-size-150;
color: inherit;
.ibo-setup-summary-title {
font-size: $ibo-font-size-150;
}
#ibo-setup-licenses--components-list {
background-color: $ibo-color-white-200;
padding: 12px;
@@ -607,7 +605,6 @@ body {
color:#a00000;
}
.setup-extension-tag {
display: inline-flex;
background-color: grey;
border-radius: 8px;
padding-left: 3px;
@@ -684,6 +681,9 @@ body {
overflow: auto;
text-align: center;
}
#installation_progress {
display: none;
}
#fresh_content{
border: 0;
min-height: 300px;

View File

@@ -1,9 +0,0 @@
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/*
* CSS of the template page
*/

View File

@@ -1,9 +0,0 @@
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/*
* Javascript file loaded in template page
*/

View File

@@ -1,17 +0,0 @@
{
"config": {
"classmap-authoritative": true
},
"autoload": {
"psr-4": {
"Combodo\\iTop\\DataFeatureRemoval\\": "src",
"": "src/NoNamespace"
}
},
"name": "combodo/combodo-data-feature-removal",
"type": "itop-extension",
"description": "iTop Data Feature Removal",
"require": {
"composer-runtime-api": "^2.0"
}
}

View File

@@ -1,163 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
<classes>
<class id="DataFeatureRemoverExtension" _delta="define">
<properties>
<category>grant_by_profile</category>
<db_table>data_feature_removal_extension</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes>
<attribute id="extension_code"/>
</attributes>
</reconciliation>
<uniqueness_rules/>
</properties>
<fields>
<field id="extension_code" xsi:type="AttributeString">
<sql>extension_code</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="label" xsi:type="AttributeString">
<sql>label</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="version" xsi:type="AttributeString">
<sql>version</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="module_names" xsi:type="AttributeText">
<sql>module_names</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="status" xsi:type="AttributeString">
<sql>status</sql>
<default_value>none</default_value>
<is_null_allowed>true</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
</fields>
<presentation>
<list>
<items>
<item id="extension_code">
<rank>10</rank>
</item>
<item id="status">
<rank>20</rank>
</item>
</items>
</list>
<search>
<item id="extension_code">
<rank>10</rank>
</item>
<item id="status">
<rank>20</rank>
</item>
</search>
<details>
<item id="extension_code">
<rank>10</rank>
</item>
<item id="status">
<rank>20</rank>
</item>
</details>
</presentation>
<methods/>
</class>
<class id="DataFeatureRemoverAuditRule" _delta="define">
<properties>
<category>grant_by_profile</category>
<db_table>data_feature_removal_auditrule</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes>
<attribute id="rule_name"/>
<attribute id="extension_code"/>
<attribute id="class_name"/>
</attributes>
</reconciliation>
<uniqueness_rules/>
</properties>
<fields>
<field id="rule_name" xsi:type="AttributeString">
<sql>rule_name</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="extension_code" xsi:type="AttributeString">
<sql>extension_code</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="class_name" xsi:type="AttributeText">
<sql>class_name</sql>
<default_value>none</default_value>
<is_null_allowed>true</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="count" xsi:type="AttributeInteger">
<sql>count</sql>
<default_value>0</default_value>
<tracking_level>all</tracking_level>
</field>
</fields>
<presentation>
<list>
<items>
<item id="rule_name">
<rank>10</rank>
</item>
<item id="extension_code">
<rank>20</rank>
</item>
<item id="class_name">
<rank>30</rank>
</item>
</items>
</list>
<search>
<item id="rule_name">
<rank>10</rank>
</item>
<item id="extension_code">
<rank>20</rank>
</item>
<item id="class_name">
<rank>30</rank>
</item>
</search>
<details>
<item id="rule_name">
<rank>10</rank>
</item>
<item id="extension_code">
<rank>20</rank>
</item>
<item id="class_name">
<rank>30</rank>
</item>
</details>
</presentation>
<methods/>
</class>
</classes>
<menus>
<menu id="DataFeatureRemovalMenu" xsi:type="WebPageMenuNode" _delta="define">
<rank>30</rank>
<parent>SystemTools</parent>
<url>$pages/exec.php?exec_module=combodo-data-feature-removal&amp;exec_page=index.php&amp;c[menu]=DataFeatureRemovalMenu</url>
<enable_admin_only>1</enable_admin_only>
</menu>
</menus>
</itop_design>

View File

@@ -1,41 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Localized data
*/
Dict::Add('EN US', 'English', 'English', [
'Menu:DataFeatureRemovalMenu' => 'Features Removal',
'combodo-data-feature-removal/Operation:Main/Title' => 'Features Removal',
'DataFeatureRemoval:Main:Title' => 'Features Removal',
'DataFeatureRemoval:Main:SubTitle' => 'Prepare features you want to enable/disable in a future setup',
'DataFeatureRemoval:Failure:Title' => 'Feature dry removal errors',
'DataFeatureRemoval:Helper:Title' => 'This utilitary allows you to enable or disable features that are installed in your iTop.',
'DataFeatureRemoval:Helper:Desc1' => 'It will prepare the setup step that proceeds to feature enabling or disabling.',
'DataFeatureRemoval:Helper:Desc2' => 'You will need to analyze if there are any data or dependency preventing you from enabling/disabling a feature.',
'DataFeatureRemoval:Features:Title' => 'Features',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove',
'DataFeatureRemoval:Table:Analysis:RemovalType' => 'Type of element',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name',
'DataFeatureRemoval:Table:Analysis:Occurence' => 'Occurence',
'UI:Button:Analyze' => 'Analyze',
'UI:Button:ModifyChoices' => 'Modify Choices',
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
'UI:Action:ForceUninstall' => 'Force uninstall',
'UI:Action:MoreInfo' => 'More information',
'DataFeatureRemoval:Table:Analysis:RemovalType:FINAL_CLASS' => 'Final class',
]);

View File

@@ -1,40 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Localized data
*/
Dict::Add('FR FR', 'French', 'Français', [
'Menu:DataFeatureRemovalMenu' => 'Features Removal',
'combodo-data-feature-removal/Operation:Main/Title' => 'Features Removal',
'DataFeatureRemoval:Main:Title' => 'Features Removal',
'DataFeatureRemoval:Main:SubTitle' => 'Prepare features you want to enable/disable in a future setup',
'DataFeatureRemoval:Failure:Title' => 'Feature dry removal errors',
'DataFeatureRemoval:Helper:Title' => 'This utilitary allows you to enable or disable features that are installed in your iTop.',
'DataFeatureRemoval:Helper:Desc1' => 'It will prepare the setup step that proceeds to feature enabling or disabling.',
'DataFeatureRemoval:Helper:Desc2' => 'You will need to analyze if there are any data or dependency preventing you from enabling/disabling a feature.',
'DataFeatureRemoval:Features:Title' => 'Features',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove',
'DataFeatureRemoval:Table:Analysis:RemovalType' => 'Type of element',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name',
'DataFeatureRemoval:Table:Analysis:Occurence' => 'Occurence',
'UI:Button:Analyze' => 'Analyze',
'UI:Button:ModifyChoices' => 'Modify Choices',
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
'UI:Action:ForceUninstall' => 'Force uninstall',
'UI:Action:MoreInfo' => 'More information',
'DataFeatureRemoval:Table:Analysis:RemovalType:FINAL_CLASS' => 'Final class',
]);

View File

@@ -1,20 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval;
use Combodo\iTop\DataFeatureRemoval\Controller\DataFeatureRemovalController;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
require_once(APPROOT.'application/startup.inc.php');
DataFeatureRemovalLog::Enable();
$oController = new DataFeatureRemovalController(MODULESROOT.DataFeatureRemovalHelper::MODULE_NAME.'/templates', DataFeatureRemovalHelper::MODULE_NAME);
$oController->SetDefaultOperation('Main');
$oController->HandleOperation();

View File

@@ -1,16 +0,0 @@
<?php
// PHP Data Model definition file
// WARNING - WARNING - WARNING
// DO NOT EDIT THIS FILE (unless you know what you are doing)
//
// If you provide a datamodel.xxxx.xml file with your module,
// this file WILL BE overwritten by the compilation of the
// module (during the setup) if the datamodel.xxxx.xml file
// contains the definition of new classes or menus.
//
// The recommended way to define new classes (for iTop 2.0 and later) is via the XML definition.
// This file remains in the module's template only for the cases where there is:
// - either no new class or menu defined in the XML file
// - or no XML file at all supplied by the module

View File

@@ -1,54 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
//
// iTop module definition file
//
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'combodo-data-feature-removal/3.3.0',
[
// Identification
//
'label' => 'iTop Data Feature Removal',
'category' => 'business',
// Setup
//
'dependencies' => [
],
'mandatory' => true,
'visible' => false,
// Components
//
'datamodel' => [
'vendor/autoload.php',
'model.combodo-data-feature-removal.php', // Contains the PHP code generated by the "compilation" of datamodel.combodo-data-feature-removal.xml
],
'webservice' => [],
'data.struct' => [
// add your 'structure' definition XML files here,
],
'data.sample' => [
// add your sample data XML files here,
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
'doc.more_information' => '', // hyperlink to more information, if any
// Default settings
//
'settings' => [
// Module specific settings go here, if any
],
]
);

View File

@@ -1,189 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Controller;
require_once APPROOT.'setup/feature_removal/SetupAudit.php';
require_once APPROOT.'setup/feature_removal/DryRemovalRuntimeEnvironment.php';
use Combodo\iTop\Application\TwigBase\Controller\Controller;
use Combodo\iTop\AuthentToken\Helper\TokenAuthLog;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Model\DataFeatureRemoverAuditRuleService;
use Combodo\iTop\DataFeatureRemoval\Model\DataFeatureRemoverExtensionService;
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
use Dict;
use Exception;
use MetaModel;
use utils;
class DataFeatureRemovalController extends Controller
{
private array $aSelectedExtensionsForCheck = [];
public function OperationMain($sErrorMessage = null)
{
$aParams = [];
$this->AddLinkedStylesheet(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/css/DataFeatureRemoval.css');
$this->AddLinkedScript(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/js/DataFeatureRemoval.js');
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$this->AddFeatureParams($aParams);
$this->AddAnalyzeParams($aParams);
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
$this->DisplayPage($aParams);
}
public function AddFeatureParams(array &$aParams)
{
$aParams['aExtensions'] = $this->GetExtensionsTable();
$aParams['sModule'] = DataFeatureRemovalHelper::MODULE_NAME;
}
public function AddAnalyzeParams(array &$aParams)
{
$iTotalCount = 0;
$aData = [];
$aColumns = [];
foreach (DataFeatureRemoverAuditRuleService::GetInstance()->ReadCheckRules() as $oRule) {
$sContent = $oRule->Get('class_name');
$sModuleName = MetaModel::GetModuleName($sContent);
$aExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetIncludingExtensions($sModuleName);
$sExtensions = implode(' ', $aExtensions);
$sTypeName = $oRule->Get('rule_name');
$sTypeDesc = \Dict::S("DataFeatureRemoval:Table:Analysis:RemovalType:$sTypeName");
$iCount = $oRule->Get('count');
$iTotalCount += $iCount;
$aColumns = ['ClassName', 'RemovalType','FeatureName','Occurence'];
$aData[] = [
<<<HTML
<label>$sContent</label>
HTML,
<<<HTML
<label>$sTypeDesc</label>
HTML,
<<<HTML
<label title="$sModuleName">$sExtensions</label>
HTML,
<<<HTML
<label>$iCount</label>
HTML,
];
}
$aParams['aCheckRules'] = $this->GetTableData('Analysis', $aColumns, $aData);
$aParams['rule_count'] = $iTotalCount;
}
public function OperationAnalyze()
{
$aSelectedExtensionsFromUI = utils::ReadPostedParam('aExtensions', []);
$this->aSelectedExtensionsForCheck = [];
foreach ($aSelectedExtensionsFromUI as $sCode => $aData) {
$sValue = $aData['enable'] ?? 'off';
if (($sValue) === 'on') {
$this->aSelectedExtensionsForCheck[] = $sCode;
}
}
$this->m_sOperation = 'Main';
try {
$this->Analyze();
$this->OperationMain();
} catch (Exception $e) {
\IssueLog::Error(__METHOD__, null, ['stack' => $e->getTraceAsString(), 'exception' => $e->getMessage()]);
$this->OperationMain($e->getMessage());
}
}
private function GetExtensionsTable(): array
{
$aExtensions = [];
$aColumns = ['', 'Version', 'Name', 'Code'];
$this->aSelectedExtensionsForCheck = DataFeatureRemoverExtensionService::GetInstance()->ReadAuditedExtensions();
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
/** @var \iTopExtension $oExtension */
$sChecked = "checked";
$sDisabledHtml = '';
if ($oExtension->bRemovedFromDisk) {
$sDisabledHtml = 'disabled=""';
} elseif (! array_key_exists($sCode, $this->aSelectedExtensionsForCheck)) {
$sChecked = "";
}
$sLabel = $oExtension->sLabel;
$sVersion = $oExtension->sVersion;
$sIdEnable = "aExtensions[$sCode][enable]";
$aExtensions[] = [
<<<HTML
<input type="checkbox" $sDisabledHtml class="extension_check" $sChecked id="$sIdEnable" name="$sIdEnable"/>
HTML,
<<<HTML
<label>$sVersion</label>
HTML,
<<<HTML
<label for="$sIdEnable">$sLabel</label>
HTML,
<<<HTML
<label for="$sIdEnable">$sCode</label>
HTML,
];
}
return $this->GetTableData('Extensions', $aColumns, $aExtensions);
}
public function GetTableData(string $sTableName, array $aColumns, array $aData): array
{
if (empty($aData)) {
return [
'Type' => 'Table',
'Columns' => [['label' => '']],
'Data' => [[ Dict::S('DbCleaner:Table:Empty')]],
];
}
$aNewColumns = [];
foreach ($aColumns as $sColumn) {
$aNewColumns[] = ['label' => Dict::S("DataFeatureRemoval:Table:$sTableName:$sColumn", $sColumn)];
}
$aColumns = $aNewColumns;
return [
'Type' => 'Table',
'Columns' => $aColumns,
'Data' => $aData,
];
}
private function Analyze()
{
DataFeatureRemoverExtensionService::GetInstance()->SaveExtensions($this->aSelectedExtensionsForCheck);
$sSourceEnvt = \MetaModel::GetEnvironment();
$oDryRemovalRuntimeEnvironment = new DryRemovalRuntimeEnvironment();
$oDryRemovalRuntimeEnvironment->Prepare($sSourceEnvt, $this->aSelectedExtensionsForCheck);
$oDryRemovalRuntimeEnvironment->CompileFrom($sSourceEnvt);
$oSetupAudit = new SetupAudit($sSourceEnvt, DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV);
$this->Save($oSetupAudit->GetIssues());
}
private function Save(array $aGetRemovedClasses)
{
\IssueLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]);
DataFeatureRemoverAuditRuleService::GetInstance()->SaveChecks($aGetRemovedClasses);
}
}

View File

@@ -1,30 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Helper;
use Exception;
use Throwable;
class DataFeatureRemovalException extends Exception
{
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null, array $aContext = [])
{
if (!is_null($previous)) {
$sStack = $previous->getTraceAsString();
$sError = $previous->getMessage();
} else {
$sStack = $this->getTraceAsString();
$sError = '';
}
$aContext['error'] = $sError;
$aContext['stack'] = $sStack;
DataFeatureRemovalLog::Error($message, null, $aContext);
parent::__construct($message, $code, $previous);
}
}

View File

@@ -1,13 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Helper;
class DataFeatureRemovalHelper
{
public const MODULE_NAME = 'combodo-data-feature-removal';
}

View File

@@ -1,25 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Helper;
use LogAPI;
class DataFeatureRemovalLog extends LogAPI
{
public const CHANNEL_DEFAULT = 'DataFeatureRemoval';
protected static $m_oFileLog = null;
public static function Enable($sTargetFile = null)
{
if (empty($sTargetFile)) {
$sTargetFile = APPROOT.'log/error.log';
}
parent::Enable($sTargetFile);
}
}

View File

@@ -1,70 +0,0 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Model;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use DataFeatureRemoverAuditRule;
use DBObjectSearch;
use DBObjectSet;
use Exception;
class DataFeatureRemoverAuditRuleService
{
private static DataFeatureRemoverAuditRuleService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): DataFeatureRemoverAuditRuleService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new DataFeatureRemoverAuditRuleService();
}
return self::$oInstance;
}
final public static function SetInstance(?DataFeatureRemoverAuditRuleService $oInstance): void
{
self::$oInstance = $oInstance;
}
public function SaveChecks(array $aGetRemovedClasses)
{
$oSearch = DBObjectSearch::FromOQL('SELECT DataFeatureRemoverAuditRule', []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
while (null != ($oObj = $oSet->Fetch())) {
$oObj->DBDelete();
}
foreach ($aGetRemovedClasses as $sClass => $iCount) {
$oObj = new DataFeatureRemoverAuditRule();
$oObj->Set('rule_name', 'FINAL_CLASS');
$oObj->Set('extension_code', $sClass);
$oObj->Set('class_name', $sClass);
$oObj->Set('count', $iCount);
$oObj->DBWrite();
}
}
public function ReadCheckRules(): array
{
try {
$oSearch = DBObjectSearch::FromOQL('SELECT DataFeatureRemoverAuditRule', []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
$aRes = [];
while (null != ($oObj = $oSet->Fetch())) {
$aRes[] = $oObj;
}
return $aRes;
} catch (Exception $e) {
throw new DataFeatureRemovalException(__FUNCTION__.' failed', 0, $e);
}
}
}

View File

@@ -1,122 +0,0 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Model;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use DataFeatureRemoverExtension;
use DBObjectSearch;
use DBObjectSet;
use Exception;
use iTopExtension;
use iTopExtensionsMap;
use MetaModel;
class DataFeatureRemoverExtensionService
{
private static DataFeatureRemoverExtensionService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): DataFeatureRemoverExtensionService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new DataFeatureRemoverExtensionService();
}
return self::$oInstance;
}
final public static function SetInstance(?DataFeatureRemoverExtensionService $oInstance): void
{
self::$oInstance = $oInstance;
}
public function SaveExtensions(array $aSelectedExtensionsForCheck)
{
$this->ReadItopExtensions();
$oSearch = DBObjectSearch::FromOQL('SELECT DataFeatureRemoverExtension', []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
while (null != ($oObj = $oSet->Fetch())) {
$oObj->DBDelete();
}
foreach ($aSelectedExtensionsForCheck as $i => $sCode) {
$oObj = new DataFeatureRemoverExtension();
$oObj->Set('extension_code', $sCode);
/** @var iTopExtension $oExtension */
$oExtension = $this->aItopExtensions[$sCode];
$oObj->Set('module_names', json_encode($oExtension->aModules));
$oObj->Set('label', $oExtension->sLabel);
$oObj->Set('version', $oExtension->sVersion);
$oObj->DBWrite();
}
}
private array $aSelectedExtensions = [];
private array $aItopExtensions = [];
private array $aIncludingExtensionsByModuleName = [];
public function ReadAuditedExtensions(): array
{
if (count($this->aSelectedExtensions) == 0) {
try {
$oSearch = DBObjectSearch::FromOQL('SELECT DataFeatureRemoverExtension', []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
while (null != ($oObj = $oSet->Fetch())) {
$sCode = $oObj->Get('extension_code');
$sLabel = $oObj->Get('label');
$sVersion = $oObj->Get('version');
$sModuleNames = $oObj->Get('module_names');
$aModuleNames = json_decode($sModuleNames, true);
if (is_array($aModuleNames) && count($aModuleNames) > 0) {
foreach ($aModuleNames as $sModuleName) {
$aExtensions = $this->aIncludingExtensionsByModuleName[$sModuleName] ?? [];
$aExtensions[] = "$sLabel / $sVersion";
$this->aIncludingExtensionsByModuleName[$sModuleName] = $aExtensions;
}
}
$this->aSelectedExtensions[$sCode] = $oObj;
}
} catch (Exception $e) {
throw new DataFeatureRemovalException(__FUNCTION__.' failed', 0, $e);
}
}
\IssueLog::Debug(__METHOD__, null, ['aSelectedExtensionsForCheck' => $this->aSelectedExtensions]);
\IssueLog::Debug(__METHOD__, null, ['aIncludingExtensionsByModuleName' => $this->aIncludingExtensionsByModuleName]);
return $this->aSelectedExtensions;
}
public function GetIncludingExtensions(string $sModuleName): array
{
$this->ReadAuditedExtensions();
return $this->aIncludingExtensionsByModuleName[$sModuleName] ?? [];
}
/**
* @return iTopExtension[]
*/
public function ReadItopExtensions(): array
{
if (count($this->aItopExtensions) == 0) {
$oExtensionsMap = new iTopExtensionsMap();
$oExtensionsMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig());
$this->aItopExtensions = $oExtensionsMap->GetAllExtensionsToDisplayInSetup(true);
uasort($this->aItopExtensions, function (iTopExtension $oiTopExtension1, iTopExtension $oiTopExtension2) {
return strcmp($oiTopExtension1->sLabel, $oiTopExtension2->sLabel);
});
}
return $this->aItopExtensions;
}
}

View File

@@ -1,8 +0,0 @@
{# @copyright Copyright (C) 2010-2024 Combodo SAS #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% UIForm Standard {} %}
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Analysis:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Analysis:SubTitle'|dict_format(rule_count) } %}
{% UIDataTable ForForm { sRef:'aCheckRules', aColumns:aCheckRules.Columns, aData:aCheckRules.Data} %}{% EndUIDataTable %}
{% EndUIPanel %}
{% EndUIForm %}

View File

@@ -1,18 +0,0 @@
{# @copyright Copyright (C) 2010-2024 Combodo SAS #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% UIForm Standard {} %}
{% UIInput ForHidden {sName:'operation', sValue:'Analyze'} %}
{% UIInput ForHidden {sName:'transaction_id', sValue:sTransactionId} %}
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Features:Title'|dict_s} %}
{% UIDataTable ForForm { sRef:'aExtensions', aColumns:aExtensions.Columns, aData:aExtensions.Data} %}{% EndUIDataTable %}
{% EndUIFieldSet %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Analyze'|dict_s, sName:'btn_apply', sId:'btn_apply', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}

View File

@@ -1,28 +0,0 @@
{# @copyright Copyright (C) 2010-2025 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{# Usable variables: #}
{# * sTitle => page title #}
{# * sMessage => success message #}
{# * sError => error message #}
{# DataFeatureRemoval #}
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Main:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Main:SubTitle'|dict_s } %}
{% UIAlert ForInformation { sTitle:'DataFeatureRemoval:Helper:Title'|dict_s } %}
{{ 'DataFeatureRemoval:Helper:Desc1'|dict_s }}<BR>
{{ 'DataFeatureRemoval:Helper:Desc2'|dict_s }}
{% EndUIAlert %}
{% 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 %}
{% include 'FeaturesTab.html.twig' %}
{% include 'ExtensionRemovalDataTab.html.twig' %}
{% EndUIPanel %}

View File

@@ -1,9 +0,0 @@
{# @copyright Copyright (C) 2010-2024 Combodo SAS #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
$(document).on('click', '#checkAllExtensions', function() {
var bChecked = this.checked;
$('.extension_check').each( function() { this.checked = bChecked });
});

View File

@@ -1,2 +0,0 @@
{# @copyright Copyright (C) 2010-2024 Combodo SAS #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}

View File

@@ -1,25 +0,0 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit4f96a7199e2c0d90e547333758b26464::getLoader();

View File

@@ -1,579 +0,0 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@@ -1,21 +0,0 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,17 +0,0 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => $baseDir . '/src/Controller/DataFeatureRemovalController.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => $baseDir . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => $baseDir . '/src/Helper/DataFeatureRemovalHelper.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => $baseDir . '/src/Helper/DataFeatureRemovalLog.php',
'Combodo\\iTop\\DataFeatureRemoval\\Model\\DataFeatureRemoverAuditRuleService' => $baseDir . '/src/Model/DataFeatureRemoverAuditRuleService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Model\\DataFeatureRemoverExtensionService' => $baseDir . '/src/Model/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\SetupAudit' => $baseDir . '/src/Service/SetupAudit.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@@ -1,9 +0,0 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@@ -1,11 +0,0 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Combodo\\iTop\\DataFeatureRemoval\\' => array($baseDir . '/src'),
'' => array($baseDir . '/src/NoNamespace'),
);

View File

@@ -1,37 +0,0 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit4f96a7199e2c0d90e547333758b26464
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit4f96a7199e2c0d90e547333758b26464', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit4f96a7199e2c0d90e547333758b26464', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit4f96a7199e2c0d90e547333758b26464::getInitializer($loader));
$loader->setClassMapAuthoritative(true);
$loader->register(true);
return $loader;
}
}

View File

@@ -1,48 +0,0 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
{
public static $prefixLengthsPsr4 = array (
'C' =>
array (
'Combodo\\iTop\\DataFeatureRemoval\\' => 32,
),
);
public static $prefixDirsPsr4 = array (
'Combodo\\iTop\\DataFeatureRemoval\\' =>
array (
0 => __DIR__ . '/../..' . '/src',
),
);
public static $fallbackDirsPsr4 = array (
0 => __DIR__ . '/../..' . '/src/NoNamespace',
);
public static $classMap = array (
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => __DIR__ . '/../..' . '/src/Controller/DataFeatureRemovalController.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalHelper.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalLog.php',
'Combodo\\iTop\\DataFeatureRemoval\\Model\\DataFeatureRemoverAuditRuleService' => __DIR__ . '/../..' . '/src/Model/DataFeatureRemoverAuditRuleService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Model\\DataFeatureRemoverExtensionService' => __DIR__ . '/../..' . '/src/Model/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\SetupAudit' => __DIR__ . '/../..' . '/src/Service/SetupAudit.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$prefixDirsPsr4;
$loader->fallbackDirsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$fallbackDirsPsr4;
$loader->classMap = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$classMap;
}, null, ClassLoader::class);
}
}

View File

@@ -10,7 +10,6 @@ namespace Combodo\iTop\DBTools\Service;
use CMDBSource;
use DBObjectSearch;
use DBObjectSet;
use IssueLog;
class DBToolsUtils
{

View File

@@ -92,7 +92,7 @@ final class CoreUpdater
$sFinalEnv = 'production';
$oRuntimeEnv = new RunTimeEnvironmentCoreUpdater($sFinalEnv, false);
$oRuntimeEnv->CheckDirectories($sFinalEnv);
$oRuntimeEnv->CompileFrom($sFinalEnv);
$oRuntimeEnv->CompileFrom('production');
$oRuntimeEnv->Rollback();
@@ -155,13 +155,21 @@ final class CoreUpdater
APPROOT.'extensions',
];
$aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $aDirsToScanForModules);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation');
$aSelectedModules = [];
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
} else {
$aSelectedModules[] = $sModuleId;
}
}
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation');
$oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade');
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation');
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation');
$oRuntimeEnv->UpdatePredefinedObjects();
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup');
$oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad');
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup');
$oRuntimeEnv->LoadData($aAvailableModules, $aSelectedModules, false /* no sample data*/);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad');
$sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion();
$oExtensionsMap = new iTopExtensionsMap();
// Default choices = as before
@@ -179,7 +187,7 @@ final class CoreUpdater
$oRuntimeEnv->RecordInstallation(
$oConfig,
$sDataModelVersion,
array_keys($aAvailableModules),
$aSelectedModules,
$aSelectedExtensionCodes,
'Done by the iTop Core Updater'
);

View File

@@ -24,13 +24,129 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\HubConnector\Controller\HubController;
use Combodo\iTop\Application\WebPage\JsonPage;
require_once(APPROOT.'application/utils.inc.php');
require_once(APPROOT.'core/log.class.inc.php');
IssueLog::Enable(APPROOT.'log/error.log');
require_once(__DIR__.'/src/Controller/HubController.php');
require_once(APPROOT.'setup/runtimeenv.class.inc.php');
require_once(APPROOT.'setup/backup.class.inc.php');
require_once(APPROOT.'core/mutex.class.inc.php');
require_once(APPROOT.'core/dict.class.inc.php');
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
require_once(__DIR__.'/hubruntimeenvironment.class.inc.php');
/**
* Overload of DBBackup to handle logging
*/
class DBBackupWithErrorReporting extends DBBackup
{
protected $aInfos = [];
protected $aErrors = [];
protected function LogInfo($sMsg)
{
$aInfos[] = $sMsg;
}
protected function LogError($sMsg)
{
IssueLog::Error($sMsg);
$aErrors[] = $sMsg;
}
public function GetInfos()
{
return $this->aInfos;
}
public function GetErrors()
{
return $this->aErrors;
}
}
/**
*
* @param string $sTargetFile
* @throws Exception
* @return DBBackupWithErrorReporting
*/
function DoBackup($sTargetFile)
{
// Make sure the target directory exists
$sBackupDir = dirname($sTargetFile);
SetupUtils::builddir($sBackupDir);
$oBackup = new DBBackupWithErrorReporting();
$oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
$sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
$oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment());
$oMutex->Lock();
try {
$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
} catch (Exception $e) {
$oMutex->Unlock();
throw $e;
}
$oMutex->Unlock();
return $oBackup;
}
/**
* Outputs the status of the current ajax execution (as a JSON structure)
*
* @param string $sMessage
* @param bool $bSuccess
* @param number $iErrorCode
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
function ReportStatus($sMessage, $bSuccess, $iErrorCode = 0, $aMoreFields = [])
{
// Do not use AjaxPage during setup phases, because it uses InterfaceDiscovery in Twig compilation
$oPage = new JsonPage();
$aResult = [
'code' => $iErrorCode,
'message' => $sMessage,
'fields' => $aMoreFields,
];
$oPage->SetData($aResult);
$oPage->SetOutputDataOnly(true);
$oPage->output();
}
/**
* Helper to output the status of a successful execution
*
* @param string $sMessage
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
function ReportSuccess($sMessage, $aMoreFields = [])
{
ReportStatus($sMessage, true, 0, $aMoreFields);
}
/**
* Helper to output the status of a failed execution
*
* @param string $sMessage
* @param number $iErrorCode
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
function ReportError($sMessage, $iErrorCode, $aMoreFields = [])
{
if ($iErrorCode == 0) {
// 0 means no error, so change it if no meaningful error code is supplied
$iErrorCode = -1;
}
ReportStatus($sMessage, false, $iErrorCode, $aMoreFields);
}
try {
SetupUtils::ExitMaintenanceMode(false); // Reset maintenance mode in case of problem
@@ -67,7 +183,7 @@ try {
foreach ($aChecks as $oCheckResult) {
if ($oCheckResult->iSeverity == CheckResult::ERROR) {
$bFailed = true;
HubController::GetInstance()->ReportError($oCheckResult->sLabel, -2);
ReportError($oCheckResult->sLabel, -2);
}
}
if (!$bFailed) {
@@ -75,27 +191,169 @@ try {
$fFreeSpace = SetupUtils::CheckDiskSpace($sDBBackupPath);
if ($fFreeSpace !== false) {
$sMessage = Dict::Format('iTopHub:BackupFreeDiskSpaceIn', SetupUtils::HumanReadableSize($fFreeSpace), dirname($sDBBackupPath));
HubController::GetInstance()->ReportSuccess($sMessage);
ReportSuccess($sMessage);
} else {
HubController::GetInstance()->ReportError(Dict::S('iTopHub:FailedToCheckFreeDiskSpace'), -1);
ReportError(Dict::S('iTopHub:FailedToCheckFreeDiskSpace'), -1);
}
}
break;
case 'do_backup':
HubController::GetInstance()->LaunchBackup();
require_once(APPROOT.'/application/startup.inc.php');
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
try {
if (MetaModel::GetConfig()->Get('demo_mode')) {
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
}
SetupLog::Info('Backup starts...');
set_time_limit(0);
$sBackupPath = APPROOT.'/data/backups/manual/backup-';
$iSuffix = 1;
$sSuffix = '';
// Generate a unique name...
do {
$sBackupFile = $sBackupPath.date('Y-m-d-His').$sSuffix;
$sSuffix = '-'.$iSuffix;
$iSuffix++ ;
} while (file_exists($sBackupFile));
$oBackup = DoBackup($sBackupFile);
$aErrors = $oBackup->GetErrors();
if (count($aErrors) > 0) {
SetupLog::Error('Backup failed.');
SetupLog::Error(implode("\n", $aErrors));
ReportError(Dict::S('iTopHub:BackupFailed'), -1, $aErrors);
} else {
SetupLog::Info('Backup successfully completed.');
ReportSuccess(Dict::S('iTopHub:BackupOk'));
}
} catch (Exception $e) {
SetupLog::Error($e->getMessage());
ReportError($e->getMessage(), $e->getCode());
}
break;
case 'compile':
HubController::GetInstance()->LaunchCompile();
SetupLog::Info('Deployment starts...');
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
}
// First step: prepare the datamodel, if it fails, roll-back
$aSelectedExtensionCodes = utils::ReadParam('extension_codes', []);
$aSelectedExtensionDirs = utils::ReadParam('extension_dirs', []);
$oRuntimeEnv = new HubRunTimeEnvironment('production', false); // use a temp environment: production-build
$oRuntimeEnv->MoveSelectedExtensions(APPROOT.'/data/downloaded-extensions/', $aSelectedExtensionDirs);
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
if ($oConfig->Get('demo_mode')) {
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
}
$aSelectModules = $oRuntimeEnv->CompileFrom('production', false); // WARNING symlinks does not seem to be compatible with manual Commit
$oRuntimeEnv->UpdateIncludes($oConfig);
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
// Safety check: check the inter dependencies, will throw an exception in case of inconsistency
$oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
$oRuntimeEnv->CheckMetaModel(); // Will throw an exception if a problem is detected
// Everything seems Ok so far, commit in env-production!
$oRuntimeEnv->WriteConfigFileSafe($oConfig);
$oRuntimeEnv->Commit();
// Report the success in a way that will be detected by the ajax caller
SetupLog::Info('Compilation completed...');
ReportSuccess('Ok'); // No access to Dict::S here
break;
case 'move_to_production':
HubController::GetInstance()->LaunchDeploy();
// Second step: update the schema and the data
// Everything happening below is based on env-production
$oRuntimeEnv = new RunTimeEnvironment('production', true);
try {
SetupLog::Info('Move to production starts...');
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
}
unlink(utils::GetDataPath().'hub/compile_authent');
// Load the "production" config file to clone & update it
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
SetupUtils::EnterReadOnlyMode($oConfig);
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
$aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
$aSelectedModules = [];
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
} else {
$aSelectedModules[] = $sModuleId;
}
}
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation');
$oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade');
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation');
$oRuntimeEnv->UpdatePredefinedObjects();
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup');
$oRuntimeEnv->LoadData($aAvailableModules, $aSelectedModules, false /* no sample data*/);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad');
// Record the installation so that the "about box" knows about the installed modules
$sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion();
$oExtensionsMap = new iTopExtensionsMap();
// Default choices = as before
$oExtensionsMap->LoadChoicesFromDatabase($oConfig);
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {
// Plus all "remote" extensions
if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) {
$oExtensionsMap->MarkAsChosen($oExtension->sCode);
}
}
$aSelectedExtensionCodes = [];
foreach ($oExtensionsMap->GetChoices() as $oExtension) {
$aSelectedExtensionCodes[] = $oExtension->sCode;
}
$aSelectedExtensions = $oExtensionsMap->GetChoices();
$oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModules, $aSelectedExtensionCodes, 'Done by the iTop Hub Connector');
// Report the success in a way that will be detected by the ajax caller
SetupLog::Info('Deployment successfully completed.');
ReportSuccess(Dict::S('iTopHub:CompiledOK'));
} catch (Exception $e) {
if (file_exists(utils::GetDataPath().'hub/compile_authent')) {
unlink(utils::GetDataPath().'hub/compile_authent');
}
// Note: at this point, the dictionnary is not necessarily loaded
SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage());
SetupLog::Error('Debug trace: '.$e->getTraceAsString());
ReportError($e->getMessage(), $e->getCode());
} finally {
SetupUtils::ExitReadOnlyMode();
}
break;
default:
HubController::GetInstance()->ReportError("Invalid operation: '$sOperation'", -1);
ReportError("Invalid operation: '$sOperation'", -1);
}
} catch (Exception $e) {
SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage());
@@ -103,5 +361,5 @@ try {
utils::PopArchiveMode();
HubController::GetInstance()->ReportError($e->getMessage(), $e->getCode());
ReportError($e->getMessage(), $e->getCode());
}

View File

@@ -1,17 +1,9 @@
<?php
namespace Combodo\iTop\HubConnector\setup;
use Config;
use Exception;
use RunTimeEnvironment;
use SetupUtils;
class HubRunTimeEnvironment extends RunTimeEnvironment
{
/**
* Constructor
*
* @param string $sEnvironment
* @param string $bAutoCommit
*/
@@ -32,7 +24,6 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
/**
* Update the includes for the target environment
*
* @param Config $oConfig
*/
public function UpdateIncludes(Config $oConfig)
@@ -42,9 +33,7 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
/**
* Move an extension (path to folder of this extension) to the target environment
*
* @param string $sExtensionDirectory The folder of the extension
*
* @throws Exception
*/
public function MoveExtension($sExtensionDirectory)
@@ -68,10 +57,8 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
/**
* Move the selected extensions located in the given directory in data/<target-env>-modules
*
* @param string $sDownloadedExtensionsDir The directory to scan
* @param string[] $aSelectedExtensionDirs The list of folders to move
*
* @throws Exception
*/
public function MoveSelectedExtensions($sDownloadedExtensionsDir, $aSelectedExtensionDirs)

View File

@@ -186,7 +186,7 @@ function collect_configuration()
// iTop modules
$oConfig = MetaModel::GetConfig();
$aInstalledModules = ModuleInstallationRepository::GetInstance()->ReadFromDB($oConfig);
$aInstalledModules = ModuleInstallationService::GetInstance()->ReadFromDB($oConfig);
foreach ($aInstalledModules as $aDBInfo) {
$aConfiguration['itop_modules'][$aDBInfo['name']] = $aDBInfo['version'];

View File

@@ -1,300 +0,0 @@
<?php
namespace Combodo\iTop\HubConnector\Controller;
use Combodo\iTop\Application\WebPage\JsonPage;
use Combodo\iTop\HubConnector\Model\DBBackupWithErrorReporting;
use Combodo\iTop\HubConnector\setup\HubRunTimeEnvironment;
use Config;
use Dict;
use Exception;
use iTopExtension;
use iTopExtensionsMap;
use iTopMutex;
use LoginWebPage;
use MetaModel;
use MFCompiler;
use RunTimeEnvironment;
use SecurityException;
use SetupLog;
use SetupUtils;
use utils;
require_once(APPROOT.'setup/runtimeenv.class.inc.php');
require_once(APPROOT.'setup/backup.class.inc.php');
require_once(APPROOT.'core/mutex.class.inc.php');
require_once(APPROOT.'core/dict.class.inc.php');
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
require_once(__DIR__.'/../setup/hubruntimeenvironment.class.inc.php');
class HubController
{
private static HubController $oInstance;
protected $bOutputHeaders = false;
protected function __construct()
{
}
final public static function GetInstance(): HubController
{
if (!isset(self::$oInstance)) {
self::$oInstance = new HubController();
}
return self::$oInstance;
}
final public static function SetInstance(?HubController $oInstance): void
{
self::$oInstance = $oInstance;
}
public function LaunchBackup()
{
require_once(APPROOT.'/application/startup.inc.php');
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
try {
if (MetaModel::GetConfig()->Get('demo_mode')) {
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
}
SetupLog::Info('Backup starts...');
set_time_limit(0);
$sBackupPath = APPROOT.'/data/backups/manual/backup-';
$iSuffix = 1;
$sSuffix = '';
// Generate a unique name...
do {
$sBackupFile = $sBackupPath.date('Y-m-d-His').$sSuffix;
$sSuffix = '-'.$iSuffix;
$iSuffix++ ;
} while (file_exists($sBackupFile));
$oBackup = $this->DoBackup($sBackupFile);
$aErrors = $oBackup->GetErrors();
if (count($aErrors) > 0) {
SetupLog::Error('Backup failed.');
SetupLog::Error(implode("\n", $aErrors));
$this->ReportError(Dict::S('iTopHub:BackupFailed'), -1, $aErrors);
} else {
SetupLog::Info('Backup successfully completed.');
$this->ReportSuccess(Dict::S('iTopHub:BackupOk'));
}
} catch (Exception $e) {
SetupLog::Error($e->getMessage());
$this->ReportError($e->getMessage(), $e->getCode());
}
}
/**
*
* @param string $sTargetFile
* @throws Exception
* @return DBBackupWithErrorReporting
*/
public function DoBackup($sTargetFile): DBBackupWithErrorReporting
{
// Make sure the target directory exists
$sBackupDir = dirname($sTargetFile);
SetupUtils::builddir($sBackupDir);
$oBackup = new DBBackupWithErrorReporting();
$oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
$sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
$oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment());
$oMutex->Lock();
try {
$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
} catch (Exception $e) {
$oMutex->Unlock();
throw $e;
}
$oMutex->Unlock();
return $oBackup;
}
public function LaunchCompile()
{
SetupLog::Info('Deployment starts...');
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
}
// First step: prepare the datamodel, if it fails, roll-back
$aSelectedExtensionCodes = utils::ReadParam('extension_codes', []);
$aSelectedExtensionDirs = utils::ReadParam('extension_dirs', []);
$oRuntimeEnv = new HubRunTimeEnvironment('production', false); // use a temp environment: production-build
$oRuntimeEnv->MoveSelectedExtensions(APPROOT.'/data/downloaded-extensions/', $aSelectedExtensionDirs);
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
if ($oConfig->Get('demo_mode')) {
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
}
$aSelectModules = $oRuntimeEnv->CompileFrom('production'); // WARNING symlinks does not seem to be compatible with manual Commit
$oRuntimeEnv->UpdateIncludes($oConfig);
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
// Safety check: check the inter dependencies, will throw an exception in case of inconsistency
$oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
$oRuntimeEnv->CheckMetaModel(); // Will throw an exception if a problem is detected
// Everything seems Ok so far, commit in env-production!
$oRuntimeEnv->WriteConfigFileSafe($oConfig);
$oRuntimeEnv->Commit();
// Report the success in a way that will be detected by the ajax caller
SetupLog::Info('Compilation completed...');
$this->ReportSuccess('Ok'); // No access to Dict::S here
}
public function LaunchDeploy()
{
// Second step: update the schema and the data
// Everything happening below is based on env-production
$oRuntimeEnv = new RunTimeEnvironment('production', true);
try {
SetupLog::Info('Move to production starts...');
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
}
unlink(utils::GetDataPath().'hub/compile_authent');
// Load the "production" config file to clone & update it
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
SetupUtils::EnterReadOnlyMode($oConfig);
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
$aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation');
$oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade');
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation');
$oRuntimeEnv->UpdatePredefinedObjects();
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup');
$oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad');
// Record the installation so that the "about box" knows about the installed modules
$sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion();
$oExtensionsMap = new iTopExtensionsMap();
// Default choices = as before
$oExtensionsMap->LoadChoicesFromDatabase($oConfig);
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {
// Plus all "remote" extensions
if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) {
$oExtensionsMap->MarkAsChosen($oExtension->sCode);
}
}
$aSelectedExtensionCodes = [];
foreach ($oExtensionsMap->GetChoices() as $oExtension) {
$aSelectedExtensionCodes[] = $oExtension->sCode;
}
$aSelectedExtensions = $oExtensionsMap->GetChoices();
$oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, array_keys($aAvailableModules), $aSelectedExtensionCodes, 'Done by the iTop Hub Connector');
// Report the success in a way that will be detected by the ajax caller
SetupLog::Info('Deployment successfully completed.');
$this->ReportSuccess(Dict::S('iTopHub:CompiledOK'));
} catch (Exception $e) {
if (file_exists(utils::GetDataPath().'hub/compile_authent')) {
unlink(utils::GetDataPath().'hub/compile_authent');
}
// Note: at this point, the dictionnary is not necessarily loaded
SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage());
SetupLog::Error('Debug trace: '.$e->getTraceAsString());
$this->ReportError($e->getMessage(), $e->getCode());
} finally {
SetupUtils::ExitReadOnlyMode();
}
}
/**
* Outputs the status of the current ajax execution (as a JSON structure)
*
* @param string $sMessage
* @param bool $bSuccess
* @param number $iErrorCode
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
public function ReportStatus($sMessage, $bSuccess, $iErrorCode = 0, $aMoreFields = [])
{
// Do not use AjaxPage during setup phases, because it uses InterfaceDiscovery in Twig compilation
$this->oLastJsonPage = new JsonPage();
$this->oLastJsonPage->SetOutputHeaders($this->bOutputHeaders);
$aResult = [
'code' => $iErrorCode,
'message' => $sMessage,
'fields' => $aMoreFields,
];
$this->oLastJsonPage->SetData($aResult);
$this->oLastJsonPage->SetOutputDataOnly(true);
$this->oLastJsonPage->output();
}
private ?JsonPage $oLastJsonPage = null;
public function GetLastJsonPage(): ?JsonPage
{
return $this->oLastJsonPage;
}
/**
* Helper to output the status of a successful execution
*
* @param string $sMessage
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
public function ReportSuccess($sMessage, $aMoreFields = [])
{
$this->ReportStatus($sMessage, true, 0, $aMoreFields);
}
/**
* Helper to output the status of a failed execution
*
* @param string $sMessage
* @param number $iErrorCode
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
public function ReportError($sMessage, $iErrorCode, $aMoreFields = [])
{
if ($iErrorCode == 0) {
// 0 means no error, so change it if no meaningful error code is supplied
$iErrorCode = -1;
}
$this->ReportStatus($sMessage, false, $iErrorCode, $aMoreFields);
}
/**
* Dont print headers for testing purpose mainly
* @param bool bOutputHeaders
*
* @return void
*/
public function SetOutputHeaders(bool $bOutputHeaders): void
{
$this->bOutputHeaders = $bOutputHeaders;
}
}

View File

@@ -1,37 +0,0 @@
<?php
namespace Combodo\iTop\HubConnector\Model;
use DBBackup;
use IssueLog;
/**
* Overload of DBBackup to handle logging
*/
class DBBackupWithErrorReporting extends DBBackup
{
protected $aInfos = [];
protected $aErrors = [];
protected function LogInfo($sMsg)
{
$this->aInfos[] = $sMsg;
}
protected function LogError($sMsg)
{
IssueLog::Error($sMsg);
$this->aErrors[] = $sMsg;
}
public function GetInfos(): array
{
return $this->aInfos;
}
public function GetErrors(): array
{
return $this->aErrors;
}
}

View File

@@ -12,7 +12,8 @@ SetupWebPage::AddModule(
// Setup
//
'dependencies' => [
'itop-structure/2.7.1 || itop-portal/3.0.0', // itop-portal : module_design_itop_design->module_designs->itop-portal
'itop-structure/2.7.1',
'itop-portal/3.0.0', // module_design_itop_design->module_designs->itop-portal
],
'mandatory' => false,
'visible' => true,

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('CS CZ', 'Czech', 'Čeština', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('DA DA', 'Danish', 'Dansk', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('DE DE', 'German', 'Deutsch', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,21 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
Dict::Add('EN US', 'English', 'English', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('FR FR', 'French', 'Français', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installé',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'va être installé',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'pas installé',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'va être désinstallé',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'non désinstallable',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'supprimé du disque',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'À propos de %1$s',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'Plus d\'informations',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Forcer la désinstallation',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('HU HU', 'Hungarian', 'Magyar', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('IT IT', 'Italian', 'Italiano', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('JA JP', 'Japanese', '日本語', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('NL NL', 'Dutch', 'Nederlands', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('PL PL', 'Polish', 'Polski', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('RU RU', 'Russian', 'Русский', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('TR TR', 'Turkish', 'Türkçe', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -156,8 +156,6 @@ return array(
'Combodo\\iTop\\Application\\UI\\Base\\AbstractUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/AbstractUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\Alert' => $baseDir . '/sources/Application/UI/Base/Component/Alert/Alert.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\AlertUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/Alert/AlertUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Badge\\Badge' => $baseDir . '/sources/Application/UI/Base/Component/Badge/Badge.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Badge\\BadgeUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/Badge/BadgeUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Breadcrumbs\\Breadcrumbs' => $baseDir . '/sources/Application/UI/Base/Component/Breadcrumbs/Breadcrumbs.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\ButtonGroup\\ButtonGroup' => $baseDir . '/sources/Application/UI/Base/Component/ButtonGroup/ButtonGroup.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\ButtonGroup\\ButtonGroupUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/ButtonGroup/ButtonGroupUIBlockFactory.php',
@@ -270,8 +268,6 @@ return array(
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardColumn' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardColumn.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardLayout' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardLayout.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardRow' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardRow.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Extension\\ExtensionDetails' => $baseDir . '/sources/Application/UI/Base/Layout/Extension/ExtensionDetails.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Extension\\ExtensionDetailsUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Layout/Extension/ExtensionDetailsUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\Column' => $baseDir . '/sources/Application/UI/Base/Layout/MultiColumn/Column/Column.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\ColumnUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Layout/MultiColumn/Column/ColumnUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\MultiColumn' => $baseDir . '/sources/Application/UI/Base/Layout/MultiColumn/MultiColumn.php',

View File

@@ -537,8 +537,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Application\\UI\\Base\\AbstractUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/AbstractUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\Alert' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Alert/Alert.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\AlertUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Alert/AlertUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Badge\\Badge' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Badge/Badge.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Badge\\BadgeUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Badge/BadgeUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Breadcrumbs\\Breadcrumbs' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Breadcrumbs/Breadcrumbs.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\ButtonGroup\\ButtonGroup' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/ButtonGroup/ButtonGroup.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\ButtonGroup\\ButtonGroupUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/ButtonGroup/ButtonGroupUIBlockFactory.php',
@@ -651,8 +649,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardColumn' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardColumn.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardLayout' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardLayout.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardRow' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardRow.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Extension\\ExtensionDetails' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Extension/ExtensionDetails.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Extension\\ExtensionDetailsUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Extension/ExtensionDetailsUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\Column' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/MultiColumn/Column/Column.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\ColumnUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/MultiColumn/Column/ColumnUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\MultiColumn' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/MultiColumn/MultiColumn.php',

View File

@@ -1,11 +1,12 @@
<?php
require_once __DIR__.'/ModuleInstallationRepository.php';
require_once __DIR__.'/ModuleInstallationService.php';
class AnalyzeInstallation
{
private static AnalyzeInstallation $oInstance;
private ?array $aAvailableModules = null;
private ?array $aSelectInstall = null;
protected function __construct()
{
@@ -22,7 +23,7 @@ class AnalyzeInstallation
final public static function SetInstance(?AnalyzeInstallation $oInstance): void
{
self::$oInstance = $oInstance;
static::$oInstance = $oInstance;
}
/**
@@ -57,7 +58,6 @@ class AnalyzeInstallation
* )
* @throws \Exception
*/
public function AnalyzeInstallation(?Config $oConfig, mixed $modulesPath, bool $bAbortOnMissingDependency = false, ?array $aModulesToLoad = null)
{
$aRes = [
@@ -73,7 +73,7 @@ class AnalyzeInstallation
//test only
$aAvailableModules = $this->aAvailableModules;
} else {
$aAvailableModules = ModuleDiscovery::GetModulesOrderedByDependencies($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
$aAvailableModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
}
foreach ($aAvailableModules as $sModuleId => $aModuleInfo) {
@@ -96,7 +96,7 @@ class AnalyzeInstallation
$aRes[$sModuleName] = $aModuleInfo;
}
$aCurrentlyInstalledModules = ModuleInstallationRepository::GetInstance()->ReadComputeInstalledModules($oConfig);
$aCurrentlyInstalledModules = ModuleInstallationService::GetInstance()->ReadComputeInstalledModules($oConfig);
// Adjust the list of proposed modules
foreach ($aCurrentlyInstalledModules as $sModuleName => $aModuleDB) {

View File

@@ -1,25 +1,25 @@
<?php
class ModuleInstallationRepository
class ModuleInstallationService
{
private static ModuleInstallationRepository $oInstance;
private static ModuleInstallationService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): ModuleInstallationRepository
final public static function GetInstance(): ModuleInstallationService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModuleInstallationRepository();
self::$oInstance = new ModuleInstallationService();
}
return self::$oInstance;
}
final public static function SetInstance(?ModuleInstallationRepository $oInstance): void
final public static function SetInstance(?ModuleInstallationService $oInstance): void
{
self::$oInstance = $oInstance;
static::$oInstance = $oInstance;
}
private ?array $aSelectInstall = null;
@@ -95,17 +95,8 @@ SQL;
$aSelectInstall = CMDBSource::QueryToArray($sSQLQuery);
} catch (MySQLException $e) {
// No database or erroneous information
SetupLog::Error(
'Can not connect to the database',
null,
[
'host' => $oConfig->Get('db_host'),
'user' => $oConfig->Get('db_user'),
'pwd:' => $oConfig->Get('db_pwd'),
'db name' => $oConfig->Get('db_name'),
'msg' => $e->getMessage(),
]
);
$this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name'));
$this->log_error('Exception '.$e->getMessage());
return false;
}
@@ -138,10 +129,8 @@ SQL;
// so assume that the datamodel version is equal to the application version
$aResult['datamodel_version'] = $aResult['product_version'];
}
SetupLog::Info(__METHOD__, null, ["product_name" => $aResult['product_name'], "product_version" => $aResult['product_version']]);
return count($aResult) == 0 ? false : $aResult;
$this->log_info("GetApplicationVersion returns: product_name: ".$aResult['product_name'].', product_version: '.$aResult['product_version']);
return empty($aResult) ? false : $aResult;
}
private function ComputeInstalledModules(array $aSelectInstall): array
@@ -192,47 +181,4 @@ SQL;
return $aInstallByModule;
}
/**
* Return previous module installation. offset is applied on parent_id.
* @param $iOffset: by default (offset=0) returns current installation
* @return array
*/
public static function GetPreviousModuleInstallationsByOffset(int $iOffset = 0): array
{
$oFilter = DBObjectSearch::FromOQL('SELECT ModuleInstallation AS mi WHERE mi.parent_id=0 AND mi.name!="datamodel"');
$oSet = new DBObjectSet($oFilter, ['installed' => false]); // Most recent first
$oSet->SetLimit($iOffset + 1);
$iParentId = 0;
while (!is_null($oModuleInstallation = $oSet->Fetch())) {
/** @var \DBObject $oModuleInstallation */
if ($iOffset == 0) {
$iParentId = $oModuleInstallation->Get('id');
break;
}
$iOffset--;
}
if ($iParentId === 0) {
IssueLog::Error("no ITOP_APPLICATION ModuleInstallation found", null, ['offset' => $iOffset]);
throw new \Exception("no ITOP_APPLICATION ModuleInstallation found");
}
$oFilter = DBObjectSearch::FromOQL("SELECT ModuleInstallation AS mi WHERE mi.id=$iParentId OR mi.parent_id=$iParentId");
$oSet = new DBObjectSet($oFilter); // Most recent first
$aRawValues = $oSet->ToArrayOfValues();
$aValues = [];
foreach ($aRawValues as $aRawValue) {
$aValue = [];
foreach ($aRawValue as $sAliasAttCode => $sValue) {
// remove 'mi.' from AttCode
$sAttCode = substr($sAliasAttCode, 3);
$aValue[$sAttCode] = $sValue;
}
$aValues[] = $aValue;
}
return $aValues;
}
}

View File

@@ -1,14 +0,0 @@
<?php
class SetupDBBackup extends DBBackup
{
protected function LogInfo($sMsg)
{
SetupLog::Ok('Info - '.$sMsg);
}
protected function LogError($sMsg)
{
SetupLog::Ok('Error - '.$sMsg);
}
}

View File

@@ -127,7 +127,7 @@ header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past
/**
* Main program
*/
$sOperation = utils::ReadParam('operation', '');
$sOperation = Utils::ReadParam('operation', '');
try {
SetupUtils::CheckSetupToken();
@@ -138,7 +138,8 @@ try {
ini_set('display_errors', true);
ini_set('display_startup_errors', true);
require_once(APPROOT.'/setup/wizardsteps_autoload.php');
require_once(APPROOT.'/setup/wizardcontroller.class.inc.php');
require_once(APPROOT.'/setup/wizardsteps.class.inc.php');
$sClass = utils::ReadParam('step_class', '');
$sState = utils::ReadParam('step_state', '');
@@ -163,7 +164,7 @@ try {
break;
case 'toggle_use_symbolic_links':
$sUseSymbolicLinks = utils::ReadParam('bUseSymbolicLinks', false);
$sUseSymbolicLinks = Utils::ReadParam('bUseSymbolicLinks', false);
$bUseSymbolicLinks = ($sUseSymbolicLinks === 'true');
MFCompiler::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
echo "toggle useSymbolicLinks flag : $bUseSymbolicLinks";

File diff suppressed because it is too large Load Diff

View File

@@ -21,8 +21,8 @@
use Combodo\iTop\Application\Branding;
use Combodo\iTop\Application\WebPage\iTopWebPage;
use Combodo\iTop\Application\WebPage\Page;
use Combodo\iTop\DesignDocument;
use Combodo\iTop\DesignElement;
use Combodo\iTop\DesignDocument;
require_once(APPROOT.'setup/setuputils.class.inc.php');
require_once(APPROOT.'setup/modelfactory.class.inc.php');
@@ -477,7 +477,7 @@ class MFCompiler
$sClass = $oClass->getAttribute("id");
$aAllClasses[] = $sClass;
try {
$sCompiledCode .= $this->CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir);
$sCompiledCode .= $this->CompileClass($oClass, $sModuleName, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir);
} catch (DOMFormatException $e) {
$sMessage = "Failed to process class '$sClass', ";
if (!empty($sModuleRootDir)) {
@@ -1189,6 +1189,7 @@ EOF
/**
* @param \MFElement $oClass
* @param string $sModuleName
* @param string $sTempTargetDir
* @param string $sFinalTargetDir
* @param string $sModuleRelativeDir
@@ -1196,7 +1197,7 @@ EOF
* @return string
* @throws \DOMFormatException
*/
protected function CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir)
protected function CompileClass($oClass, $sModuleName, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir)
{
$sClass = $oClass->getAttribute('id');
$oProperties = $oClass->GetUniqueElement('properties');
@@ -1209,6 +1210,7 @@ EOF
$aClassParams = [];
$aClassParams['category'] = $this->GetPropString($oProperties, 'category', '');
$aClassParams['key_type'] = "'autoincrement'";
$aClassParams['created_in'] = "'$sModuleName'";
if ((bool)$this->GetPropNumber($oProperties, 'is_link', 0)) {
$aClassParams['is_link'] = 'true';
}
@@ -3357,8 +3359,6 @@ EOF;
$bDataXmlPrecompiledFileExists = false;
clearstatcache();
$iDataXmlFileLastModified = 0;
if (!empty($sPrecompiledFileUri)) {
$sDataXmlProvidedPrecompiledFile = $sTempTargetDir.DIRECTORY_SEPARATOR.$sPrecompiledFileUri;
$bDataXmlPrecompiledFileExists = file_exists($sDataXmlProvidedPrecompiledFile) ;

View File

@@ -34,7 +34,7 @@ require_once(APPROOT.'/application/startup.inc.php');
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
$sOperation = utils::ReadParam('operation', 'step1');
$sOperation = Utils::ReadParam('operation', 'step1');
$oP = new SetupPage('iTop email test utility');
// Although this page doesn't expose sensitive info, with it we can send multiple emails
@@ -208,7 +208,7 @@ function DisplayStep2(SetupPage $oP, $sFrom, $sTo)
$oP->add("<p>Sending an email to '".htmlentities($sTo, ENT_QUOTES, 'utf-8')."'... (From: '".htmlentities($sFrom, ENT_QUOTES, 'utf-8')."')</p>\n");
$oP->add("<form method=\"post\">\n");
$oEmail = new EMail();
$oEmail = new Email();
$oEmail->SetRecipientTO($sTo);
$oEmail->SetRecipientFrom($sFrom);
$oEmail->SetSubject("Test iTop");
@@ -256,8 +256,8 @@ try {
case 'step2':
$oP->no_cache();
$sTo = utils::ReadParam('to', '', false, 'raw_data');
$sFrom = utils::ReadParam('from', '', false, 'raw_data');
$sTo = Utils::ReadParam('to', '', false, 'raw_data');
$sFrom = Utils::ReadParam('from', '', false, 'raw_data');
DisplayStep2($oP, $sFrom, $sTo);
break;

View File

@@ -3,11 +3,143 @@
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
require_once(APPROOT.'/setup/itopextension.class.inc.php');
require_once(APPROOT.'/setup/parameters.class.inc.php');
require_once(APPROOT.'/core/cmdbsource.class.inc.php');
require_once(APPROOT.'/setup/modulediscovery.class.inc.php');
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
/**
* Basic helper class to describe an extension, with some characteristics and a list of modules
*/
class iTopExtension
{
public const SOURCE_WIZARD = 'datamodels';
public const SOURCE_MANUAL = 'extensions';
public const SOURCE_REMOTE = 'data';
/**
* @var string
*/
public $sCode;
/**
* @var string
*/
public $sVersion;
/**
* @var string
*/
public $sInstalledVersion;
/**
* @var string
*/
public $sLabel;
/**
* @var string
*/
public $sDescription;
/**
* @var string
*/
public $sSource;
/**
* @var bool
*/
public $bMandatory;
/**
* @var string
*/
public $sMoreInfoUrl;
/**
* @var bool
*/
public $bMarkedAsChosen;
/**
* If null, check if at least one module cannot be uninstalled
* @var bool|null
*/
public ?bool $bCanBeUninstalled = null;
/**
* @var bool
*/
public $bVisible;
/**
* @var string[]
*/
public $aModules;
/**
* @var string[]
*/
public $aModuleVersion;
/**
* @var string[]
*/
public $aModuleInfo;
/**
* @var string
*/
public $sSourceDir;
/**
*
* @var string[]
*/
public $aMissingDependencies;
/**
* @var bool
*/
public bool $bInstalled = false;
/**
* @var bool
*/
public bool $bRemovedFromDisk = false;
public function __construct()
{
$this->sCode = '';
$this->sLabel = '';
$this->sDescription = '';
$this->sSource = self::SOURCE_WIZARD;
$this->bMandatory = false;
$this->sMoreInfoUrl = '';
$this->bMarkedAsChosen = false;
$this->sVersion = ITOP_VERSION;
$this->sInstalledVersion = '';
$this->aModules = [];
$this->aModuleVersion = [];
$this->aModuleInfo = [];
$this->sSourceDir = '';
$this->bVisible = true;
$this->aMissingDependencies = [];
}
/**
* @since 3.3.0
* @return bool
*/
public function CanBeUninstalled(): bool
{
if (!is_null($this->bCanBeUninstalled)) {
return $this->bCanBeUninstalled;
}
foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) {
$this->bCanBeUninstalled = $aModuleInfo['uninstallable'] === 'yes';
return $this->bCanBeUninstalled;
}
return true;
}
}
/**
* Helper class to discover all available extensions on a given iTop system
@@ -176,26 +308,28 @@ class iTopExtensionsMap
return $this->aExtensionsByCode[$sExtensionCode] ?? null;
}
/**
* @param array<string> $aExtensionCodes
* @return void
*/
public function DeclareExtensionAsRemoved(array $aExtensionCodes): void
/*public function GetMissingExtensions(array $aSelectedExtensions)
{
$aRemovedExtension = [];
foreach ($aExtensionCodes as $sCode) {
/** @var \iTopExtension $oExtension */
$oExtension = $this->GetFromExtensionCode($sCode);
if (!is_null($oExtension)) {
$aRemovedExtension [] = $oExtension;
\IssueLog::Info(__METHOD__.": remove extension locally", null, ['extension_code' => $oExtension->sCode]);
\SetupLog::Info(__METHOD__, null, ['selected' => $aSelectedExtensions]);
$aExtensionsFromDb = array_keys($this->aExtensionsByCode);
sort($aExtensionsFromDb);
\SetupLog::Info(__METHOD__, null, ['found' => $aExtensionsFromDb]);
$aRes = [];
foreach (array_diff($aExtensionsFromDb, $aSelectedExtensions) as $sExtensionCode) {
$oExtension = $this->GetFromExtensionCode($sExtensionCode);
if (!is_null($oExtension) && $oExtension->bVisible && $oExtension->sSource != iTopExtension::SOURCE_WIZARD) {
\SetupLog::Info(__METHOD__."$sExtensionCode", null, ['visible' => $oExtension->bVisible, 'mandatory' => $oExtension->bMandatory]);
$aRes [] = $sExtensionCode;
} else {
\IssueLog::Warning(__METHOD__." cannot find extensions", null, ['code' => $sCode]);
\SetupLog::Info(__METHOD__." MISSING $sExtensionCode");
}
}
\SetupLog::Info(__METHOD__, null, $aRes);
ModuleDiscovery::DeclareRemovedExtensions($aRemovedExtension);
}
return $aRes;
}*/
/**
* Read (recursively) a directory to find if it contains extensions (or modules)
@@ -329,7 +463,7 @@ class iTopExtensionsMap
$aSearchDirs = array_merge($aSearchDirs, $this->aScannedDirs);
try {
ModuleDiscovery::GetModulesOrderedByDependencies($aSearchDirs, true);
ModuleDiscovery::GetAvailableModules($aSearchDirs, true);
} catch (MissingDependencyException $e) {
// Some modules have missing dependencies
// Let's check what is the impact at the "extensions" level
@@ -369,75 +503,6 @@ class iTopExtensionsMap
return array_merge($this->aInstalledExtensions ?? [], $this->aExtensions);
}
/**
* @param bool $bKeepMissingDependencyExtensions
*
* @return array<\iTopExtension>>
*/
public function GetAllExtensionsToDisplayInSetup(bool $bKeepMissingDependencyExtensions = false): array
{
$aRes = [];
foreach ($this->GetAllExtensionsWithPreviouslyInstalled() as $oExtension) {
/** @var \iTopExtension $oExtension */
if (($oExtension->sSource !== iTopExtension::SOURCE_WIZARD) && ($oExtension->bVisible)) {
if ($bKeepMissingDependencyExtensions || (count($oExtension->aMissingDependencies) == 0)) {
if (!$oExtension->bMandatory) {
$oExtension->bMandatory = ($oExtension->sSource === iTopExtension::SOURCE_REMOTE);
}
$aRes[$oExtension->sCode] = $oExtension;
}
}
}
return $aRes;
}
public function GetAllExtensionsOptionInfo(): array
{
$aRes = [];
foreach ($this->GetAllExtensionsToDisplayInSetup() as $sCode => $oExtension) {
$aRes[] = [
'extension_code' => $oExtension->sCode,
'title' => $oExtension->sLabel,
'description' => $oExtension->sDescription,
'more_info' => $oExtension->sMoreInfoUrl,
'default' => true, // by default offer to install all modules
'modules' => $oExtension->aModules,
'mandatory' => $oExtension->bMandatory,
'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource),
'uninstallable' => $oExtension->CanBeUninstalled(),
'missing' => $oExtension->bRemovedFromDisk,
];
}
return $aRes;
}
protected function GetExtensionSourceLabel($sSource)
{
$sDecorationClass = '';
switch ($sSource) {
case iTopExtension::SOURCE_MANUAL:
$sResult = 'Local extensions folder';
$sDecorationClass = 'fas fa-folder';
break;
case iTopExtension::SOURCE_REMOTE:
$sResult = (ITOP_APPLICATION == 'iTop') ? 'iTop Hub' : 'ITSM Designer';
$sDecorationClass = (ITOP_APPLICATION == 'iTop') ? 'fc fc-chameleon-icon' : 'fa pencil-ruler';
break;
default:
$sResult = '';
}
if ($sResult == '') {
return '';
}
return '<i class="setup-extension--icon '.$sDecorationClass.'" data-tooltip-content="'.$sResult.'"></i>';
}
/**
* Mark the given extension as chosen
* @param string $sExtensionCode The code of the extension (code without version number)
@@ -523,7 +588,7 @@ class iTopExtensionsMap
return true;
}
public function LoadInstalledExtensionsFromDatabase(Config $oConfig): array|false
protected function LoadInstalledExtensionsFromDatabase(Config $oConfig): array|false
{
try {
if (CMDBSource::DBName() === null) {
@@ -566,27 +631,6 @@ class iTopExtensionsMap
}
}
public static function GetChoicesFromDatabase(Config $oConfig): array|false
{
try {
if (CMDBSource::DBName() === null) {
CMDBSource::InitFromConfig($oConfig);
}
$sLatestInstallationDate = CMDBSource::QueryToScalar("SELECT max(installed) FROM ".$oConfig->Get('db_subname')."priv_extension_install");
$aDBInfo = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_extension_install WHERE installed = '".$sLatestInstallationDate."'");
$aChoices = [];
foreach ($aDBInfo as $aExtensionInfo) {
$aChoices[] = $aExtensionInfo['code'];
}
return $aChoices;
} catch (MySQLException $e) {
// No database or erroneous information
return false;
}
}
/**
* Tells if the given module name is "chosen" since it is part of a "chosen" extension (in the specified source dir)
* @param string $sModuleNameToFind

View File

@@ -1,105 +0,0 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use ContextTag;
use DBObjectSearch;
use DBObjectSet;
use IssueLog;
use MetaModel;
use SetupLog;
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
abstract class AbstractSetupAudit
{
protected bool $bClassesInitialized = false;
protected array $aClassesBefore = [];
protected array $aClassesAfter = [];
protected array $aRemovedClasses = [];
protected array $aFinalClassesToCleanup = [];
public function __construct()
{
}
abstract public function ComputeClasses(): void;
public function GetRemovedClasses(): array
{
$this->ComputeClasses();
if (count($this->aRemovedClasses) == 0) {
if (count($this->aClassesBefore) == 0) {
return $this->aRemovedClasses;
}
if (count($this->aClassesAfter) == 0) {
return $this->aRemovedClasses;
}
$aExtensionsNames = array_diff($this->aClassesBefore, $this->aClassesAfter);
$this->aRemovedClasses = [];
$aClasses = array_values($aExtensionsNames);
sort($aClasses);
foreach ($aClasses as $i => $sClass) {
$this->aRemovedClasses[] = $sClass;
}
}
return $this->aRemovedClasses;
}
public function GetIssues(bool $bStopDataCheckAtFirstIssue = false): array
{
$this->aFinalClassesToCleanup = [];
foreach ($this->GetRemovedClasses() as $sClass) {
if (MetaModel::IsAbstract($sClass)) {
continue;
}
if (!MetaModel::IsStandaloneClass($sClass)) {
$iCount = $this->Count($sClass);
$this->aFinalClassesToCleanup[$sClass] = $iCount;
if ($bStopDataCheckAtFirstIssue && $iCount > 0) {
//setup envt: should raise issue ASAP
$this->LogInfoWithProperLogger("Setup audit found data to cleanup", null, $this->aFinalClassesToCleanup);
return $this->aFinalClassesToCleanup;
}
}
}
$this->LogInfoWithProperLogger("Setup audit found data to cleanup", null, ['data_to_cleanup' => $this->aFinalClassesToCleanup]);
return $this->aFinalClassesToCleanup;
}
public function GetDataToCleanupCount(): int
{
$res = 0;
foreach ($this->aFinalClassesToCleanup as $sClass => $iCount) {
$res += $iCount;
}
return $res;
}
private function Count($sClass): int
{
$oSearch = DBObjectSearch::FromOQL("SELECT $sClass", []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
return $oSet->Count();
}
//could be shared with others in log APIs ?
private function LogInfoWithProperLogger($sMessage, $sChannel = null, $aContext = []): void
{
if (ContextTag::Check(ContextTag::TAG_SETUP)) {
SetupLog::Info($sMessage, $sChannel, $aContext);
} else {
IssueLog::Info($sMessage, $sChannel, $aContext);
}
}
}

View File

@@ -2,21 +2,16 @@
namespace Combodo\iTop\Setup\FeatureRemoval;
use Combodo\iTop\Setup\ModuleDependency\Module;
use Config;
use InstallationChoicesToModuleConverter;
use iTopExtensionsMap;
use MetaModel;
use ModuleDiscovery;
use RunTimeEnvironment;
use SetupUtils;
use utils;
class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
{
public const DRY_REMOVAL_AUDIT_ENV = "extension-removal";
protected array $aExtensionsByCode;
private bool $bExtensionMapModified = false;
/**
* Toolset for building a run-time environment
@@ -42,54 +37,33 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
$sEnv = $this->sFinalEnv;
$this->aExtensionsByCode = $aExtensionCodesToRemove;
//SetupUtils::rrmdir(APPROOT."/data/$sEnv-modules");
$this->Cleanup();
SetupUtils::copydir(APPROOT."/data/$sSourceEnv-modules", APPROOT."/data/$sEnv-modules");
$this->DeclareExtensionAsRemoved($aExtensionCodesToRemove);
if (count($aExtensionCodesToRemove) > 0) {
$this->RemoveExtensionsLocally($aExtensionCodesToRemove);
}
$oDryRemovalConfig = clone(MetaModel::GetConfig());
$oDryRemovalConfig->ChangeModulesPath($sSourceEnv, $this->sFinalEnv);
$this->WriteConfigFileSafe($oDryRemovalConfig);
$sSourceDir = $oDryRemovalConfig->Get('source_dir');
$aSearchDirs = $this->GetExtraDirsToCompile($sSourceDir);
$aModulesToLoad = $this->GetModulesToLoad($sSourceEnv, $aSearchDirs);
try {
ModuleDiscovery::GetModulesOrderedByDependencies($aSearchDirs, true, $aModulesToLoad);
} catch (\MissingDependencyException $e) {
\IssueLog::Error("Cannot prepare setup due to dependency issue", null, ['msg' => $e->getMessage(), 'modules_to_load' => $aModulesToLoad]);
throw $e;
}
}
private function DeclareExtensionAsRemoved(array $aExtensionCodes): void
private function RemoveExtensionsLocally(array $aExtensionCodes): void
{
$oExtensionsMap = new iTopExtensionsMap($this->sFinalEnv);
$oExtensionsMap->DeclareExtensionAsRemoved($aExtensionCodes);
}
$oExtensionsMap = new \iTopExtensionsMap($this->sFinalEnv);
private function GetModulesToLoad(string $sSourceEnv, $aSearchDirs): array
{
$oSourceConfig = new Config(utils::GetConfigFilePath($sSourceEnv));
$aChoices = iTopExtensionsMap::GetChoicesFromDatabase($oSourceConfig);
$sSourceDir = $oSourceConfig->Get('source_dir');
$sInstallFilePath = APPROOT.$sSourceDir.'/installation.xml';
if (! is_file($sInstallFilePath)) {
$sInstallFilePath = null;
foreach ($aExtensionCodes as $sCode) {
/** @var \iTopExtension $oExtension */
$oExtension = $oExtensionsMap->GetFromExtensionCode($sCode);
if (!is_null($oExtension)) {
$sDir = $oExtension->sSourceDir;
\IssueLog::Info(__METHOD__.": remove extension locally", null, [$oExtension->sCode => $sDir]);
SetupUtils::rrmdir($sDir);
} else {
\IssueLog::Warning(__METHOD__." cannot find extensions", null, ['env' => $this->sFinalEnv, 'code' => $sCode]);
}
}
$aModuleIdsToLoad = InstallationChoicesToModuleConverter::GetInstance()->GetModules($aChoices, $aSearchDirs, $sInstallFilePath);
$aModulesToLoad = [];
foreach ($aModuleIdsToLoad as $sModuleId) {
$oModule = new Module($sModuleId);
$sModuleName = $oModule->GetModuleName();
$aModulesToLoad[] = $sModuleName;
}
return $aModulesToLoad;
}
public function Cleanup()
@@ -101,4 +75,23 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
SetupUtils::rrmdir(APPROOT."/conf/$sEnv");
@unlink(APPROOT."/data/datamodel-$sEnv.xml");
}
/**
* @return \iTopExtensionsMap|null
*/
/*protected function GetExtensionMap(): ?iTopExtensionsMap
{
if (is_null(parent::GetExtensionMap())) {
return null;
}
if (!$this->bExtensionMapModified) {
$this->bExtensionMapModified = true;
foreach ($this->aExtensionsByCode as $sCode) {
parent::GetExtensionMap()->RemoveExtension($sCode);
}
}
return parent::GetExtensionMap();
}*/
}

View File

@@ -1,40 +0,0 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use MetaModel;
require_once __DIR__.'/AbstractSetupAudit.php';
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
class InplaceSetupAudit extends AbstractSetupAudit
{
//file used when present to trigger audit exception when testing specific setups
public const GETISSUE_ERROR_MSG_FILE_FORTESTONLY = '.setup_audit_error_msg.txt';
private string $sEnvAfter;
public function __construct(array $aClassesBefore, string $sEnvAfter)
{
parent::__construct();
$this->aClassesBefore = $aClassesBefore;
$this->sEnvAfter = $sEnvAfter;
}
public function ComputeClasses(): void
{
if ($this->bClassesInitialized) {
return;
}
$sCurrentEnvt = MetaModel::GetEnvironment();
if ($sCurrentEnvt === $this->sEnvAfter) {
$this->aClassesAfter = MetaModel::GetClasses();
} else {
$this->aClassesAfter = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvAfter);
}
$this->bClassesInitialized = true;
}
}

View File

@@ -2,13 +2,8 @@
namespace Combodo\iTop\Setup\FeatureRemoval;
use ContextTag;
use CoreException;
use Exception;
use IssueLog;
use MetaModel;
use SetupLog;
use utils;
class ModelReflectionSerializer
{
@@ -34,53 +29,27 @@ class ModelReflectionSerializer
public function GetModelFromEnvironment(string $sEnv): array
{
IssueLog::Info(__METHOD__, null, ['env' => $sEnv]);
$sCurrentEnvt = MetaModel::GetEnvironment();
if ($sCurrentEnvt === $sEnv) {
$aClasses = MetaModel::GetClasses();
if (count($aClasses) === 0) {
//MetaModel not started yet
$sConfFile = utils::GetConfigFilePath($sEnv);
MetaModel::Startup($sConfFile, false /* $bModelOnly */, false /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
$aClasses = MetaModel::GetClasses();
}
return $aClasses;
}
$sPHPExec = trim(utils::GetConfig()->Get('php_path'));
\IssueLog::Info(__METHOD__, null, ['env' => $sEnv]);
$sPHPExec = trim(\MetaModel::GetConfig()->Get('php_path'));
$sOutput = "";
$iRes = 0;
$sCommandLine = sprintf("$sPHPExec %s/get_model_reflection.php --env=%s", __DIR__, escapeshellarg($sEnv));
exec($sCommandLine, $sOutput, $iRes);
exec(sprintf("$sPHPExec %s/get_model_reflection.php --env='%s'", __DIR__, $sEnv), $sOutput, $iRes);
if ($iRes != 0) {
$this->LogErrorWithProperLogger("Cannot get classes", null, ['env' => $sEnv, 'code' => $iRes, "output" => $sOutput]);
throw new CoreException("Cannot get classes from env ".$sEnv);
\IssueLog::Error("Cannot get classes", null, ['env' => $sEnv, 'code' => $iRes, "output" => $sOutput]);
throw new CoreException("Cannot get classes");
}
$aClasses = json_decode($sOutput[0] ?? null, true);
if (false === $aClasses) {
$this->LogErrorWithProperLogger("Invalid JSON", null, ['env' => $sEnv, "output" => $sOutput]);
\IssueLog::Error("Invalid JSON", null, ["output" => $sOutput]);
throw new Exception("cannot get classes");
}
if (!is_array($aClasses)) {
$this->LogErrorWithProperLogger("not an array", null, ['env' => $sEnv, "classes" => $aClasses, "output" => $sOutput]);
throw new Exception("cannot get classes from $sEnv");
\IssueLog::Error("not an array", null, ["classes" => $aClasses]);
throw new Exception("cannot get classes");
}
return $aClasses;
}
//could be shared with others in log APIs ?
private function LogErrorWithProperLogger($sMessage, $sChannel = null, $aContext = []): void
{
if (ContextTag::Check(ContextTag::TAG_SETUP)) {
SetupLog::Error($sMessage, $sChannel, $aContext);
} else {
IssueLog::Error($sMessage, $sChannel, $aContext);
}
}
}

View File

@@ -2,50 +2,70 @@
namespace Combodo\iTop\Setup\FeatureRemoval;
require_once __DIR__.'/AbstractSetupAudit.php';
use DBObjectSearch;
use DBObjectSet;
use MetaModel;
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
class SetupAudit extends AbstractSetupAudit
class SetupAudit
{
//file used when present to trigger audit exception when testing specific setups
public const GETISSUE_ERROR_MSG_FILE_FORTESTONLY = '.setup_audit_error_msg.txt';
private string $sEnvBefore;
private string $sEnvAfter;
private string $sEnvBeforeExtensionRemoval;
private string $sEnvAfterExtensionRemoval;
public function __construct(string $sEnvBefore, string $sEnvAfter)
{
parent::__construct();
$this->sEnvBefore = $sEnvBefore;
$this->sEnvAfter = $sEnvAfter;
}
private array $aClassesBeforeRemoval;
private array $aClassesAfterRemoval;
private array $aRemovedClasses;
private array $aFinalClassesRemoved;
public function ComputeClasses(): void
public function __construct(string $sEnvBeforeExtensionRemoval, string $sEnvAfterExtensionRemoval = DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV)
{
if ($this->bClassesInitialized) {
return;
$this->sEnvBeforeExtensionRemoval = $sEnvBeforeExtensionRemoval;
$this->sEnvAfterExtensionRemoval = $sEnvAfterExtensionRemoval;
$sCurrentEnvt = MetaModel::GetEnvironment();
if ($sCurrentEnvt === $this->sEnvBeforeExtensionRemoval) {
$this->aClassesBeforeRemoval = MetaModel::GetClasses();
} else {
$this->aClassesBeforeRemoval = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvBeforeExtensionRemoval);
}
$this->aClassesBefore = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvBefore);
$this->aClassesAfter = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvAfter);
if ($sCurrentEnvt === $this->sEnvAfterExtensionRemoval) {
$this->aClassesAfterRemoval = MetaModel::GetClasses();
} else {
$this->aClassesAfterRemoval = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvAfterExtensionRemoval);
}
$this->bClassesInitialized = true;
$this->aRemovedClasses = [];
$this->aFinalClassesRemoved = [];
}
/*public function SetSelectedExtensions(Config $oConfig, array $aSelectedExtensions)
{
$oExtensionsMap = new \iTopExtensionsMap();
$oExtensionsMap->LoadChoicesFromDatabase($oConfig);
sort($aSelectedExtensions);
$this->aExtensionToRemove = $oExtensionsMap->GetMissingExtensions($aSelectedExtensions);
sort($this->aExtensionToRemove);
\SetupLog::Info(__METHOD__, null, ['aExtensionToRemove' => $this->aExtensionToRemove]);
}*/
public function GetRemovedClasses(): array
{
$this->ComputeClasses();
if (count($this->aRemovedClasses) == 0) {
if (count($this->aClassesBefore) == 0) {
if (count($this->aClassesBeforeRemoval) == 0) {
return $this->aRemovedClasses;
}
if (count($this->aClassesAfter) == 0) {
if (count($this->aClassesAfterRemoval) == 0) {
return $this->aRemovedClasses;
}
$aExtensionsNames = array_diff($this->aClassesBefore, $this->aClassesAfter);
$aExtensionsNames = array_diff($this->aClassesBeforeRemoval, $this->aClassesAfterRemoval);
$this->aRemovedClasses = [];
$aClasses = array_values($aExtensionsNames);
sort($aClasses);
@@ -57,4 +77,50 @@ class SetupAudit extends AbstractSetupAudit
return $this->aRemovedClasses;
}
/** test only: return file path that force audit error being raised
*
* @return string
*/
public static function GetErrorMessageFilePathForTestOnly(): string
{
return APPROOT."/data/".self::GETISSUE_ERROR_MSG_FILE_FORTESTONLY;
}
public function GetIssues(bool $bThrowExceptionAtFirstIssue = false): array
{
$sErrorMessageFilePath = self::GetErrorMessageFilePathForTestOnly();
if ($bThrowExceptionAtFirstIssue && is_file($sErrorMessageFilePath)) {
$sMsg = file_get_contents($sErrorMessageFilePath);
throw new \Exception($sMsg);
}
$this->aFinalClassesRemoved = [];
foreach ($this->GetRemovedClasses() as $sClass) {
if (MetaModel::IsAbstract($sClass)) {
continue;
}
if (!MetaModel::IsStandaloneClass($sClass)) {
$iCount = $this->Count($sClass);
$this->aFinalClassesRemoved[$sClass] = $iCount;
if ($bThrowExceptionAtFirstIssue && $iCount > 0) {
//setup envt: should raise issue ASAP
throw new \Exception($sClass);
}
}
}
return $this->aFinalClassesRemoved;
}
private function Count($sClass): int
{
$oSearch = DBObjectSearch::FromOQL("SELECT $sClass", []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
return $oSet->Count();
}
}

View File

@@ -19,7 +19,7 @@ if (is_null($sEnv)) {
$sConfFile = utils::GetConfigFilePath($sEnv);
try {
MetaModel::Startup($sConfFile, false /* $bModelOnly */, false /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
MetaModel::Startup($sConfFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
} catch (\Throwable $e) {
echo $e->getMessage();
echo $e->getTraceAsString();

View File

@@ -1,144 +0,0 @@
<?php
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
require_once(APPROOT.'/setup/parameters.class.inc.php');
require_once(APPROOT.'/core/cmdbsource.class.inc.php');
require_once(APPROOT.'/setup/modulediscovery.class.inc.php');
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
/**
* Basic helper class to describe an extension, with some characteristics and a list of modules
*/
class iTopExtension
{
public const SOURCE_WIZARD = 'datamodels';
public const SOURCE_MANUAL = 'extensions';
public const SOURCE_REMOTE = 'data';
/**
* @var string
*/
public $sCode;
/**
* @var string
*/
public $sVersion;
/**
* @var string
*/
public $sInstalledVersion;
/**
* @var string
*/
public $sLabel;
/**
* @var string
*/
public $sDescription;
/**
* @var string
*/
public $sSource;
/**
* @var bool
*/
public $bMandatory;
/**
* @var string
*/
public $sMoreInfoUrl;
/**
* @var bool
*/
public $bMarkedAsChosen;
/**
* If null, check if at least one module cannot be uninstalled
* @var bool|null
*/
public ?bool $bCanBeUninstalled = null;
/**
* @var bool
*/
public $bVisible;
/**
* @var string[]
*/
public $aModules;
/**
* @var string[]
*/
public $aModuleVersion;
/**
* @var string[]
*/
public $aModuleInfo;
/**
* @var string
*/
public $sSourceDir;
/**
*
* @var string[]
*/
public $aMissingDependencies;
/**
* @var bool
*/
public bool $bInstalled = false;
/**
* @var bool
*/
public bool $bRemovedFromDisk = false;
public function __construct()
{
$this->sCode = '';
$this->sLabel = '';
$this->sDescription = '';
$this->sSource = self::SOURCE_WIZARD;
$this->bMandatory = false;
$this->sMoreInfoUrl = '';
$this->bMarkedAsChosen = false;
$this->sVersion = ITOP_VERSION;
$this->sInstalledVersion = '';
$this->aModules = [];
$this->aModuleVersion = [];
$this->aModuleInfo = [];
$this->sSourceDir = '';
$this->bVisible = true;
$this->aMissingDependencies = [];
}
/**
* @since 3.3.0
* @return bool
*/
public function CanBeUninstalled(): bool
{
if (!is_null($this->bCanBeUninstalled)) {
return $this->bCanBeUninstalled;
}
foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) {
if ($aModuleInfo['uninstallable'] !== 'yes') {
return false;
}
}
return true;
}
}

View File

@@ -1801,7 +1801,7 @@ EOF
*/
public function FindModules()
{
$aAvailableModules = ModuleDiscovery::GetModulesOrderedByDependencies($this->aRootDirs);
$aAvailableModules = ModuleDiscovery::GetAvailableModules($this->aRootDirs);
$aResult = [];
foreach ($aAvailableModules as $sId => $aModule) {
$oModule = new MFModule($sId, $aModule['root_dir'], $aModule['label'], isset($aModule['auto_select']));

View File

@@ -5,7 +5,7 @@ namespace Combodo\iTop\Setup\ModuleDependency;
require_once(APPROOT.'/setup/runtimeenv.class.inc.php');
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
use ModuleFileReaderException;
use RunTimeEnvironment;
/**
@@ -61,19 +61,19 @@ class DependencyExpression
}
}
public static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
{
if (!isset(self::$oPhpExpressionEvaluator)) {
self::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
if (!isset(static::$oPhpExpressionEvaluator)) {
static::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
}
return self::$oPhpExpressionEvaluator;
return static::$oPhpExpressionEvaluator;
}
/**
* Return module names potentially required by current dependency
*
* @return array<string>
* @return array
*/
public function GetRemainingModuleNamesToResolve(): array
{

View File

@@ -114,7 +114,7 @@ class Module
}
/**
* @return array<string> list of unique module names
* @return array: list of unique module names
*/
public function GetUnresolvedDependencyModuleNames(): array
{

View File

@@ -19,16 +19,16 @@ class ModuleDependencySort
final public static function GetInstance(): ModuleDependencySort
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModuleDependencySort();
if (!isset(static::$oInstance)) {
static::$oInstance = new static();
}
return self::$oInstance;
return static::$oInstance;
}
final public static function SetInstance(?ModuleDependencySort $oInstance): void
{
self::$oInstance = $oInstance;
static::$oInstance = $oInstance;
}
/**
@@ -168,7 +168,7 @@ class ModuleDependencySort
foreach ($aCountDepsByModuleId as $sModuleId => $iInDegreeCounter) {
$oModule = $aUnresolvedDependencyModules[$sModuleId];
if ($bOneLoopAtLeast && ($iInDegreeCounter > 0)) {
if ($bOneLoopAtLeast && $iInDegreeCounter > 0) {
break;
}

View File

@@ -22,13 +22,13 @@
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use Combodo\iTop\Setup\ModuleDependency\Module;
use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
require_once(APPROOT.'setup/modulediscovery/ModuleFileReader.php');
require_once(__DIR__.'/moduledependency/moduledependencysort.class.inc.php');
require_once(__DIR__.'/itopextension.class.inc.php');
use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort;
class MissingDependencyException extends CoreException
{
@@ -95,9 +95,6 @@ class ModuleDiscovery
protected static $m_aModules = [];
protected static $m_aModuleVersionByName = [];
/** @var array<\iTopExtension> $m_aRemovedExtensions */
protected static array $m_aRemovedExtensions = [];
// All the entries below are list of file paths relative to the module directory
protected static $m_aFilesList = ['datamodel', 'webservice', 'dictionary', 'data.struct', 'data.sample'];
@@ -134,10 +131,6 @@ class ModuleDiscovery
list($sModuleName, $sModuleVersion) = static::GetModuleName($sId);
if (self::IsModuleInExtensionList(self::$m_aRemovedExtensions, $sModuleName, $sModuleVersion, $aArgs)) {
return;
}
if (array_key_exists($sModuleName, self::$m_aModuleVersionByName)) {
if (version_compare($sModuleVersion, self::$m_aModuleVersionByName[$sModuleName]['version'], '>')) {
// Newer version, let's upgrade
@@ -196,6 +189,21 @@ class ModuleDiscovery
}
}
/**
* Get the list of "discovered" modules, ordered based on their (inter) dependencies
*
* @param bool $bAbortOnMissingDependency ...
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
*
* @return array
* @throws \MissingDependencyException
*/
protected static function GetModules($bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
// Order the modules to take into account their inter-dependencies
return self::OrderModulesByDependencies(self::$m_aModules, $bAbortOnMissingDependency, $aModulesToLoad);
}
/**
* Arrange an list of modules, based on their (inter) dependencies
* @param array $aModules The list of modules to process: 'id' => $aModuleInfo
@@ -206,20 +214,15 @@ class ModuleDiscovery
*/
public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
if (is_null($aModulesToLoad) && count(self::$m_aRemovedExtensions) === 0) {
if (is_null($aModulesToLoad)) {
$aFilteredModules = $aModules;
} else {
$aFilteredModules = [];
foreach ($aModules as $sModuleId => $aModuleInfo) {
foreach ($aModules as $sModuleId => $aModule) {
$oModule = new Module($sModuleId);
$sModuleName = $oModule->GetModuleName();
if (self::IsModuleInExtensionList(self::$m_aRemovedExtensions, $sModuleName, $oModule->GetVersion(), $aModuleInfo)) {
continue;
}
if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad)) {
$aFilteredModules[$sModuleId] = $aModuleInfo;
if (in_array($sModuleName, $aModulesToLoad)) {
$aFilteredModules[$sModuleId] = $aModule;
}
}
}
@@ -227,19 +230,27 @@ class ModuleDiscovery
return ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aFilteredModules, $bAbortOnMissingDependency);
}
/**
* @param array<\iTopExtension> $aRemovedExtension
* @return void
*/
public static function DeclareRemovedExtensions(array $aRemovedExtension): void
private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
{
if (self::$m_aRemovedExtensions != $aRemovedExtension) {
self::ResetCache();
if (!isset(static::$oPhpExpressionEvaluator)) {
static::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
}
self::$m_aRemovedExtensions = $aRemovedExtension;
return static::$oPhpExpressionEvaluator;
}
private static function Init($aSearchDirs): void
/**
* Search (on the disk) for all defined iTop modules, load them and returns the list (as an array)
* of the possible iTop modules to install
*
* @param $aSearchDirs array of directories to search (absolute paths)
* @param bool $bAbortOnMissingDependency ...
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
*
* @return array A big array moduleID => ModuleData
* @throws \Exception
*/
public static function GetAvailableModules($aSearchDirs, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
if (self::$m_aSearchDirs != $aSearchDirs) {
self::ResetCache();
@@ -258,60 +269,13 @@ class ModuleDiscovery
clearstatcache();
self::ListModuleFiles(basename($sSearchDir), dirname($sSearchDir));
}
return self::GetModules($bAbortOnMissingDependency, $aModulesToLoad);
} else {
// Reuse the previous results
return self::GetModules($bAbortOnMissingDependency, $aModulesToLoad);
}
}
/**
* Return all modules found on disk ordered by dependencies. Skipping modules coming from extensions declared as removed (@see ModuleDiscovery::DeclareRemovedExtensions)
* @param $aSearchDirs array of directories to search (absolute paths)
* @param bool $bAbortOnMissingDependency ...
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
*
* @return array A big array moduleID => ModuleData
* @throws \Exception
*/
public static function GetModulesOrderedByDependencies($aSearchDirs, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
self::Init($aSearchDirs);
return self::OrderModulesByDependencies(self::$m_aModules, $bAbortOnMissingDependency, $aModulesToLoad);
}
/**
* @deprecated use \ModuleDiscovery::GetModulesOrderedByDependencies instead
*/
public static function GetAvailableModules($aSearchDirs, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
return ModuleDiscovery::GetModulesOrderedByDependencies($aSearchDirs, $bAbortOnMissingDependency, $aModulesToLoad);
}
/**
* Return all modules found on disk (without any dependency consideration). Skipping modules coming from extensions declared as removed (@see ModuleDiscovery::DeclareRemovedExtensions)
*
* @param $aSearchDirs array of directories to search (absolute paths)
*
* @return array A big array moduleID => ModuleData
* @throws \Exception
*/
public static function GetAllModules($aSearchDirs)
{
self::Init($aSearchDirs);
$aNonRemovedModules = [];
foreach (self::$m_aModules as $sModuleId => $aModuleInfo) {
$oModule = new Module($sModuleId);
$sModuleName = $oModule->GetModuleName();
if (self::IsModuleInExtensionList(self::$m_aRemovedExtensions, $sModuleName, $oModule->GetVersion(), $aModuleInfo)) {
continue;
}
$aNonRemovedModules[$sModuleId] = $aModuleInfo;
}
return $aNonRemovedModules;
}
public static function ResetCache()
{
self::$m_aSearchDirs = null;
@@ -321,12 +285,10 @@ class ModuleDiscovery
/**
* Helper function to interpret the name of a module
*
* @param $sModuleId string Identifier of the module, in the form 'name/version'
*
* @return array of 2 elements (name, version)
* @return array(name, version)
*/
public static function GetModuleName($sModuleId): array
public static function GetModuleName($sModuleId)
{
$aMatches = [];
if (preg_match('!^(.*)/(.*)$!', $sModuleId, $aMatches)) {
@@ -380,59 +342,6 @@ class ModuleDiscovery
throw new Exception("Data directory (".$sDirectory.") not found or not readable.");
}
}
/**
* @param array<\iTopExtension> $aExtensions
* @param string $sModuleName
* @param string $sModuleVersion
* @param array $aModuleInfo
*
* @return bool
*/
private static function IsModuleInExtensionList(array $aExtensions, string $sModuleName, string $sModuleVersion, array $aModuleInfo): bool
{
if (count($aExtensions) === 0) {
return false;
}
$aNonMatchingPaths = [];
$sModuleFilePath = $aModuleInfo[ModuleFileReader::MODULE_FILE_PATH];
/** @var \iTopExtension $oExtension */
foreach ($aExtensions as $oExtension) {
$sCurrentVersion = $oExtension->aModuleVersion[$sModuleName] ?? null;
if (is_null($sCurrentVersion)) {
continue;
}
if ($sModuleVersion !== $sCurrentVersion) {
continue;
}
$aCurrentModuleInfo = $oExtension->aModuleInfo[$sModuleName] ?? null;
if (is_null($aCurrentModuleInfo)) {
SetupLog::Warning("Missing $sModuleName in ".$oExtension->sLabel.". it should not happen");
continue;
}
// use case: same module coming from 2 different extensions
// we remove only the one coming from removed extensions
$sCurrentModuleFilePath = $aCurrentModuleInfo[ModuleFileReader::MODULE_FILE_PATH];
if (realpath($sModuleFilePath) !== realpath($sCurrentModuleFilePath)) {
$aNonMatchingPaths[] = $sCurrentModuleFilePath;
continue;
}
SetupLog::Info("Module considered as removed", null, ['extension_code' => $oExtension->sCode, 'module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sCurrentModuleFilePath]);
return true;
}
if (count($aNonMatchingPaths) > 0) {
//add log for support
SetupLog::Debug("Module kept as it came from non removed extensions", null, ['module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath, 'non_matching_paths' => $aNonMatchingPaths]);
}
return false;
}
} // End of class
/** Alias for backward compatibility with old module files in which

View File

@@ -7,15 +7,15 @@ use CoreException;
use Exception;
use ParseError;
use PhpParser\Error;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\ElseIf_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\If_;
use PhpParser\ParserFactory;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Stmt\ElseIf_;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Arg;
require_once __DIR__.'/ModuleFileReaderException.php';
require_once APPROOT.'sources/PhpParser/Evaluation/PhpExpressionEvaluator.php';
@@ -36,7 +36,6 @@ class ModuleFileReader
public const MODULE_INFO_PATH = 0;
public const MODULE_INFO_ID = 1;
public const MODULE_INFO_CONFIG = 2;
public const MODULE_FILE_PATH = "module_file_path";
public const STATIC_CALLWHITELIST = [
"utils::GetItopVersionWikiSyntax",
@@ -49,23 +48,21 @@ class ModuleFileReader
final public static function GetInstance(): ModuleFileReader
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModuleFileReader();
if (!isset(static::$oInstance)) {
static::$oInstance = new static();
}
return self::$oInstance;
return static::$oInstance;
}
final public static function SetInstance(?ModuleFileReader $oInstance): void
{
self::$oInstance = $oInstance;
static::$oInstance = $oInstance;
}
/**
* Read the information from a module file (module.xxx.php)
*
* @param string $sModuleFilePath
*
* @param string $sModuleFile
* @return array
* @throws ModuleFileReaderException
*/
@@ -111,9 +108,7 @@ class ModuleFileReader
* Read the information from a module file (module.xxx.php)
* Warning: this method is using eval() function to load the ModuleInstallerAPI classes.
* Current method is never called at design/runtime. It is acceptable to use it during setup only.
*
* @param string $sModuleFilePath
*
* @param string $sModuleFile
* @return array
* @throws ModuleFileReaderException
*/
@@ -169,7 +164,7 @@ class ModuleFileReader
private function CompleteModuleInfoWithFilePath(array &$aModuleInfo)
{
if (count($aModuleInfo) == 3) {
$aModuleInfo[static::MODULE_INFO_CONFIG][self::MODULE_FILE_PATH] = $aModuleInfo[static::MODULE_INFO_PATH];
$aModuleInfo[static::MODULE_INFO_CONFIG]['module_file_path'] = $aModuleInfo[static::MODULE_INFO_PATH];
}
}
@@ -180,21 +175,15 @@ class ModuleFileReader
}
$sModuleInstallerClass = $aModuleInfo['installer'];
if (strlen($sModuleInstallerClass) === 0) {
return null;
}
if (!class_exists($sModuleInstallerClass)) {
$sModuleFilePath = $aModuleInfo[self::MODULE_FILE_PATH];
$sModuleFilePath = $aModuleInfo['module_file_path'];
$this->ReadModuleFileInformationUnsafe($sModuleFilePath);
}
if (!class_exists($sModuleInstallerClass)) {
\IssueLog::Error(__METHOD__, null, $aModuleInfo);
throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleInfo['label']);
}
if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) {
\IssueLog::Error(__METHOD__, null, $aModuleInfo);
throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleInfo['label']);
}
@@ -203,7 +192,7 @@ class ModuleFileReader
/**
* @param string $sModuleFilePath
* @param \PhpParser\Node\Stmt\Expression $oExpression
* @param \PhpParser\Node\Expr\Assign $oAssignation
*
* @return array|null
* @throws ModuleFileReaderException

View File

@@ -1,215 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Setup\ModuleDependency\DependencyExpression;
require_once __DIR__.'/ModuleInstallationException.php';
require_once(APPROOT.'/setup/moduledependency/module.class.inc.php');
class InstallationChoicesToModuleConverter
{
private static ?InstallationChoicesToModuleConverter $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): InstallationChoicesToModuleConverter
{
if (!isset(self::$oInstance)) {
self::$oInstance = new InstallationChoicesToModuleConverter();
}
return self::$oInstance;
}
final public static function SetInstance(?InstallationChoicesToModuleConverter $oInstance): void
{
self::$oInstance = $oInstance;
}
/**
* @param array $aInstallationChoices
* @param array $aSearchDirs
*
* @return array
* @throws \ModuleInstallationException
*/
public function GetModules(array $aInstallationChoices, array $aSearchDirs, ?string $sInstallationFilePath = null): array
{
$aPackageModules = ModuleDiscovery::GetAllModules($aSearchDirs);
$bInstallationFileProvided = ! is_null($sInstallationFilePath) && is_file($sInstallationFilePath);
if ($bInstallationFileProvided) {
$oXMLParameters = new XMLParameters($sInstallationFilePath);
$aSteps = $oXMLParameters->Get('steps', []);
if (!is_array($aSteps)) {
return [];
}
$aInstalledModuleNames = $this->FindInstalledPackageModules($aPackageModules, $aInstallationChoices, $aSteps);
} else {
$aInstalledModuleNames = $this->FindInstalledPackageModules($aPackageModules, $aInstallationChoices);
}
$aInstalledModules = [];
foreach (array_keys($aPackageModules) as $sModuleId) {
list($sModuleName) = ModuleDiscovery::GetModuleName($sModuleId);
if (in_array($sModuleName, $aInstalledModuleNames)) {
$aInstalledModules[] = $sModuleId;
}
}
return $aInstalledModules;
}
private function FindInstalledPackageModules(array $aPackageModules, array $aInstallationChoices, array $aInstallationDescription = null): array
{
$aInstalledModules = [];
$this->ProcessDefaultModules($aPackageModules, $aInstalledModules);
if (is_null($aInstallationDescription)) {
//in legacy usecase: choices are flat modules list already
foreach ($aInstallationChoices as $sModuleName) {
$aInstalledModules[$sModuleName] = true;
}
} else {
$this->GetModuleNamesFromInstallationChoices($aInstallationChoices, $aInstallationDescription, $aInstalledModules);
}
$this->ProcessAutoSelectModules($aPackageModules, $aInstalledModules);
return array_keys($aInstalledModules);
}
private function IsDefaultModule(string $sModuleId, array $aModule): bool
{
if (($sModuleId === ROOT_MODULE)) {
return false;
}
if (isset($aModule['auto_select'])) {
return false;
}
if ($aModule['category'] === 'authentication') {
return true;
}
return !$aModule['visible'];
}
private function ProcessDefaultModules(array &$aPackageModules, array &$aInstalledModules): void
{
foreach ($aPackageModules as $sModuleId => $aModule) {
if ($this->IsDefaultModule($sModuleId, $aModule)) {
list($sModuleName) = ModuleDiscovery::GetModuleName($sModuleId);
$aInstalledModules[$sModuleName] = true;
unset($aPackageModules[$sModuleId]);
}
}
}
private function IsAutoSelectedModule(array $aInstalledModules, string $sModuleId, array $aModule): bool
{
if (($sModuleId === ROOT_MODULE)) {
return false;
}
if (!isset($aModule['auto_select'])) {
return false;
}
try {
SetupInfo::SetSelectedModules($aInstalledModules);
return DependencyExpression::GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($aModule['auto_select']);
} catch (Exception $e) {
IssueLog::Error('Error evaluating module auto-select', null, [
'module' => $sModuleId,
'error' => $e->getMessage(),
'evaluated code' => $aModule['auto_select'],
'stacktrace' => $e->getTraceAsString(),
]);
}
return false;
}
private function ProcessAutoSelectModules(array $aPackageModules, array &$aInstalledModules): void
{
foreach ($aPackageModules as $sModuleId => $aModule) {
if ($this->IsAutoSelectedModule($aInstalledModules, $sModuleId, $aModule)) {
list($sModuleName) = ModuleDiscovery::GetModuleName($sModuleId);
$aInstalledModules[$sModuleName] = true;
}
}
}
private function GetModuleNamesFromInstallationChoices(array $aInstallationChoices, array $aInstallationDescription, array &$aModuleNames): void
{
foreach ($aInstallationDescription as $aStepInfo) {
$aOptions = $aStepInfo['options'] ?? null;
if (is_array($aOptions)) {
foreach ($aOptions as $aChoiceInfo) {
$this->ProcessSelectedChoice($aInstallationChoices, $aChoiceInfo, $aModuleNames);
}
}
$aOptions = $aStepInfo['alternatives'] ?? null;
if (is_array($aOptions)) {
foreach ($aOptions as $aChoiceInfo) {
$this->ProcessSelectedChoice($aInstallationChoices, $aChoiceInfo, $aModuleNames);
}
}
}
}
private function ProcessSelectedChoice(array $aInstallationChoices, array $aChoiceInfo, array &$aInstalledModules)
{
if (!is_array($aChoiceInfo)) {
return;
}
$sMandatory = $aChoiceInfo['mandatory'] ?? 'false';
$aCurrentModules = $aChoiceInfo['modules'] ?? [];
$sExtensionCode = $aChoiceInfo['extension_code'];
$bSelected = ($sMandatory === 'true') || in_array($sExtensionCode, $aInstallationChoices);
if (!$bSelected) {
return;
}
foreach ($aCurrentModules as $sModuleId) {
$aInstalledModules[$sModuleId] = true;
}
$aAlternatives = $aChoiceInfo['alternatives'] ?? null;
if (is_array($aAlternatives)) {
foreach ($aAlternatives as $aSubChoiceInfo) {
$this->ProcessSelectedChoice($aInstallationChoices, $aSubChoiceInfo, $aInstalledModules);
}
}
$aSubOptionsChoiceInfo = $aChoiceInfo['sub_options'] ?? null;
if (is_array($aSubOptionsChoiceInfo)) {
$aSubOptions = $aSubOptionsChoiceInfo['options'] ?? null;
if (is_array($aSubOptions)) {
foreach ($aSubOptions as $aSubChoiceInfo) {
$this->ProcessSelectedChoice($aInstallationChoices, $aSubChoiceInfo, $aInstalledModules);
}
}
$aSubAlternatives = $aSubOptionsChoiceInfo['alternatives'] ?? null;
if (is_array($aSubAlternatives)) {
foreach ($aSubAlternatives as $aSubChoiceInfo) {
$this->ProcessSelectedChoice($aInstallationChoices, $aSubChoiceInfo, $aInstalledModules);
}
}
}
}
}

View File

@@ -1,5 +0,0 @@
<?php
class ModuleInstallationException extends Exception
{
}

View File

@@ -130,7 +130,6 @@ abstract class ModuleInstallerAPI
if (in_array($sTo, $aNewValues)) {
$sEnumCol = $oAttDef->Get("sql");
$aFields = CMDBSource::QueryToArray("SHOW COLUMNS FROM `$sTableName` WHERE Field = '$sEnumCol'");
$aCurrentValues = [];
if (isset($aFields[0]['Type'])) {
$sColType = $aFields[0]['Type'];
// Note: the parsing should rely on str_getcsv (requires PHP 5.3) to cope with escaped string

View File

@@ -7,7 +7,6 @@ class InvalidParameterException extends Exception
abstract class Parameters
{
public $aData = null;
private ?array $aParamValues = null;
public function __construct()
{
@@ -27,26 +26,24 @@ abstract class Parameters
*/
public function GetParamForConfigArray()
{
if (is_null($this->aParamValues)) {
$aDBParams = $this->Get('database');
$this->aParamValues = [
'mode' => $this->Get('mode'),
'db_server' => $aDBParams['server'],
'db_user' => $aDBParams['user'],
'db_pwd' => $aDBParams['pwd'],
'db_name' => $aDBParams['name'],
'new_db_name' => $aDBParams['name'],
'db_prefix' => $aDBParams['prefix'],
'db_tls_enabled' => $aDBParams['db_tls_enabled'],
'db_tls_ca' => $aDBParams['db_tls_ca'],
'application_path' => $this->Get('url', ''),
'language' => $this->Get('language', ''),
'graphviz_path' => $this->Get('graphviz_path', ''),
'source_dir' => $this->Get('source_dir', ''),
];
}
$aDBParams = $this->Get('database');
$aParamValues = [
'mode' => $this->Get('mode'),
'db_server' => $aDBParams['server'],
'db_user' => $aDBParams['user'],
'db_pwd' => $aDBParams['pwd'],
'db_name' => $aDBParams['name'],
'new_db_name' => $aDBParams['name'],
'db_prefix' => $aDBParams['prefix'],
'db_tls_enabled' => $aDBParams['db_tls_enabled'],
'db_tls_ca' => $aDBParams['db_tls_ca'],
'application_path' => $this->Get('url', ''),
'language' => $this->Get('language', ''),
'graphviz_path' => $this->Get('graphviz_path', ''),
'source_dir' => $this->Get('source_dir', ''),
];
return $this->aParamValues;
return $aParamValues;
}
public function Set($sCode, $value)
@@ -107,9 +104,7 @@ class PHPParameters extends Parameters
{
if ($this->aData == null) {
require_once($sParametersFile);
if (isset($ITOP_PARAMS)) {
$this->aData = $ITOP_PARAMS; // Defined in the file loaded just above
}
$this->aData = $ITOP_PARAMS; // Defined in the file loaded just above
}
}
}

View File

@@ -32,8 +32,7 @@ require_once APPROOT."setup/modulediscovery.class.inc.php";
require_once APPROOT.'setup/modelfactory.class.inc.php';
require_once APPROOT.'setup/compiler.class.inc.php';
require_once APPROOT.'setup/extensionsmap.class.inc.php';
require_once APPROOT.'setup/moduleinstallation/AnalyzeInstallation.php';
require_once APPROOT.'/setup/moduleinstallation/InstallationChoicesToModuleConverter.php';
require_once APPROOT.'setup/AnalyzeInstallation.php';
define('MODULE_ACTION_OPTIONAL', 1);
define('MODULE_ACTION_MANDATORY', 2);
@@ -127,10 +126,11 @@ class RunTimeEnvironment
* from the given file
* @param $oConfig object The configuration (volatile, not necessarily already on disk)
* @param $bModelOnly boolean Whether or not to allow loading a data model with no corresponding DB
* @return none
*/
public function InitDataModel($oConfig, $bModelOnly = true, $bUseCache = false): void
public function InitDataModel($oConfig, $bModelOnly = true, $bUseCache = false)
{
require_once APPROOT.'/setup/moduleinstallation/moduleinstallation.class.inc.php';
require_once APPROOT.'/setup/moduleinstallation.class.inc.php';
$sConfigFile = $oConfig->GetLoadedFile();
if (strlen($sConfigFile) > 0) {
@@ -226,30 +226,12 @@ class RunTimeEnvironment
return ($oExtension->sSource == iTopExtension::SOURCE_REMOTE);
}
public function GetExtraDirsToCompile(string $sSourceDir): array
{
$sSourceDirFull = APPROOT.$sSourceDir;
if (!is_dir($sSourceDirFull)) {
throw new Exception("The source directory '$sSourceDirFull' does not exist (or could not be read)");
}
$aDirsToCompile = [$sSourceDirFull];
if (is_dir(APPROOT.'extensions')) {
$aDirsToCompile[] = APPROOT.'extensions';
}
$sExtraDir = utils::GetDataPath().$this->sTargetEnv.'-modules/';
if (is_dir($sExtraDir)) {
$aDirsToCompile[] = $sExtraDir;
}
return $aDirsToCompile;
}
/**
* Get the installed modules (only the installed ones)
*/
protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir)
{
\SetupLog::Info(__METHOD__);
$sSourceDirFull = APPROOT.$sSourceDir;
if (!is_dir($sSourceDirFull)) {
throw new Exception("The source directory '$sSourceDirFull' does not exist (or could not be read)");
@@ -366,7 +348,6 @@ class RunTimeEnvironment
//
$oFactory = new ModelFactory($sSourceDirFull);
$aModulesToCompile = $this->GetMFModulesToCompile($sSourceEnv, $sSourceDir);
$oModule = null;
foreach ($aModulesToCompile as $oModule) {
if ($oModule instanceof MFDeltaModule) {
// Just before loading the delta, let's save an image of the datamodel
@@ -376,7 +357,7 @@ class RunTimeEnvironment
$oFactory->LoadModule($oModule);
}
if (!is_null($oModule) && ($oModule instanceof MFDeltaModule)) {
if ($oModule instanceof MFDeltaModule) {
// A delta was loaded, let's save a second copy of the datamodel
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sTargetEnv.'-with-delta.xml');
} else {
@@ -568,14 +549,9 @@ class RunTimeEnvironment
//
$aAvailableModules = $this->AnalyzeInstallation($oConfig, $this->GetBuildDir());
foreach ($aSelectedModuleCodes as $sModuleId) {
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
}
if (!array_key_exists($sModuleId, $aAvailableModules)) {
continue;
}
$aModuleData = $aAvailableModules[$sModuleId];
$sName = $sModuleId;
$sVersion = $aModuleData['available_version'];
@@ -649,7 +625,7 @@ class RunTimeEnvironment
public function GetApplicationVersion(Config $oConfig)
{
try {
$aSelectInstall = ModuleInstallationRepository::GetInstance()->ReadFromDB($oConfig);
$aSelectInstall = ModuleInstallationService::GetInstance()->ReadFromDB($oConfig);
} catch (MySQLException $e) {
// No database or erroneous information
$this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name'));
@@ -687,8 +663,7 @@ class RunTimeEnvironment
$aResult['datamodel_version'] = $aResult['product_version'];
}
$this->log_info("GetApplicationVersion returns: product_name: ".$aResult['product_name'].', product_version: '.$aResult['product_version']);
return count($aResult) == 0 ? false : $aResult;
return empty($aResult) ? false : $aResult;
}
public static function MakeDirSafe($sDir)
@@ -876,20 +851,16 @@ class RunTimeEnvironment
/**
* Call the given handler method for all selected modules having an installation handler
* @param array[] $aAvailableModules
* @param string[] $aSelectedModules
* @param string $sHandlerName
* @param string[]|null $aSelectedModules
* @throws CoreException
*/
public function CallInstallerHandlers($aAvailableModules, $sHandlerName, $aSelectedModules = null)
public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sHandlerName)
{
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
}
if (is_null($aSelectedModules) || in_array($sModuleId, $aSelectedModules)) {
if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules)) {
$aArgs = [MetaModel::GetConfig(), $aModule['installed_version'], $aModule['available_version']];
RunTimeEnvironment::CallInstallerHandler($aModule, $sHandlerName, $aArgs);
RunTimeEnvironment::CallInstallerHandler($aAvailableModules[$sModuleId], $sHandlerName, $aArgs);
}
}
}
@@ -916,7 +887,6 @@ class RunTimeEnvironment
try {
call_user_func_array($aCallSpec, $aArgs);
} catch (Exception $e) {
$sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID] ?? "";
$sErrorMessage = "Module $sModuleId : error when calling module installer class $sModuleInstallerClass for $sHandlerName handler";
$aExceptionContextData = [
'ModulelId' => $sModuleId,
@@ -933,10 +903,10 @@ class RunTimeEnvironment
/**
* Load data from XML files for the selected modules (structural data and/or sample data)
* @param array[] $aAvailableModules All available modules and their definition
* @param string[] $aSelectedModules List of selected modules
* @param bool $bSampleData Wether or not to load sample data
* @param null|string[] $aSelectedModules List of selected modules
*/
public function LoadData($aAvailableModules, $bSampleData, $aSelectedModules = null)
public function LoadData($aAvailableModules, $aSelectedModules, $bSampleData)
{
$oDataLoader = new XMLDataLoader();
@@ -949,33 +919,30 @@ class RunTimeEnvironment
$aFiles = [];
$aPreviouslyLoadedFiles = [];
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
}
$sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']);
// Load data only for selected AND newly installed modules
if (is_null($aSelectedModules) || in_array($sModuleId, $aSelectedModules)) {
if ($aModule['installed_version'] != '') {
// Simulate the load of the previously loaded XML files to get the mapping of the keys
if ($bSampleData) {
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
if (($sModuleId != ROOT_MODULE)) {
$sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']);
// Load data only for selected AND newly installed modules
if (in_array($sModuleId, $aSelectedModules)) {
if ($aModule['installed_version'] != '') {
// Simulate the load of the previously loaded XML files to get the mapping of the keys
if ($bSampleData) {
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
} else {
// Load only structural data
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
}
} else {
// Load only structural data
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
}
} else {
if ($bSampleData) {
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
} else {
// Load only structural data
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
if ($bSampleData) {
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
} else {
// Load only structural data
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
}
}
}
}
}
// Simulate the load of the previously loaded files, in order to initialize
@@ -984,7 +951,7 @@ class RunTimeEnvironment
foreach ($aPreviouslyLoadedFiles as $sFileRelativePath) {
$sFileName = APPROOT.$sFileRelativePath;
SetupLog::Info("Loading file: $sFileName (just to get the keys mapping)");
if (!file_exists($sFileName)) {
if (empty($sFileName) || !file_exists($sFileName)) {
throw(new Exception("File $sFileName does not exist"));
}
@@ -996,7 +963,7 @@ class RunTimeEnvironment
foreach ($aFiles as $sFileRelativePath) {
$sFileName = APPROOT.$sFileRelativePath;
SetupLog::Info("Loading file: $sFileName");
if (!file_exists($sFileName)) {
if (empty($sFileName) || !file_exists($sFileName)) {
throw(new Exception("File $sFileName does not exist"));
}

View File

@@ -1,980 +0,0 @@
<?php
/**
* Copyright (C) 2013-2026 Combodo SAS
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
*/
require_once(APPROOT.'setup/parameters.class.inc.php');
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
require_once(APPROOT.'setup/backup.class.inc.php');
require_once(APPROOT.'setup/sequencers/StepSequencer.php');
require_once(APPROOT.'setup/SetupDBBackup.php');
/**
* The base class for the installation process.
* The installation process is split into a sequence of unitary steps
* for performance reasons (i.e; timeout, memory usage) and also in order
* to provide some feedback about the progress of the installation.
*
* This class can be used for a step by step interactive installation
* while displaying a progress bar, or in an unattended manner
* (for example from the command line), to run all the steps
* in one go.
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
class ApplicationInstallSequencer extends StepSequencer
{
protected Parameters $oParams;
protected static bool $bMetaModelStarted = false;
protected Config $oConfig;
/**
* @param \Parameters $oParams
*
* @throws \ConfigException
* @throws \CoreException
*/
public function __construct(Parameters $oParams)
{
$this->oParams = $oParams;
$aParamValues = $oParams->GetParamForConfigArray();
$this->oConfig = new Config();
$this->oConfig->UpdateFromParams($aParamValues);
utils::SetConfig($this->oConfig);
}
/**
* @return string
*/
protected function GetTargetEnv()
{
$sTargetEnvironment = $this->oParams->Get('target_env', '');
if ($sTargetEnvironment !== '') {
return $sTargetEnvironment;
}
return 'production';
}
/**
* @return string
*/
protected function GetTargetDir()
{
$sTargetEnv = $this->GetTargetEnv();
return 'env-'.$sTargetEnv;
}
protected function GetConfig()
{
$sTargetEnvironment = $this->GetTargetEnv();
$sConfigFile = APPCONF.$sTargetEnvironment.'/'.ITOP_CONFIG_FILE;
try {
$oConfig = new Config($sConfigFile);
} catch (Exception $e) {
return null;
}
$aParamValues = $this->oParams->GetParamForConfigArray();
$oConfig->UpdateFromParams($aParamValues);
return $oConfig;
}
protected function DoLogParameters($sPrefix = 'install-', $sOperation = 'Installation')
{
// Log the parameters...
$oDoc = new DOMDocument('1.0', 'UTF-8');
$oDoc->preserveWhiteSpace = false;
$oDoc->formatOutput = true;
$this->oParams->ToXML($oDoc, null, 'installation');
$sXML = $oDoc->saveXML();
$sSafeXml = preg_replace("|<pwd>([^<]*)</pwd>|", "<pwd>**removed**</pwd>", $sXML);
SetupLog::Info("======= ".$sOperation." starts =======\nParameters:\n$sSafeXml\n");
// Save the response file as a stand-alone file as well
$sFileName = $sPrefix.date('Y-m-d');
$index = 0;
while (file_exists(APPROOT.'log/'.$sFileName.'.xml')) {
$index++;
$sFileName = $sPrefix.date('Y-m-d').'-'.$index;
}
file_put_contents(APPROOT.'log/'.$sFileName.'.xml', $sSafeXml);
}
/**
* Executes the next step of the installation and reports about the progress
* and the next step to perform
*
* @param string $sStep The identifier of the step to execute
* @param string|null $sInstallComment
*
* @return array (status => , message => , percentage-completed => , next-step => , next-step-label => )
*/
public function ExecuteStep($sStep = '', $sInstallComment = null)
{
try {
$fStart = microtime(true);
SetupLog::Info("##### STEP {$sStep} start");
$this->EnterReadOnlyMode();
switch ($sStep) {
case '':
$this->DoLogParameters();
$aResult = [
'status' => self::OK,
'message' => '',
'percentage-completed' => 0,
'next-step' => 'copy',
'next-step-label' => 'Copying data model files',
];
break;
case 'copy':
$aPreinstall = $this->oParams->Get('preinstall');
$aCopies = $aPreinstall['copies'] ?? [];
$this->DoCopy($aCopies);
$sReport = "Copying...";
$aResult = [
'status' => self::OK,
'message' => $sReport,
];
if (isset($aPreinstall['backup'])) {
$aResult['next-step'] = 'backup';
$aResult['next-step-label'] = 'Performing a backup of the database';
$aResult['percentage-completed'] = 20;
} else {
$aResult['next-step'] = 'compile';
$aResult['next-step-label'] = 'Compiling the data model';
$aResult['percentage-completed'] = 20;
}
break;
case 'backup':
$aPreinstall = $this->oParams->Get('preinstall');
// __DB__-%Y-%m-%d
$sDestination = $aPreinstall['backup']['destination'];
$sSourceConfigFile = $aPreinstall['backup']['configuration_file'];
$sMySQLBinDir = $this->oParams->Get('mysql_bindir', null);
$this->DoBackup($sDestination, $sSourceConfigFile, $sMySQLBinDir);
$aResult = [
'status' => self::OK,
'message' => "Created backup",
'next-step' => 'compile',
'next-step-label' => 'Compiling the data model',
'percentage-completed' => 20,
];
break;
case 'compile':
$aSelectedModules = $this->oParams->Get('selected_modules');
$sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest');
$sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions');
$aMiscOptions = $this->oParams->Get('options', []);
$aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []);
$bUseSymbolicLinks = null;
if ((isset($aMiscOptions['symlinks']) && $aMiscOptions['symlinks'])) {
if (function_exists('symlink')) {
$bUseSymbolicLinks = true;
SetupLog::Info("Using symbolic links instead of copying data model files (for developers only!)");
} else {
SetupLog::Info("Symbolic links (function symlinks) does not seem to be supported on this platform (OS/PHP version).");
}
}
$this->DoCompile(
$aRemovedExtensionCodes,
$aSelectedModules,
$sSourceDir,
$sExtensionDir,
$bUseSymbolicLinks
);
$sNextStep = 'db-schema';
$sNextStepLabel = 'Updating database schema';
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => $sNextStep,
'next-step-label' => $sNextStepLabel,
'percentage-completed' => 40,
];
break;
case 'db-schema':
$aSelectedModules = $this->oParams->Get('selected_modules', []);
$this->DoUpdateDBSchema(
$aSelectedModules
);
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => 'after-db-create',
'next-step-label' => 'Creating profiles',
'percentage-completed' => 60,
];
break;
case 'after-db-create':
$aAdminParams = $this->oParams->Get('admin_account');
$aSelectedModules = $this->oParams->Get('selected_modules', []);
$this->AfterDBCreate(
$aAdminParams,
$aSelectedModules
);
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => 'load-data',
'next-step-label' => 'Loading data',
'percentage-completed' => 80,
];
break;
case 'load-data':
$aSelectedModules = $this->oParams->Get('selected_modules', []);
$bSampleData = ($this->oParams->Get('sample_data', 0) == 1);
$this->DoLoadFiles(
$aSelectedModules,
$bSampleData
);
$aResult = [
'status' => self::INFO,
'message' => 'All data loaded',
'next-step' => 'create-config',
'next-step-label' => 'Creating the configuration File',
'percentage-completed' => 99,
];
break;
case 'create-config':
$sPreviousConfigFile = $this->oParams->Get('previous_configuration_file', '');
$sDataModelVersion = $this->oParams->Get('datamodel_version', '0.0.0');
$aSelectedModuleCodes = $this->oParams->Get('selected_modules', []);
$aSelectedExtensionCodes = $this->oParams->Get('selected_extensions', []);
$this->DoCreateConfig(
$sPreviousConfigFile,
$sDataModelVersion,
$aSelectedModuleCodes,
$aSelectedExtensionCodes,
$sInstallComment
);
$aResult = [
'status' => self::INFO,
'message' => 'Configuration file created',
'next-step' => '',
'next-step-label' => 'Completed',
'percentage-completed' => 100,
];
break;
default:
$aResult = [
'status' => self::ERROR,
'message' => '',
'next-step' => '',
'next-step-label' => "Unknown setup step '$sStep'.",
'percentage-completed' => 100,
];
break;
}
$this->ExitReadOnlyMode();
} catch (Exception $e) {
$aResult = [
'status' => self::ERROR,
'message' => $e->getMessage(),
'next-step' => '',
'next-step-label' => '',
'percentage-completed' => 100,
'error_code' => $e->getCode(),
];
$this->ReportException($e);
} finally {
$fDuration = round(microtime(true) - $fStart, 2);
SetupLog::Info("##### STEP {$sStep} duration: {$fDuration}s");
}
return $aResult;
}
protected function ReportException(Exception $e)
{
SetupLog::Error('An exception occurred: '.$e->getMessage().' at line '.$e->getLine().' in file '.$e->getFile());
$idx = 0;
// Log the call stack, but not the parameters since they may contain passwords or other sensitive data
SetupLog::Ok("Call stack:");
foreach ($e->getTrace() as $aTrace) {
$sLine = empty($aTrace['line']) ? "" : $aTrace['line'];
$sFile = empty($aTrace['file']) ? "" : $aTrace['file'];
$sClass = empty($aTrace['class']) ? "" : $aTrace['class'];
$sType = empty($aTrace['type']) ? "" : $aTrace['type'];
$sFunction = empty($aTrace['function']) ? "" : $aTrace['function'];
$sVerb = empty($sClass) ? $sFunction : "$sClass{$sType}$sFunction";
SetupLog::Ok("#$idx $sFile($sLine): $sVerb(...)");
$idx++;
}
}
protected function EnterReadOnlyMode()
{
if ($this->GetTargetEnv() != 'production') {
return;
}
if (SetupUtils::IsInReadOnlyMode()) {
return;
}
SetupUtils::EnterReadOnlyMode($this->GetConfig());
}
protected function ExitReadOnlyMode()
{
if ($this->GetTargetEnv() != 'production') {
return;
}
if (!SetupUtils::IsInReadOnlyMode()) {
return;
}
SetupUtils::ExitReadOnlyMode();
}
protected function DoCopy($aCopies)
{
$aReports = [];
foreach ($aCopies as $aCopy) {
$sSource = $aCopy['source'];
$sDestination = APPROOT.$aCopy['destination'];
SetupUtils::builddir($sDestination);
SetupUtils::tidydir($sDestination);
SetupUtils::copydir($sSource, $sDestination);
$aReports[] = "'{$aCopy['source']}' to '{$aCopy['destination']}' (OK)";
}
if (count($aReports) > 0) {
$sReport = "Copies: ".count($aReports).': '.implode('; ', $aReports);
} else {
$sReport = "No file copy";
}
return $sReport;
}
/**
* @param string $sBackupFileFormat
* @param string $sSourceConfigFile
* @param string $sMySQLBinDir
*
* @throws \BackupException
* @throws \CoreException
* @throws \MySQLException
* @since 2.5.0 uses a {@link Config} object to store DB parameters
*/
protected function DoBackup($sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null)
{
$oBackup = new SetupDBBackup($this->oConfig);
$sTargetFile = $oBackup->MakeName($sBackupFileFormat);
if (!empty($sMySQLBinDir)) {
$oBackup->SetMySQLBinDir($sMySQLBinDir);
}
CMDBSource::InitFromConfig($this->oConfig);
$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
}
/**
* @param array $aRemovedExtensionCodes
* @param array $aSelectedModules
* @param string $sSourceDir
* @param string $sExtensionDir
* @param boolean $bUseSymbolicLinks
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*
* @since 3.1.0 N°2013 added the aParamValues param
*/
protected function DoCompile($aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null)
{
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
*/
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
SetupLog::Info("Compiling data model.");
require_once(APPROOT.'setup/modulediscovery.class.inc.php');
require_once(APPROOT.'setup/modelfactory.class.inc.php');
require_once(APPROOT.'setup/compiler.class.inc.php');
$aParamValues = $this->oParams->GetParamForConfigArray();
$sEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
if (empty($sSourceDir) || empty($sTargetDir)) {
throw new Exception("missing parameter source_dir and/or target_dir");
}
$sSourcePath = APPROOT.$sSourceDir;
$aDirsToScan = [$sSourcePath];
$sExtensionsPath = APPROOT.$sExtensionDir;
if (is_dir($sExtensionsPath)) {
// if the extensions dir exists, scan it for additional modules as well
$aDirsToScan[] = $sExtensionsPath;
}
$sExtraPath = APPROOT.'/data/'.$sEnvironment.'-modules/';
if (is_dir($sExtraPath)) {
// if the extra dir exists, scan it for additional modules as well
$aDirsToScan[] = $sExtraPath;
}
$sTargetPath = APPROOT.$sTargetDir;
if (!is_dir($sSourcePath)) {
throw new Exception("Failed to find the source directory '$sSourcePath', please check the rights of the web server");
}
$bIsAlreadyInMaintenanceMode = SetupUtils::IsInMaintenanceMode();
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) {
$sConfigFilePath = utils::GetConfigFilePath($sEnvironment);
if (is_file($sConfigFilePath)) {
$oConfig = new Config($sConfigFilePath);
$oConfig->UpdateFromParams($aParamValues);
SetupUtils::EnterMaintenanceMode($oConfig);
}
}
try {
if (!is_dir($sTargetPath)) {
if (!mkdir($sTargetPath)) {
throw new Exception("Failed to create directory '$sTargetPath', please check the rights of the web server");
} else {
// adjust the rights if and only if the directory was just created
// owner:rwx user/group:rx
chmod($sTargetPath, 0755);
}
} elseif (substr($sTargetPath, 0, strlen(APPROOT)) == APPROOT) {
// If the directory is under the root folder - as expected - let's clean-it before compiling
SetupUtils::tidydir($sTargetPath);
}
$oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan);
$oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes);
$oFactory = new ModelFactory($aDirsToScan);
$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
$oFactory->LoadModule($oDictModule);
$sDeltaFile = APPROOT.'core/datamodel.core.xml';
if (file_exists($sDeltaFile)) {
$oCoreModule = new MFCoreModule('core', 'Core Module', $sDeltaFile);
$oFactory->LoadModule($oCoreModule);
}
$sDeltaFile = APPROOT.'application/datamodel.application.xml';
if (file_exists($sDeltaFile)) {
$oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile);
$oFactory->LoadModule($oApplicationModule);
}
$aModules = $oFactory->FindModules();
foreach ($aModules as $oModule) {
$sModule = $oModule->GetName();
if (in_array($sModule, $aSelectedModules)) {
$oFactory->LoadModule($oModule);
}
}
// Dump the "reference" model, just before loading any actual delta
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'.xml');
$sDeltaFile = utils::GetDataPath().$sEnvironment.'.delta.xml';
if (file_exists($sDeltaFile)) {
$oDelta = new MFDeltaModule($sDeltaFile);
$oFactory->LoadModule($oDelta);
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'-with-delta.xml');
}
$oMFCompiler = new MFCompiler($oFactory, $sEnvironment);
$oMFCompiler->Compile($sTargetPath, null, $bUseSymbolicLinks);
//$aCompilerLog = $oMFCompiler->GetLog();
//SetupLog::Info(implode("\n", $aCompilerLog));
SetupLog::Info("Data model successfully compiled to '$sTargetPath'.");
$sCacheDir = APPROOT.'/data/cache-'.$sEnvironment.'/';
SetupUtils::builddir($sCacheDir);
SetupUtils::tidydir($sCacheDir);
} catch (Exception $e) {
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) {
SetupUtils::ExitMaintenanceMode();
}
throw $e;
}
// Special case to patch a ugly patch in itop-config-mgmt
$sFileToPatch = $sTargetPath.'/itop-config-mgmt-1.0.0/model.itop-config-mgmt.php';
if (file_exists($sFileToPatch)) {
$sContent = file_get_contents($sFileToPatch);
$sContent = str_replace("require_once(APPROOT.'modules/itop-welcome-itil/model.itop-welcome-itil.php');", "//\n// The line below is no longer needed in iTop 2.0 -- patched by the setup program\n// require_once(APPROOT.'modules/itop-welcome-itil/model.itop-welcome-itil.php');", $sContent);
file_put_contents($sFileToPatch, $sContent);
}
// Set an "Instance UUID" identifying this machine based on a file located in the data directory
$sInstanceUUIDFile = utils::GetDataPath().'instance.txt';
SetupUtils::builddir(utils::GetDataPath());
if (!file_exists($sInstanceUUIDFile)) {
$sIntanceUUID = utils::CreateUUID('filesystem');
file_put_contents($sInstanceUUIDFile, $sIntanceUUID);
}
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) {
SetupUtils::ExitMaintenanceMode();
}
}
protected function GetModelInfoPath(string $sEnv): string
{
return APPROOT."data/beforecompilation_".$sEnv."_modelinfo.json";
}
protected function SaveModelInfo(string $sEnvironment): bool
{
$sModelInfoPath = $this->GetModelInfoPath($sEnvironment);
try {
$aModelInfo = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($sEnvironment);
} catch (Exception $e) {
//logged already
return is_file($sModelInfoPath);
}
return (bool) file_put_contents($sModelInfoPath, json_encode($aModelInfo));
}
protected function GetPreviousModelInfo(string $sEnvironment): array
{
$sContent = file_get_contents($this->GetModelInfoPath($sEnvironment));
$aModelInfo = json_decode($sContent, true);
if (false === $aModelInfo) {
throw new Exception("Could not read (before compilation) previous model to audit data");
}
return $aModelInfo;
}
protected function IsSetupDataAuditEnabled($sSkipDataAudit, array $aParamValues): bool
{
if ($sSkipDataAudit === "checked") {
SetupLog::Info("Setup data audit disabled", null, ['skip-data-audit' => $sSkipDataAudit]);
return false;
}
$sMode = $aParamValues['mode'];
if ($sMode !== "upgrade") {
//first install
return false;
}
$sPath = APPROOT.$this->GetTargetDir();
if (!is_dir($sPath)) {
SetupLog::Info("Reinstallation of an iTop from a backup (No ".$this->GetTargetDir()." found). Setup data audit disabled", null, ['skip-data-audit' => $sSkipDataAudit]);
return false;
}
return true;
}
/**
* @param $aSelectedModules
*
* @throws \ConfigException
* @throws \CoreException
* @throws \MySQLException
*/
protected function DoUpdateDBSchema($aSelectedModules)
{
$sTargetEnvironment = $this->GetTargetEnv();
$sModulesDir = $this->GetTargetDir();
$aParamValues = $this->oParams->GetParamForConfigArray();
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
*/
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
SetupLog::Info("Update Database Schema for environment '$sTargetEnvironment'.");
$sMode = $aParamValues['mode'];
$sDBPrefix = $aParamValues['db_prefix'];
$sDBName = $aParamValues['db_name'];
$oConfig = new Config();
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
$oProductionEnv->InitDataModel($oConfig, true); // load data model only
// Migrate columns
self::MoveColumns($sDBPrefix);
// Migrate application data format
//
// priv_internalUser caused troubles because MySQL transforms table names to lower case under Windows
// This becomes an issue when moving your installation data to/from Windows
// Starting 2.0, all table names must be lowercase
if ($sMode != 'install') {
SetupLog::Info("Renaming '{$sDBPrefix}priv_internalUser' into '{$sDBPrefix}priv_internaluser' (lowercase)");
// This command will have no effect under Windows...
// and it has been written in two steps so as to make it work under windows!
CMDBSource::SelectDB($sDBName);
try {
$sRepair = "RENAME TABLE `{$sDBPrefix}priv_internalUser` TO `{$sDBPrefix}priv_internaluser_other`, `{$sDBPrefix}priv_internaluser_other` TO `{$sDBPrefix}priv_internaluser`";
CMDBSource::Query($sRepair);
} catch (Exception $e) {
SetupLog::Info("Renaming '{$sDBPrefix}priv_internalUser' failed (already done in a previous upgrade?)");
}
// let's remove the records in priv_change which have no counterpart in priv_changeop
SetupLog::Info("Cleanup of '{$sDBPrefix}priv_change' to remove orphan records");
CMDBSource::SelectDB($sDBName);
try {
$sTotalCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_change`";
$iTotalCount = (int)CMDBSource::QueryToScalar($sTotalCount);
SetupLog::Info("There is a total of $iTotalCount records in {$sDBPrefix}priv_change.");
$sOrphanCount = "SELECT COUNT(c.id) FROM `{$sDBPrefix}priv_change` AS c left join `{$sDBPrefix}priv_changeop` AS o ON c.id = o.changeid WHERE o.id IS NULL";
$iOrphanCount = (int)CMDBSource::QueryToScalar($sOrphanCount);
SetupLog::Info("There are $iOrphanCount useless records in {$sDBPrefix}priv_change (".sprintf('%.2f', ((100.0 * $iOrphanCount) / $iTotalCount))."%)");
if ($iOrphanCount > 0) {
//N°3793
if ($iOrphanCount > 100000) {
SetupLog::Info("There are too much useless records ($iOrphanCount) in {$sDBPrefix}priv_change. Cleanup cannot be done during setup.");
} else {
SetupLog::Info("Removing the orphan records...");
$sCleanup = "DELETE FROM `{$sDBPrefix}priv_change` USING `{$sDBPrefix}priv_change` LEFT JOIN `{$sDBPrefix}priv_changeop` ON `{$sDBPrefix}priv_change`.id = `{$sDBPrefix}priv_changeop`.changeid WHERE `{$sDBPrefix}priv_changeop`.id IS NULL;";
CMDBSource::Query($sCleanup);
SetupLog::Info("Cleanup completed successfully.");
}
} else {
SetupLog::Info("Ok, nothing to cleanup.");
}
} catch (Exception $e) {
SetupLog::Info("Cleanup of orphan records in `{$sDBPrefix}priv_change` failed: ".$e->getMessage());
}
}
// Module specific actions (migrate the data)
$aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir);
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation', $aSelectedModules);
if (!$oProductionEnv->CreateDatabaseStructure(MetaModel::GetConfig(), $sMode)) {
throw new Exception("Failed to create/upgrade the database structure for environment '$sTargetEnvironment'");
}
// Set a DBProperty with a unique ID to identify this instance of iTop
$sUUID = DBProperty::GetProperty('database_uuid', '');
if ($sUUID === '') {
$sUUID = utils::CreateUUID('database');
DBProperty::SetProperty('database_uuid', $sUUID, 'Installation/upgrade of '.ITOP_APPLICATION, 'Unique ID of this '.ITOP_APPLICATION.' Database');
}
// priv_change now has an 'origin' field to distinguish between the various input sources
// Let's initialize the field with 'interactive' for all records were it's null
// Then check if some records should hold a different value, based on a pattern matching in the userinfo field
CMDBSource::SelectDB($sDBName);
try {
$sCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_change` WHERE `origin` IS NULL";
$iCount = (int)CMDBSource::QueryToScalar($sCount);
if ($iCount > 0) {
SetupLog::Info("Initializing '{$sDBPrefix}priv_change.origin' ($iCount records to update)");
// By default all uninitialized values are considered as 'interactive'
$sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'interactive' WHERE `origin` IS NULL";
CMDBSource::Query($sInit);
// CSV Import was identified by the comment at the end
$sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'csv-import.php' WHERE `userinfo` LIKE '%Web Service (CSV)'";
CMDBSource::Query($sInit);
// CSV Import was identified by the comment at the end
$sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'csv-interactive' WHERE `userinfo` LIKE '%(CSV)' AND origin = 'interactive'";
CMDBSource::Query($sInit);
// Syncho data sources were identified by the comment at the end
// Unfortunately the comment is localized, so we have to search for all possible patterns
$sCurrentLanguage = Dict::GetUserLanguage();
$aSuffixes = [];
foreach (array_keys(Dict::GetLanguages()) as $sLangCode) {
Dict::SetUserLanguage($sLangCode);
$sSuffix = CMDBSource::Quote('%'.Dict::S('Core:SyncDataExchangeComment'));
$aSuffixes[$sSuffix] = true;
}
Dict::SetUserLanguage($sCurrentLanguage);
$sCondition = "`userinfo` LIKE ".implode(" OR `userinfo` LIKE ", array_keys($aSuffixes));
$sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'synchro-data-source' WHERE ($sCondition)";
CMDBSource::Query($sInit);
SetupLog::Info("Initialization of '{$sDBPrefix}priv_change.origin' completed.");
} else {
SetupLog::Info("'{$sDBPrefix}priv_change.origin' already initialized, nothing to do.");
}
} catch (Exception $e) {
SetupLog::Error("Initializing '{$sDBPrefix}priv_change.origin' failed: ".$e->getMessage());
}
// priv_async_task now has a 'status' field to distinguish between the various statuses rather than just relying on the date columns
// Let's initialize the field with 'planned' or 'error' for all records were it's null
CMDBSource::SelectDB($sDBName);
try {
$sCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_async_task` WHERE `status` IS NULL";
$iCount = (int)CMDBSource::QueryToScalar($sCount);
if ($iCount > 0) {
SetupLog::Info("Initializing '{$sDBPrefix}priv_async_task.status' ($iCount records to update)");
$sInit = "UPDATE `{$sDBPrefix}priv_async_task` SET `status` = 'planned' WHERE (`status` IS NULL) AND (`started` IS NULL)";
CMDBSource::Query($sInit);
$sInit = "UPDATE `{$sDBPrefix}priv_async_task` SET `status` = 'error' WHERE (`status` IS NULL) AND (`started` IS NOT NULL)";
CMDBSource::Query($sInit);
SetupLog::Info("Initialization of '{$sDBPrefix}priv_async_task.status' completed.");
} else {
SetupLog::Info("'{$sDBPrefix}priv_async_task.status' already initialized, nothing to do.");
}
} catch (Exception $e) {
SetupLog::Error("Initializing '{$sDBPrefix}priv_async_task.status' failed: ".$e->getMessage());
}
SetupLog::Info("Database Schema Successfully Updated for environment '$sTargetEnvironment'.");
}
/**
* @param string $sDBPrefix
*
* @throws \CoreException
* @throws \MySQLException
*/
protected static function MoveColumns($sDBPrefix)
{
// In 2.6.0 the 'fields' attribute has been moved from Query to QueryOQL for dependencies reasons
ModuleInstallerAPI::MoveColumnInDB($sDBPrefix.'priv_query', 'fields', $sDBPrefix.'priv_query_oql', 'fields');
}
protected function AfterDBCreate(
$aAdminParams,
$aSelectedModules
) {
$sAdminUser = $aAdminParams['user'];
$sAdminPwd = $aAdminParams['pwd'];
$sAdminLanguage = $aAdminParams['language'];
$aParamValues = $this->oParams->GetParamForConfigArray();
$sTargetEnvironment = $this->GetTargetEnv();
$sModulesDir = $this->GetTargetDir();
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
*/
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
SetupLog::Info('After Database Creation');
$sMode = $aParamValues['mode'];
$oConfig = new Config();
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
$oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
self::$bMetaModelStarted = true; // No need to reload the final MetaModel in case the installer runs synchronously
// Perform here additional DB setup... profiles, etc...
//
$aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir);
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation', $aSelectedModules);
$oProductionEnv->UpdatePredefinedObjects();
if ($sMode == 'install') {
if (!self::CreateAdminAccount(MetaModel::GetConfig(), $sAdminUser, $sAdminPwd, $sAdminLanguage)) {
throw(new Exception("Failed to create the administrator account '$sAdminUser'"));
} else {
SetupLog::Info("Administrator account '$sAdminUser' created.");
}
}
// Perform final setup tasks here
//
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup', $aSelectedModules);
}
/**
* Helper function to create and administrator account for iTop
* @return boolean true on success, false otherwise
*/
protected static function CreateAdminAccount(Config $oConfig, $sAdminUser, $sAdminPwd, $sLanguage)
{
SetupLog::Info('CreateAdminAccount');
if (UserRights::CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage)) {
return true;
} else {
return false;
}
}
protected function DoLoadFiles(
$aSelectedModules,
$bSampleData = false
) {
$aParamValues = $this->oParams->GetParamForConfigArray();
$sTargetEnvironment = $this->GetTargetEnv();
$sModulesDir = $this->GetTargetDir();
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
*/
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
$oConfig = new Config();
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
//Load the MetaModel if needed (asynchronous mode)
if (!self::$bMetaModelStarted) {
$oProductionEnv->InitDataModel($oConfig, false); // load data model and connect to the database
self::$bMetaModelStarted = true; // No need to reload the final MetaModel in case the installer runs synchronously
}
$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, APPROOT.$sModulesDir);
$oProductionEnv->LoadData($aAvailableModules, $bSampleData, $aSelectedModules);
// Perform after dbload setup tasks here
//
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad', $aSelectedModules);
}
/**
* @param string $sPreviousConfigFile
* @param string $sDataModelVersion
* @param array $aSelectedModuleCodes
* @param array $aSelectedExtensionCodes
* @param string|null $sInstallComment
*
* @param null $sInstallComment
*
* @throws \ConfigException
* @throws \CoreException
* @throws \Exception
*/
protected function DoCreateConfig(
$sPreviousConfigFile,
$sDataModelVersion,
$aSelectedModuleCodes,
$aSelectedExtensionCodes,
$sInstallComment = null
) {
$aParamValues = $this->oParams->GetParamForConfigArray();
$sTargetEnvironment = $this->GetTargetEnv();
$sModulesDir = $this->GetTargetDir();
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
*/
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
$aParamValues['selected_modules'] = implode(',', $aSelectedModuleCodes);
$sMode = $aParamValues['mode'];
if ($sMode == 'upgrade') {
try {
$oOldConfig = new Config($sPreviousConfigFile);
$oConfig = clone($oOldConfig);
} catch (Exception $e) {
// In case the previous configuration is corrupted... start with a blank new one
$oConfig = new Config();
}
} else {
$oConfig = new Config();
// To preserve backward compatibility while upgrading to 2.0.3 (when tracking_level_linked_set_default has been introduced)
// the default value on upgrade differs from the default value at first install
$oConfig->Set('tracking_level_linked_set_default', LINKSET_TRACKING_NONE, 'first_install');
}
$oConfig->Set('access_mode', ACCESS_FULL);
// Final config update: add the modules
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
// Record which modules are installed...
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
$oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database
if (!$oProductionEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModuleCodes, $aSelectedExtensionCodes, $sInstallComment)) {
throw new Exception("Failed to record the installation information");
}
// Make sure the root configuration directory exists
if (!file_exists(APPCONF)) {
mkdir(APPCONF);
chmod(APPCONF, 0770); // RWX for owner and group, nothing for others
SetupLog::Info("Created configuration directory: ".APPCONF);
}
// Write the final configuration file
$sConfigFile = APPCONF.(($sTargetEnvironment == '') ? 'production' : $sTargetEnvironment).'/'.ITOP_CONFIG_FILE;
$sConfigDir = dirname($sConfigFile);
@mkdir($sConfigDir);
@chmod($sConfigDir, 0770); // RWX for owner and group, nothing for others
$oConfig->WriteToFile($sConfigFile);
// try to make the final config file read-only
@chmod($sConfigFile, 0440); // Read-only for owner and group, nothing for others
// Ready to go !!
require_once(APPROOT.'core/dict.class.inc.php');
MetaModel::ResetAllCaches();
}
}

View File

@@ -1,203 +0,0 @@
<?php
/**
* Copyright (C) 2013-2026 Combodo SAS
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
*/
require_once(APPROOT.'setup/parameters.class.inc.php');
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
require_once APPROOT.'setup/feature_removal/SetupAudit.php';
require_once(APPROOT.'setup/sequencers/StepSequencer.php');
require_once(APPROOT.'setup/sequencers/ApplicationInstallSequencer.php');
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
class DataAuditSequencer extends ApplicationInstallSequencer
{
public const DATA_AUDIT_FAILED = 100;
protected function GetTempEnv()
{
$sTargetEnv = $this->GetTargetEnv();
return 'dry-'.$sTargetEnv;
}
protected function GetTargetDir()
{
$sTargetEnv = $this->GetTempEnv();
return 'env-'.$sTargetEnv;
}
/**
* Executes the next step of the installation and reports about the progress
* and the next step to perform
*
* @param string $sStep The identifier of the step to execute
* @param string|null $sInstallComment
*
* @return array (status => , message => , percentage-completed => , next-step => , next-step-label => )
*/
public function ExecuteStep($sStep = '', $sInstallComment = null)
{
try {
$fStart = microtime(true);
SetupLog::Info("##### STEP {$sStep} start");
$this->EnterReadOnlyMode();
switch ($sStep) {
case '':
$this->DoLogParameters('data-audit-', 'Data Audit');
$aResult = [
'status' => self::OK,
'message' => '',
'percentage-completed' => 20,
'next-step' => 'compile',
'next-step-label' => 'Compiling the data model',
];
break;
case 'compile':
$aSelectedModules = $this->oParams->Get('selected_modules');
$sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest');
$sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions');
$aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []);
$this->DoCompile(
$aRemovedExtensionCodes,
$aSelectedModules,
$sSourceDir,
$sExtensionDir,
false
);
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => 'write-config',
'next-step-label' => 'Writing audit config',
'percentage-completed' => 40,
];
break;
case 'write-config':
$this->DoWriteConfig();
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => 'setup-audit',
'next-step-label' => 'Checking data consistency with the new data model',
'percentage-completed' => 60,
];
break;
case 'setup-audit':
$this->DoSetupAudit();
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => 'cleanup',
'next-step-label' => 'Temporary folders cleanup',
'percentage-completed' => 80,
];
break;
case 'cleanup' :
$this->DoCleanup();
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => '',
'next-step-label' => 'Completed',
'percentage-completed' => 100,
];
break;
default:
$aResult = [
'status' => self::ERROR,
'message' => '',
'next-step' => '',
'next-step-label' => "Unknown setup step '$sStep'.",
'percentage-completed' => 100,
];
break;
}
} catch (Exception $e) {
$aResult = [
'status' => self::ERROR,
'message' => $e->getMessage(),
'next-step' => '',
'next-step-label' => '',
'percentage-completed' => 100,
'error_code' => $e->getCode(),
];
$this->ReportException($e);
$this->ExitReadOnlyMode();
} finally {
$fDuration = round(microtime(true) - $fStart, 2);
SetupLog::Info("##### STEP {$sStep} duration: {$fDuration}s");
}
return $aResult;
}
protected function DoWriteConfig()
{
$sConfigFilePath = utils::GetConfigFilePath($this->GetTargetEnv());
if (is_file($sConfigFilePath)) {
$oConfig = new Config($sConfigFilePath);
$sTempConfigFileName = utils::GetConfigFilePath($this->GetTempEnv());
$sConfigDir = dirname($sTempConfigFileName);
@mkdir($sConfigDir);
@chmod($sConfigDir, 0770); // RWX for owner and group, nothing for others
return $oConfig->WriteToFile($sTempConfigFileName);
}
return false;
}
protected function DoSetupAudit()
{
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
*/
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
$sTargetEnvironment = $this->GetTempEnv();
$sPreviousEnvironment = $this->GetTargetEnv();
$oSetupAudit = new SetupAudit($sPreviousEnvironment, $sTargetEnvironment);
//Make sure the MetaModel is started before analysing for issues
$sConfFile = utils::GetConfigFilePath($sPreviousEnvironment);
MetaModel::Startup($sConfFile, false /* $bModelOnly */, false /* $bAllowCache */, false /* $bTraceSourceFiles */, $sPreviousEnvironment);
$oSetupAudit->GetIssues(true);
$iCount = $oSetupAudit->GetDataToCleanupCount();
if ($iCount > 0) {
throw new Exception("$iCount elements require data adjustments or cleanup in the backoffice prior to upgrading iTop", static::DATA_AUDIT_FAILED);
}
}
protected function DoCleanup()
{
$sDestination = APPROOT.$this->GetTargetDir();
SetupUtils::tidydir($sDestination);
SetupUtils::rmdir_safe($sDestination);
}
}

View File

@@ -1,106 +0,0 @@
<?php
/**
* Copyright (C) 2013-2026 Combodo SAS
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
*/
abstract class StepSequencer
{
public const OK = 1;
public const ERROR = 2;
public const WARNING = 3;
public const INFO = 4;
protected array $aStepsHistory = [];
public function LogStep($sStep, $aResult)
{
$this->aStepsHistory[] = ['step' => $sStep, 'result' => $aResult];
}
public function GetHistory()
{
return $this->aStepsHistory;
}
/**
* Runs all the installation steps in one go and directly outputs
* some information about the progress and the success of the various
* sequential steps.
*
* @param bool $bVerbose
* @param string|null $sMessage
* @param string|null $sComment
*
* @return boolean True if the installation was successful, false otherwise
*/
public function ExecuteAllSteps(bool $bVerbose = true, ?string &$sMessage = null, ?string $sComment = null)
{
$sStep = '';
$sStepLabel = '';
$iOverallStatus = self::OK;
do {
if ($bVerbose) {
if ($sStep != '') {
echo "$sStepLabel\n";
echo "Executing '$sStep'\n";
} else {
echo "Starting...\n";
}
}
$aRes = $this->ExecuteStep($sStep, $sComment);
$this->LogStep($sStep, $aRes);
$sStep = $aRes['next-step'];
$sStepLabel = $aRes['next-step-label'];
$sMessage = $aRes['message'];
if ($bVerbose) {
switch ($aRes['status']) {
case self::OK:
echo "Ok. ".$aRes['percentage-completed']." % done.\n";
break;
case self::ERROR:
$iOverallStatus = self::ERROR;
echo "Error: ".$aRes['message']."\n";
break;
case self::WARNING:
$iOverallStatus = self::WARNING;
echo "Warning: ".$aRes['message']."\n";
echo $aRes['percentage-completed']." % done.\n";
break;
case self::INFO:
echo "Info: ".$aRes['message']."\n";
echo $aRes['percentage-completed']." % done.\n";
break;
}
} else {
switch ($aRes['status']) {
case self::ERROR:
$iOverallStatus = self::ERROR;
break;
case self::WARNING:
$iOverallStatus = self::WARNING;
break;
}
}
} while (($aRes['status'] != self::ERROR) && ($aRes['next-step'] != ''));
return ($iOverallStatus == self::OK);
}
abstract public function ExecuteStep($sStep = '', $sComment = null);
}

Some files were not shown because too many files have changed in this diff Show More