Compare commits

...

31 Commits

Author SHA1 Message Date
Timothee
6ddba19416 N°9141 Add message when force option checked (whether extensions actually needed it or not) 2026-02-27 10:35:09 +01:00
Timothee
270e8fdb91 N°9141 Log when extension uninstallation is forced 2026-02-27 10:28:20 +01:00
Timothee
f0629724ea Fix Code Style 2026-02-26 10:30:36 +01:00
Timmy38
9b6d321ab0 N°9009 Add phpunit test to GetSelectedModules 2026-02-26 10:28:52 +01:00
Timothee
329191aa47 N°9144 Fix backup option presence during install 2026-02-26 09:35:58 +01:00
odain
6915df1409 N°9168 - fix ci 2026-02-24 11:36:04 +01:00
odain
fd68558d1c Merge branch 'feature/9168-moveintocore' into feature/uninstallation 2026-02-24 10:56:55 +01:00
odain
bd9129dee2 N°9168 - merge data feature removal to iTop core code
N°9168 - phpstan fix

N°9168 - align module version with itop core
2026-02-24 10:56:23 +01:00
Timothee
fe34a6f9c3 N°9144 Small fixes
> Prevent verification when some dependencies are missing
> Close unclosed div
> Change progress bar title depending on step
> Fix ignored "check uninstall check" flag
> Added phpunit tests to cover "check uninstall check" flag
> Progress bar appropriately reflect error status (red & not animated)
2026-02-23 18:04:49 +01:00
Timothee
24aec0d08d N°9144 Fix back from module choice 2026-02-23 15:03:31 +01:00
Timothee
a287527d29 N°9010 Fix Code Style 2026-02-19 15:45:33 +01:00
Timmy38
a42b061f19 N°9010 fix flags when extension is missing 2026-02-19 14:50:19 +01:00
Timmy38
c4d7c89553 N°9144 Add data audit setup step
N°8864 Fix unneeded char
2026-02-19 14:27:24 +01:00
odain
5b58e40fc9 N°8760 - fix installation choices db query - read code instead of title + add log 2026-02-08 20:34:40 +01:00
odain
2b21556c76 Merge branch 'feature/8760-audit-squashed' into feature/uninstallation 2026-02-06 16:48:26 +01:00
odain
77626f8159 N°8760 - Audit uninstall of extensions that declare final classes
N°8760 - be able to list modules based on extension choices
refactoring: move some classes in a moduleinstallation folder (coming
namespace)

N°8760 - module dependency check applied before audit

N°8760 - make dependency check work during audit

N°8760 - fix ci

N°8760 - fix ci

N°8760 - add GetCreatedIn to get module name based on DBObject class - everything stored in MetaModel during compilation and autoload

N°8760 - be able to describe from which module a datamodel class comes via MetaModel created_in field

N°8760 - rename GetCreatedIn <- GetModuleName + compute module name live instead having complex stuff in MetaModel/compilation

temp review 1

review: renaming InstallationChoicesToModuleConverter

review: renaming InstallationChoicesToModuleConverter

review: ModuleDiscovery:GetModulesOrderedByDependencies replacing deprecated GetAvailableModules method

ci: fix typo

cleanup

review: rework InstallationChoicesToModuleConverter

N°8760 - review tests
2026-02-06 16:48:00 +01:00
odain
bb6248a6e7 N°8764 - enhance setup wizards transition computation/tests 2026-01-28 14:47:52 +01:00
Molkobain
130d98aa3f N°9106 - Adapt extensions pills so they can be read correctly by Behat 2026-01-27 10:44:51 +01:00
odain
00c590232a N°8764 - fix setup wizards transitions - damned missing file 2026-01-23 09:47:52 +01:00
odain
97828225db N°8764 - fix setup wizards transitions 2026-01-23 09:28:54 +01:00
odain
03e59c9749 N°8764 - fix missing setup wizards titles + cache step computation 2026-01-22 15:54:15 +01:00
odain
985a49dc9f code style 2026-01-21 18:37:23 +01:00
odain
adae35ccc4 N°8764 - use last working model in case setup wizard to do setup audit - skip if no model available to make setup work 2026-01-21 17:11:11 +01:00
Timothee
f0c9629f5f N°8763 Add phpunit tests for iTopExtension::CanBeUninstalled 2026-01-21 10:27:49 +01:00
odain
4e96b297c2 N°8864 - code readability 2026-01-20 16:21:52 +01:00
odain
ae620c6663 N°8760 - Audit uninstall of extensions that declare final classes - refactor extension computations and move them in ExtensionMap
N°8760 - Audit uninstall of extensions that declare final classes - refactor extension computations and move them in ExtensionMap
2026-01-20 15:53:27 +01:00
odain
b5c51a2983 simplify WizardStep::GetTitle 2026-01-20 15:53:27 +01:00
odain
3b2d845c00 N°8981 - reduce log verbosity during setup 2026-01-20 15:53:27 +01:00
Timothee
36c545a6c4 N°8763 Fix non-uninstallable check in multi-modules extensions 2026-01-20 15:35:53 +01:00
odain
5ecb4936f0 Merge branch 'feature/8981-prepare' into feature/uninstallation 2026-01-14 10:13:42 +01:00
odain
cfc933b92b Merge branch 'feature/8981-prepare' into feature/uninstallation 2026-01-13 16:33:10 +01:00
110 changed files with 8933 additions and 4266 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.class.inc.php');
require_once(APPROOT.'/setup/moduleinstallation/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.class.inc.php');
MetaModel::IncludeModule('setup/moduleinstallation/moduleinstallation.class.inc.php');
MetaModel::IncludeModule('core/event.class.inc.php');
MetaModel::IncludeModule('core/action.class.inc.php');

View File

@@ -2766,7 +2766,7 @@ class Config
$oEmptyConfig = new Config('dummy_file', false); // Do NOT load any config file, just set the default values
$aAddOns = $oEmptyConfig->GetAddOns();
$aModules = ModuleDiscovery::GetAvailableModules([APPROOT.$sModulesDir]);
$aModules = ModuleDiscovery::GetModulesOrderedByDependencies([APPROOT.$sModulesDir]);
foreach ($aModules as $sModuleId => $aModuleInfo) {
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
if (is_null($aSelectedModules) || in_array($sModuleName, $aSelectedModules)) {

View File

@@ -22,6 +22,8 @@ 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';
@@ -468,11 +470,35 @@ abstract class MetaModel
* @return string
* @throws \CoreException
*/
final public static function GetCreatedIn($sClass)
final public static function GetModuleName($sClass)
{
self::_check_subclass($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';
}
return self::$m_aClassParams[$sClass]["created_in"] ?? "";
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';
}
/**
@@ -3158,7 +3184,6 @@ 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",

File diff suppressed because one or more lines are too long

View File

@@ -605,6 +605,7 @@ body {
color:#a00000;
}
.setup-extension-tag {
display: inline-flex;
background-color: grey;
border-radius: 8px;
padding-left: 3px;
@@ -681,9 +682,6 @@ body {
overflow: auto;
text-align: center;
}
#installation_progress {
display: none;
}
#fresh_content{
border: 0;
min-height: 300px;

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="Combodo\iTop\DataFeatureRemoval\" />
<sourceFolder url="file://$MODULE_DIR$/src/NoNamespace" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/combodo-data-feature-removal.iml" filepath="$PROJECT_DIR$/.idea/combodo-data-feature-removal.iml" />
</modules>
</component>
</project>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/vendor/composer" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.4">
<option name="suggestChangeDefaultLanguageLevel" value="false" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="0556f797-a2a3-4617-8eb0-c7985d4d9530" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ComposerSettings" synchronizationState="SYNCHRONIZE">
<pharConfigPath>$PROJECT_DIR$/composer.json</pharConfigPath>
<execution />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="PHP 7.2">
<include_path>
<path value="$PROJECT_DIR$/vendor/composer" />
</include_path>
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 1
}</component>
<component name="ProjectId" id="38XKfHC46lRTrwwYc7IxUdzfXo1" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="autoscrollFromSource" value="true" />
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"last_opened_file_path": "/home/combodo/workspaceHUB/HubInstallation",
"node.js.detected.package.eslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"nodejs_package_manager_path": "npm"
}
}]]></component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
{
"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

@@ -0,0 +1,163 @@
<?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

@@ -0,0 +1,41 @@
<?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

@@ -0,0 +1,40 @@
<?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

@@ -0,0 +1,20 @@
<?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

@@ -0,0 +1,16 @@
<?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

@@ -0,0 +1,54 @@
<?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

@@ -0,0 +1,189 @@
<?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

@@ -0,0 +1,30 @@
<?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

@@ -0,0 +1,13 @@
<?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

@@ -0,0 +1,25 @@
<?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

@@ -0,0 +1,70 @@
<?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

@@ -0,0 +1,122 @@
<?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

@@ -0,0 +1,8 @@
{# @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

@@ -0,0 +1,18 @@
{# @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

@@ -0,0 +1,28 @@
{# @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

@@ -0,0 +1,9 @@
{# @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

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

View File

@@ -0,0 +1,25 @@
<?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

@@ -0,0 +1,579 @@
<?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

@@ -0,0 +1,21 @@
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

@@ -0,0 +1,17 @@
<?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

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

View File

@@ -0,0 +1,11 @@
<?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

@@ -0,0 +1,37 @@
<?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

@@ -0,0 +1,48 @@
<?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);
}
}

14
setup/SetupDBBackup.php Normal file
View File

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

View File

@@ -138,8 +138,7 @@ try {
ini_set('display_errors', true);
ini_set('display_startup_errors', true);
require_once(APPROOT.'/setup/wizardcontroller.class.inc.php');
require_once(APPROOT.'/setup/wizardsteps.class.inc.php');
require_once(APPROOT.'/setup/wizardsteps_autoload.php');
$sClass = utils::ReadParam('step_class', '');
$sState = utils::ReadParam('step_state', '');

File diff suppressed because it is too large Load Diff

View File

@@ -477,7 +477,7 @@ class MFCompiler
$sClass = $oClass->getAttribute("id");
$aAllClasses[] = $sClass;
try {
$sCompiledCode .= $this->CompileClass($oClass, $sModuleName, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir);
$sCompiledCode .= $this->CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir);
} catch (DOMFormatException $e) {
$sMessage = "Failed to process class '$sClass', ";
if (!empty($sModuleRootDir)) {
@@ -1189,7 +1189,6 @@ EOF
/**
* @param \MFElement $oClass
* @param string $sModuleName
* @param string $sTempTargetDir
* @param string $sFinalTargetDir
* @param string $sModuleRelativeDir
@@ -1197,7 +1196,7 @@ EOF
* @return string
* @throws \DOMFormatException
*/
protected function CompileClass($oClass, $sModuleName, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir)
protected function CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir)
{
$sClass = $oClass->getAttribute('id');
$oProperties = $oClass->GetUniqueElement('properties');
@@ -1210,7 +1209,6 @@ 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';
}

View File

@@ -194,7 +194,7 @@ class iTopExtensionsMap
}
}
\ModuleDiscovery::DeclareRemovedExtensions($aRemovedExtension);
ModuleDiscovery::DeclareRemovedExtensions($aRemovedExtension);
}
/**
@@ -329,7 +329,7 @@ class iTopExtensionsMap
$aSearchDirs = array_merge($aSearchDirs, $this->aScannedDirs);
try {
ModuleDiscovery::GetAvailableModules($aSearchDirs, true);
ModuleDiscovery::GetModulesOrderedByDependencies($aSearchDirs, true);
} catch (MissingDependencyException $e) {
// Some modules have missing dependencies
// Let's check what is the impact at the "extensions" level
@@ -369,6 +369,75 @@ 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)
@@ -454,7 +523,7 @@ class iTopExtensionsMap
return true;
}
protected function LoadInstalledExtensionsFromDatabase(Config $oConfig): array|false
public function LoadInstalledExtensionsFromDatabase(Config $oConfig): array|false
{
try {
if (CMDBSource::DBName() === null) {
@@ -497,6 +566,27 @@ 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

@@ -2,10 +2,15 @@
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
{
@@ -37,14 +42,27 @@ 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);
$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
@@ -53,6 +71,27 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
$oExtensionsMap->DeclareExtensionAsRemoved($aExtensionCodes);
}
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;
}
$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()
{
$sEnv = $this->sFinalEnv;

View File

@@ -6,6 +6,7 @@ use ContextTag;
use CoreException;
use Exception;
use IssueLog;
use MetaModel;
use SetupLog;
use utils;
@@ -34,14 +35,29 @@ 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'));
$sOutput = "";
$iRes = 0;
exec(sprintf("$sPHPExec %s/get_model_reflection.php --env='%s'", __DIR__, $sEnv), $sOutput, $iRes);
$sCommandLine = sprintf("$sPHPExec %s/get_model_reflection.php --env=%s", __DIR__, escapeshellarg($sEnv));
exec($sCommandLine, $sOutput, $iRes);
if ($iRes != 0) {
$this->LogErrorWithProperLogger("Cannot get classes", null, ['env' => $sEnv, 'code' => $iRes, "output" => $sOutput]);
throw new CoreException("Cannot get classes");
throw new CoreException("Cannot get classes from env ".$sEnv);
}
$aClasses = json_decode($sOutput[0] ?? null, true);

View File

@@ -2,8 +2,6 @@
namespace Combodo\iTop\Setup\FeatureRemoval;
use MetaModel;
require_once __DIR__.'/AbstractSetupAudit.php';
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
@@ -28,33 +26,12 @@ class SetupAudit extends AbstractSetupAudit
return;
}
$sCurrentEnvt = MetaModel::GetEnvironment();
if ($sCurrentEnvt === $this->sEnvBefore) {
$this->aClassesBefore = MetaModel::GetClasses();
} else {
$this->aClassesBefore = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvBefore);
}
if ($sCurrentEnvt === $this->sEnvAfter) {
$this->aClassesAfter = MetaModel::GetClasses();
} else {
$this->aClassesAfter = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvAfter);
}
$this->aClassesBefore = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvBefore);
$this->aClassesAfter = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvAfter);
$this->bClassesInitialized = true;
}
/*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();

View File

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

View File

@@ -135,8 +135,9 @@ class iTopExtension
return $this->bCanBeUninstalled;
}
foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) {
$this->bCanBeUninstalled = $aModuleInfo['uninstallable'] === 'yes';
return $this->bCanBeUninstalled;
if ($aModuleInfo['uninstallable'] !== 'yes') {
return false;
}
}
return true;
}

View File

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

View File

@@ -61,7 +61,7 @@ class DependencyExpression
}
}
private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
public static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
{
if (!isset(self::$oPhpExpressionEvaluator)) {
self::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);

View File

@@ -96,7 +96,7 @@ class ModuleDiscovery
protected static $m_aModuleVersionByName = [];
/** @var array<\iTopExtension> $m_aRemovedExtensions */
protected static $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'];
@@ -196,21 +196,6 @@ 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
@@ -238,6 +223,7 @@ class ModuleDiscovery
}
}
}
return ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aFilteredModules, $bAbortOnMissingDependency);
}
@@ -245,7 +231,7 @@ class ModuleDiscovery
* @param array<\iTopExtension> $aRemovedExtension
* @return void
*/
public static function DeclareRemovedExtensions(array $aRemovedExtension)
public static function DeclareRemovedExtensions(array $aRemovedExtension): void
{
if (self::$m_aRemovedExtensions != $aRemovedExtension) {
self::ResetCache();
@@ -253,79 +239,7 @@ class ModuleDiscovery
self::$m_aRemovedExtensions = $aRemovedExtension;
}
/**
* @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::Info("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;
}
private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
{
if (!isset(self::$oPhpExpressionEvaluator)) {
self::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
}
return self::$oPhpExpressionEvaluator;
}
/**
* 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)
private static function Init($aSearchDirs): void
{
if (self::$m_aSearchDirs != $aSearchDirs) {
self::ResetCache();
@@ -344,13 +258,60 @@ 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;
@@ -419,6 +380,59 @@ 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

@@ -73,7 +73,7 @@ class AnalyzeInstallation
//test only
$aAvailableModules = $this->aAvailableModules;
} else {
$aAvailableModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
$aAvailableModules = ModuleDiscovery::GetModulesOrderedByDependencies($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
}
foreach ($aAvailableModules as $sModuleId => $aModuleInfo) {

View File

@@ -0,0 +1,215 @@
<?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

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

View File

@@ -32,7 +32,8 @@ 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/AnalyzeInstallation.php';
require_once APPROOT.'setup/moduleinstallation/AnalyzeInstallation.php';
require_once APPROOT.'/setup/moduleinstallation/InstallationChoicesToModuleConverter.php';
define('MODULE_ACTION_OPTIONAL', 1);
define('MODULE_ACTION_MANDATORY', 2);
@@ -129,7 +130,7 @@ class RunTimeEnvironment
*/
public function InitDataModel($oConfig, $bModelOnly = true, $bUseCache = false): void
{
require_once APPROOT.'/setup/moduleinstallation.class.inc.php';
require_once APPROOT.'/setup/moduleinstallation/moduleinstallation.class.inc.php';
$sConfigFile = $oConfig->GetLoadedFile();
if (strlen($sConfigFile) > 0) {
@@ -225,12 +226,30 @@ 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)");

View File

@@ -0,0 +1,980 @@
<?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

@@ -0,0 +1,203 @@
<?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

@@ -0,0 +1,106 @@
<?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);
}

View File

@@ -25,21 +25,17 @@ function WizardAsyncAction(sActionCode, oParams, OnErrorFunction)
function WizardUpdateButtons()
{
if (CanMoveForward())
{
if (CanMoveForward()) {
$("#btn_next").prop('disabled', false);
}
else
{
else {
$("#btn_next").prop('disabled', true);
}
if (CanMoveBackward())
{
if (CanMoveBackward()) {
$("#btn_back").prop('disabled', false);
}
else
{
else {
$("#btn_back").prop('disabled', true);
}
}

View File

@@ -509,7 +509,7 @@ class SetupUtils
}
require_once(APPROOT.'setup/modulediscovery.class.inc.php');
try {
ModuleDiscovery::GetAvailableModules($aDirsToScan, true, $aSelectedModules);
ModuleDiscovery::GetModulesOrderedByDependencies($aDirsToScan, true, $aSelectedModules);
} catch (Exception $e) {
$aResult[] = new CheckResult(CheckResult::ERROR, $e->getMessage());
}
@@ -2150,7 +2150,7 @@ class SetupInfo
/**
* Called by the setup process to initializes the list of selected modules. Do not call this method
* from an 'auto_select' rule
* @param hash $aModules
* @param array $aModules
* @return void
*/
public static function SetSelectedModules($aModules)

View File

@@ -5,8 +5,7 @@ use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
require_once(APPROOT.'/application/utils.inc.php');
require_once(APPROOT.'/setup/setuppage.class.inc.php');
require_once(APPROOT.'/setup/wizardcontroller.class.inc.php');
require_once(APPROOT.'/setup/wizardsteps.class.inc.php');
require_once(APPROOT.'/setup/wizardsteps_autoload.php');
class InstallationFileService
{
@@ -259,7 +258,7 @@ class InstallationFileService
{
$sProductionModuleDir = APPROOT.'data/'.$this->sTargetEnvironment.'-modules/';
$aAvailableModules = $this->GetProductionEnv()->AnalyzeInstallation(MetaModel::GetConfig(), $this->GetExtraDirs(), false, null);
$aAvailableModules = $this->GetProductionEnv()->AnalyzeInstallation(MetaModel::GetConfig(), $this->GetExtraDirs());
$this->aAutoSelectModules = [];
foreach ($aAvailableModules as $sModuleId => $aModule) {

View File

@@ -272,7 +272,7 @@ $bFoundIssues = false;
$bInstall = utils::ReadParam('install', true, true /* CLI allowed */);
if ($bInstall) {
echo "Starting the unattended installation...\n";
$oWizard = new ApplicationInstaller($oParams);
$oWizard = new ApplicationInstallSequencer($oParams);
$bRes = $oWizard->ExecuteAllSteps();
if (!$bRes) {
echo "\nencountered installation issues!";

View File

@@ -31,8 +31,7 @@ require_once('../approot.inc.php');
require_once(APPROOT.'/application/utils.inc.php');
require_once(APPROOT.'/core/config.class.inc.php');
require_once(APPROOT.'/setup/setuppage.class.inc.php');
require_once(APPROOT.'/setup/wizardcontroller.class.inc.php');
require_once(APPROOT.'/setup/wizardsteps.class.inc.php');
require_once(APPROOT.'/setup/wizardsteps_autoload.php');
Session::Start();
clearstatcache(); // Make sure we know what we are doing !

View File

@@ -18,6 +18,12 @@
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Application\WebPage\WebPage;
require_once(APPROOT.'setup/setuputils.class.inc.php');
require_once(APPROOT.'setup/parameters.class.inc.php');
require_once(APPROOT.'setup/applicationinstaller.class.inc.php');
require_once(APPROOT.'core/mutex.class.inc.php');
require_once(APPROOT.'setup/extensionsmap.class.inc.php');
/**
* Engine for displaying the various pages of a "wizard"
* Each "step" of the wizard must be implemented as
@@ -53,7 +59,7 @@ class WizardController
/**
* Pushes information about the current step onto the stack
* @param hash $aStepInfo Array('class' => , 'state' => )
* @param array $aStepInfo Array('class' => , 'state' => )
*/
protected function PushStep($aStepInfo)
{
@@ -133,7 +139,7 @@ class WizardController
public function Start()
{
$sCurrentStepClass = $this->sInitialStepClass;
$oStep = new $sCurrentStepClass($this, $this->sInitialState);
$oStep = $this->GetWizardStep($sCurrentStepClass, $this->sInitialState);
$this->DisplayStep($oStep);
}
/**
@@ -145,21 +151,24 @@ class WizardController
$sCurrentStepClass = utils::ReadParam('_class', $this->sInitialStepClass);
$sCurrentState = utils::ReadParam('_state', $this->sInitialState);
/** @var \WizardStep $oStep */
$oStep = new $sCurrentStepClass($this, $sCurrentState);
$oStep = $oStep = $this->GetWizardStep($sCurrentStepClass, $sCurrentState);
if ($oStep->ValidateParams()) {
$this->PushStep(['class' => $sCurrentStepClass, 'state' => $sCurrentState]);
if ($oStep->CanComeBack()) {
$this->PushStep(['class' => $sCurrentStepClass, 'state' => $sCurrentState]);
}
$aPossibleSteps = $oStep->GetPossibleSteps();
$aNextStepInfo = $oStep->ProcessParams(true); // true => moving forward
if (in_array($aNextStepInfo['class'], $aPossibleSteps)) {
$oNextStep = new $aNextStepInfo['class']($this, $aNextStepInfo['state']);
$oWizardState = $oStep->UpdateWizardStateAndGetNextStep(true); // true => moving forward
if (in_array($oWizardState->GetNextStep(), $aPossibleSteps)) {
$oNextStep = $this->GetWizardStep($oWizardState->GetNextStep(), $oWizardState->GetState());
$this->DisplayStep($oNextStep);
} else {
throw new Exception("Internal error: Unexpected next step '{$aNextStepInfo['class']}'. The possible next steps are: ".implode(', ', $aPossibleSteps));
throw new Exception("Internal error: Unexpected next step '{$oWizardState->GetNextStep()}'. The possible next steps are: ".implode(', ', $aPossibleSteps));
}
} else {
$this->DisplayStep($oStep);
}
}
/**
* Move one step back
*/
@@ -168,12 +177,12 @@ class WizardController
// let the current step save its parameters
$sCurrentStepClass = utils::ReadParam('_class', $this->sInitialStepClass);
$sCurrentState = utils::ReadParam('_state', $this->sInitialState);
$oStep = new $sCurrentStepClass($this, $sCurrentState);
$aNextStepInfo = $oStep->ProcessParams(false); // false => Moving backwards
$oStep = $this->GetWizardStep($sCurrentStepClass, $sCurrentState);
$oWizardState = $oStep->UpdateWizardStateAndGetNextStep(false); // false => Moving backwards
// Display the previous step
$aCurrentStepInfo = $this->PopStep();
$oStep = new $aCurrentStepInfo['class']($this, $aCurrentStepInfo['state']);
$oStep = $this->GetWizardStep($aCurrentStepInfo['class'], $aCurrentStepInfo['state']);
$this->DisplayStep($oStep);
}
@@ -315,7 +324,7 @@ on the page's parameters
$sStep = $this->sInitialStepClass;
}
$oStep = new $sStep($this, '');
$oStep = $this->GetWizardStep($sStep);
$aAllSteps[$sStep] = $oStep->GetPossibleSteps();
foreach ($aAllSteps[$sStep] as $sNextStep) {
if (!array_key_exists($sNextStep, $aAllSteps)) {
@@ -347,7 +356,7 @@ on the page's parameters
$sOutput .= "\tnode [shape = doublecircle]; ".implode(' ', $aDeadEnds).";\n";
$sOutput .= "\tnode [shape = box];\n";
foreach ($aAllSteps as $sStep => $aNextSteps) {
$oStep = new $sStep($this, '');
$oStep = $this->GetWizardStep($sStep);
$sOutput .= "\t$sStep [ label = \"".$oStep->GetTitle()."\"];\n";
if (count($aNextSteps) > 0) {
foreach ($aNextSteps as $sNextStep) {
@@ -358,314 +367,19 @@ on the page's parameters
$sOutput .= "}\n";
return $sOutput;
}
}
/**
* Abstract class to build "steps" for the wizard controller
* If a step needs to maintain an internal "state" (for complex steps)
* then it's up to the derived class to implement the behavior based on
* the internal 'sCurrentState' variable.
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
abstract class WizardStep
{
/**
* A reference to the WizardController
* @var WizardController
* @param string $sCurrentStepClass
* @param string $sCurrentState
*
* @return \WizardStep
* @throws \Exception
*/
protected $oWizard;
/**
* Current 'state' of the wizard step. Simple 'steps' can ignore it
* @var string
*/
protected $sCurrentState;
public function __construct(WizardController $oWizard, $sCurrentState)
private function GetWizardStep(string $sCurrentStepClass, string $sCurrentState = ''): WizardStep
{
$this->oWizard = $oWizard;
$this->sCurrentState = $sCurrentState;
}
public function GetState()
{
return $this->sCurrentState;
}
/**
* Displays the wizard page for the current class/state
* The page can contain any number of "<input/>" fields, but no "<form>...</form>" tag
* The name of the input fields (and their id if one is supplied) MUST NOT start with "_"
* (this is reserved for the wizard's own parameters)
* @return void
*/
abstract public function Display(WebPage $oPage);
/**
* Processes the page's parameters and (if moving forward) returns the next step/state to be displayed
* @param bool $bMoveForward True if the wizard is moving forward 'Next >>' button pressed, false otherwise
* @return hash array('class' => $sNextClass, 'state' => $sNextState)
*/
abstract public function ProcessParams($bMoveForward = true);
/**
* Returns the list of possible steps from this step forward
* @return array Array of strings (step classes)
*/
abstract public function GetPossibleSteps();
/**
* Returns title of the current step
* @return string The title of the wizard page for the current step
*/
abstract public function GetTitle();
/**
* Tells whether the parameters are Ok to move forward
* @return boolean True to move forward, false to stey on the same step
*/
public function ValidateParams()
{
return true;
}
/**
* Tells whether this step/state is the last one of the wizard (dead-end)
* @return boolean True if the 'Next >>' button should be displayed
*/
public function CanMoveForward()
{
return true;
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return 'return true;';
}
/**
* Returns the label for the " Next >> " button
* @return string The label for the button
*/
public function GetNextButtonLabel()
{
return 'Next';
}
/**
* Tells whether this step/state allows to go back or not
* @return boolean True if the '<< Back' button should be displayed
*/
public function CanMoveBackward()
{
return true;
}
/**
* Tells whether the "Back" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveBackward()
{
return 'return true;';
}
/**
* Tells whether this step of the wizard requires that the configuration file be writable
* @return bool True if the wizard will possibly need to modify the configuration at some point
*/
public function RequiresWritableConfig()
{
return true;
}
/**
* Overload this function to implement asynchronous action(s) (AJAX)
* @param string $sCode The code of the action (if several actions need to be distinguished)
* @param hash $aParameters The action's parameters name => value
*/
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
}
}
/*
* Example of a simple Setup Wizard with some parameters to store
* the installation mode (install | upgrade) and a simple asynchronous
* (AJAX) action.
*
* The setup wizard is executed by the following code:
*
* $oWizard = new WizardController('Step1');
* $oWizard->Run();
*
class Step1 extends WizardStep
{
public function GetTitle()
{
return 'Welcome';
}
public function GetPossibleSteps()
{
return array('Step2', 'Step2bis');
}
public function ProcessParams($bMoveForward = true)
{
$sNextStep = '';
$sInstallMode = utils::ReadParam('install_mode');
if ($sInstallMode == 'install')
{
$this->oWizard->SetParameter('install_mode', 'install');
$sNextStep = 'Step2';
if (!is_subclass_of($sCurrentStepClass, WizardStep::class)) {
throw new Exception('Unknown step '.$sCurrentStepClass);
}
else
{
$this->oWizard->SetParameter('install_mode', 'upgrade');
$sNextStep = 'Step2bis';
}
return array('class' => $sNextStep, 'state' => '');
}
public function Display(WebPage $oPage)
{
$oPage->p('This is Step 1!');
$sInstallMode = $this->oWizard->GetParameter('install_mode', 'install');
$sChecked = ($sInstallMode == 'install') ? ' checked ' : '';
$oPage->p('<input type="radio" name="install_mode" value="install"'.$sChecked.'/> Install');
$sChecked = ($sInstallMode == 'upgrade') ? ' checked ' : '';
$oPage->p('<input type="radio" name="install_mode" value="upgrade"'.$sChecked.'/> Upgrade');
return new $sCurrentStepClass($this, $sCurrentState);
}
}
class Step2 extends WizardStep
{
public function GetTitle()
{
return 'Installation Parameters';
}
public function GetPossibleSteps()
{
return array('Step3');
}
public function ProcessParams($bMoveForward = true)
{
return array('class' => 'Step3', 'state' => '');
}
public function Display(WebPage $oPage)
{
$oPage->p('This is Step 2! (Installation)');
}
}
class Step2bis extends WizardStep
{
public function GetTitle()
{
return 'Upgrade Parameters';
}
public function GetPossibleSteps()
{
return array('Step2ter');
}
public function ProcessParams($bMoveForward = true)
{
$sUpgradeInfo = utils::ReadParam('upgrade_info');
$this->oWizard->SetParameter('upgrade_info', $sUpgradeInfo);
$sAdditionalUpgradeInfo = utils::ReadParam('additional_upgrade_info');
$this->oWizard->SetParameter('additional_upgrade_info', $sAdditionalUpgradeInfo);
return array('class' => 'Step2ter', 'state' => '');
}
public function Display(WebPage $oPage)
{
$oPage->p('This is Step 2bis! (Upgrade)');
$sUpgradeInfo = $this->oWizard->GetParameter('upgrade_info', '');
$oPage->p('Type your name here: <input type="text" id="upgrade_info" name="upgrade_info" value="'.$sUpgradeInfo.'" size="20"/><span id="v_upgrade_info"></span>');
$sAdditionalUpgradeInfo = $this->oWizard->GetParameter('additional_upgrade_info', '');
$oPage->p('The installer replies: <input type="text" name="additional_upgrade_info" value="'.$sAdditionalUpgradeInfo.'" size="20"/>');
$oPage->add_ready_script("$('#upgrade_info').change(function() {
$('#v_upgrade_info').html('<img src=\"../images/indicator.gif\"/>');
WizardAsyncAction('', { upgrade_info: $('#upgrade_info').val() }); });");
}
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
usleep(300000); // 300 ms
$sName = $aParameters['upgrade_info'];
$sReply = addslashes("Hello ".$sName);
$oPage->add_ready_script(
<<<EOF
$("#v_upgrade_info").html('');
$("input[name=additional_upgrade_info]").val("$sReply");
EOF
);
}
}
class Step2ter extends WizardStep
{
public function GetTitle()
{
return 'Additional Upgrade Info';
}
public function GetPossibleSteps()
{
return array('Step3');
}
public function ProcessParams($bMoveForward = true)
{
return array('class' => 'Step3', 'state' => '');
}
public function Display(WebPage $oPage)
{
$oPage->p('This is Step 2ter! (Upgrade)');
}
}
class Step3 extends WizardStep
{
public function GetTitle()
{
return 'Installation Complete';
}
public function GetPossibleSteps()
{
return array();
}
public function ProcessParams($bMoveForward = true)
{
return array('class' => '', 'state' => '');
}
public function Display(WebPage $oPage)
{
$oPage->p('This is the FINAL Step');
}
public function CanMoveForward()
{
return false;
}
}
End of the example */

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,111 @@
<?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 AbstractWizStepInstall extends WizardStep
{
/**
* Prepare the parameters to execute the installation asynchronously
* @return array A big hash array that can be converted to XML or JSON with all the needed parameters
*/
protected function BuildConfig()
{
$sMode = $this->oWizard->GetParameter('install_mode', 'install');
$aSelectedModules = json_decode($this->oWizard->GetParameter('selected_modules'), true);
$aSelectedExtensions = json_decode($this->oWizard->GetParameter('selected_extensions'), true);
$sBackupDestination = '';
$sPreviousConfigurationFile = '';
$sDBName = $this->oWizard->GetParameter('db_name');
if ($sMode == 'upgrade') {
$sPreviousVersionDir = $this->oWizard->GetParameter('previous_version_dir', '');
if (!empty($sPreviousVersionDir)) {
$aPreviousInstance = SetupUtils::GetPreviousInstance($sPreviousVersionDir);
if ($aPreviousInstance['found']) {
$sPreviousConfigurationFile = $aPreviousInstance['configuration_file'];
}
}
if ($this->oWizard->GetParameter('db_backup', false)) {
$sBackupDestination = $this->oWizard->GetParameter('db_backup_path', '');
}
} else {
$sDBNewName = $this->oWizard->GetParameter('db_new_name', '');
if ($sDBNewName != '') {
$sDBName = $sDBNewName; // Database will be created
}
}
$sSourceDir = $this->oWizard->GetParameter('source_dir');
$aCopies = [];
if (($sMode == 'upgrade') && ($this->oWizard->GetParameter('upgrade_type') == 'keep-previous')) {
$sPreviousVersionDir = $this->oWizard->GetParameter('previous_version_dir');
$aCopies[] = ['source' => $sSourceDir, 'destination' => 'modules']; // Source is an absolute path, destination is relative to APPROOT
$aCopies[] = ['source' => $sPreviousVersionDir.'/portal', 'destination' => 'portal']; // Source is an absolute path, destination is relative to APPROOT
$sSourceDir = APPROOT.'modules';
}
$aInstallParams = [
'mode' => $sMode,
'preinstall' => [
'copies' => $aCopies,
// 'backup' => see below
],
'source_dir' => str_replace(APPROOT, '', $sSourceDir),
'datamodel_version' => $this->oWizard->GetParameter('datamodel_version'), //TODO: let the installer compute this automatically...
'previous_configuration_file' => $sPreviousConfigurationFile,
'extensions_dir' => 'extensions',
'target_env' => 'production',
'workspace_dir' => '',
'database' => [
'server' => $this->oWizard->GetParameter('db_server'),
'user' => $this->oWizard->GetParameter('db_user'),
'pwd' => $this->oWizard->GetParameter('db_pwd'),
'name' => $sDBName,
'db_tls_enabled' => $this->oWizard->GetParameter('db_tls_enabled'),
'db_tls_ca' => $this->oWizard->GetParameter('db_tls_ca'),
'prefix' => $this->oWizard->GetParameter('db_prefix'),
],
'url' => $this->oWizard->GetParameter('application_url'),
'graphviz_path' => $this->oWizard->GetParameter('graphviz_path'),
'admin_account' => [
'user' => $this->oWizard->GetParameter('admin_user'),
'pwd' => $this->oWizard->GetParameter('admin_pwd'),
'language' => $this->oWizard->GetParameter('admin_language'),
],
'language' => $this->oWizard->GetParameter('default_language'),
'selected_modules' => $aSelectedModules,
'selected_extensions' => $aSelectedExtensions,
'sample_data' => $this->oWizard->GetParameter('sample_data', '') === 'yes',
'old_addon' => $this->oWizard->GetParameter('old_addon', false), // whether or not to use the "old" userrights profile addon
'options' => json_decode($this->oWizard->GetParameter('misc_options', '[]'), true),
'mysql_bindir' => $this->oWizard->GetParameter('mysql_bindir'),
];
if ($sBackupDestination != '') {
$aInstallParams['preinstall']['backup'] = [
'destination' => $sBackupDestination,
'configuration_file' => $sPreviousConfigurationFile,
];
}
return $aInstallParams;
}
}

View File

@@ -0,0 +1,73 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* @since 3.0.0 N°4092
*/
abstract class AbstractWizStepMiscParams extends WizardStep
{
/**
* @since 3.0.0 N°4092
*/
final protected function AddUseSymlinksFlagOption(WebPage $oPage): void
{
if (MFCompiler::CanUseSymbolicLinksFlagBeUsed()) {
$sChecked = (MFCompiler::IsUseSymbolicLinksFlagPresent()) ? ' checked' : '';
$oPage->add('<fieldset>');
$oPage->add('<legend>Dev parameters</legend>');
$oPage->p('<input id="use-symbolic-links" type="checkbox"'.$sChecked.'><label for="use-symbolic-links">&nbsp;Create symbolic links instead of creating a copy in env-production (useful for debugging extensions)');
$oPage->add('</fieldset>');
$oPage->add_ready_script(
<<<'JS'
$("#use-symbolic-links").on("click", function() {
var $this = $(this),
bUseSymbolicLinks = $this.prop("checked");
var sAuthent = $('#authent_token').val();
var oAjaxParams = { operation: 'toggle_use_symbolic_links', bUseSymbolicLinks: bUseSymbolicLinks, authent: sAuthent};
$.post(GetAbsoluteUrlAppRoot()+'setup/ajax.dataloader.php', oAjaxParams);
});
JS
);
}
}
final protected function AddForceUninstallFlagOption(WebPage $oPage): void
{
$sChecked = $this->oWizard->GetParameter('force-uninstall', false) ? ' checked ' : '';
$oPage->add('<fieldset>');
$oPage->add('<legend>Advanced parameters</legend>');
$oPage->p('<input id="force-uninstall" type="checkbox"'.$sChecked.' name="force-uninstall"><label for="force-uninstall">&nbsp;Disable uninstallation checks for extensions');
$oPage->add('</fieldset>');
$oPage->add_ready_script(
<<<'JS'
$("#force-uninstall").on("click", function() {
let $this = $(this);
let bForceUninstall = $this.prop("checked");
if( bForceUninstall && !confirm('Beware, uninstalling extensions flagged as non uninstallable may result in data corruption and application crashes. Are you sure you want to continue ?')){
$this.prop("checked",false);
}
});
JS
);
}
}

View File

@@ -0,0 +1,109 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* Administrator Account definition screen
*/
class WizStepAdminAccount extends WizardStep
{
public function GetTitle()
{
return 'Administrator Account';
}
public function GetPossibleSteps()
{
return [WizStepInstallMiscParams::class];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$this->oWizard->SaveParameter('admin_user', '');
$this->oWizard->SaveParameter('admin_pwd', '');
$this->oWizard->SaveParameter('confirm_pwd', '');
$this->oWizard->SaveParameter('admin_language', 'EN US');
return new WizardState(WizStepInstallMiscParams::class);
}
public function Display(WebPage $oPage)
{
$sAdminUser = $this->oWizard->GetParameter('admin_user', 'admin');
$sAdminPwd = $this->oWizard->GetParameter('admin_pwd', '');
$sConfirmPwd = $this->oWizard->GetParameter('confirm_pwd', '');
$sAdminLanguage = $this->oWizard->GetParameter('admin_language', 'EN US');
$oPage->add('<h2>Definition of the Administrator Account</h2>');
$oPage->add('<fieldset>');
$oPage->add('<legend>Administrator Account</legend>');
$oPage->add('<table>');
$oPage->add('<tr><td>Login: </td><td><input id="admin_user" class="ibo-input" name="admin_user" type="text" size="25" maxlength="64" value="'.utils::EscapeHtml($sAdminUser).'"><span id="v_admin_user"/></td></tr>');
$oPage->add('<tr><td>Password: </td><td><input id="admin_pwd" class="ibo-input" autocomplete="off" name="admin_pwd" type="password" size="25" maxlength="64" value="'.utils::EscapeHtml($sAdminPwd).'"><span id="v_admin_pwd"/></td></tr>');
$oPage->add('<tr><td>Confirm password: </td><td><input id="confirm_pwd" class="ibo-input" autocomplete="off" name="confirm_pwd" type="password" size="25" maxlength="64" value="'.utils::EscapeHtml($sConfirmPwd).'"></td></tr>');
$sSourceDir = APPROOT.'dictionaries/';
$aLanguages = SetupUtils::GetAvailableLanguages($sSourceDir);
$oPage->add('<tr><td>Language: </td><td>');
$oPage->add(SetupUtils::GetLanguageSelect($sSourceDir, 'admin_language', $sAdminLanguage));
$oPage->add('</td></tr>');
$oPage->add('</table>');
$oPage->add('</fieldset>');
$oPage->add_ready_script(
<<<EOF
$('#admin_user').on('change keyup', function() { WizardUpdateButtons(); } );
$('#admin_pwd').on('change keyup', function() { WizardUpdateButtons(); } );
$('#confirm_pwd').on('change keyup', function() { WizardUpdateButtons(); } );
EOF
);
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return
<<<EOF
bRet = ($('#admin_user').val() != '');
if (!bRet)
{
$("#v_admin_user").html('<i class="fas fa-exclamation-triangle setup-invalid-field--icon" title="This field cannot be empty"></i>');
}
else
{
$("#v_admin_user").html('');
}
bPasswordsMatch = ($('#admin_pwd').val() == $('#confirm_pwd').val());
if (!bPasswordsMatch)
{
$('#v_admin_pwd').html('<i class="fas fa-exclamation-triangle setup-invalid-field--icon" title="Retyped password does not match"></i>');
}
else
{
$('#v_admin_pwd').html('');
}
bRet = bPasswordsMatch && bRet;
return bRet;
EOF
;
}
}

View File

@@ -0,0 +1,118 @@
<?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
*/
/**
* Database Connection parameters screen
*/
use Combodo\iTop\Application\WebPage\WebPage;
class WizStepDBParams extends WizardStep
{
public function GetTitle()
{
return 'Database Configuration';
}
public function GetPossibleSteps()
{
return [WizStepAdminAccount::class];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$this->oWizard->SaveParameter('db_server', '');
$this->oWizard->SaveParameter('db_user', '');
$this->oWizard->SaveParameter('db_pwd', '');
$this->oWizard->SaveParameter('db_name', '');
$this->oWizard->SaveParameter('db_prefix', '');
$this->oWizard->SaveParameter('new_db_name', '');
$this->oWizard->SaveParameter('create_db', '');
$this->oWizard->SaveParameter('db_new_name', '');
$this->oWizard->SaveParameter('db_tls_enabled', false);
$this->oWizard->SaveParameter('db_tls_ca', '');
return new WizardState(WizStepAdminAccount::class);
}
public function Display(WebPage $oPage)
{
$oPage->add('<h2>Configuration of the database connection:</h2>');
$sDBServer = $this->oWizard->GetParameter('db_server', '');
$sDBUser = $this->oWizard->GetParameter('db_user', '');
$sDBPwd = $this->oWizard->GetParameter('db_pwd', '');
$sDBName = $this->oWizard->GetParameter('db_name', '');
$sDBPrefix = $this->oWizard->GetParameter('db_prefix', '');
$sTlsEnabled = $this->oWizard->GetParameter('db_tls_enabled', '');
$sTlsCA = $this->oWizard->GetParameter('db_tls_ca', '');
$sNewDBName = $this->oWizard->GetParameter('db_new_name', false);
$oPage->add('<table>');
SetupUtils::DisplayDBParameters(
$oPage,
true,
$sDBServer,
$sDBUser,
$sDBPwd,
$sDBName,
$sDBPrefix,
$sTlsEnabled,
$sTlsCA,
$sNewDBName
);
$sAuthentToken = $this->oWizard->GetParameter('authent', '');
$oPage->add('<input type="hidden" id="authent_token" value="'.$sAuthentToken.'"/>');
$oPage->add('</table>');
$sCreateDB = $this->oWizard->GetParameter('create_db', 'yes');
if ($sCreateDB == 'no') {
$oPage->add_ready_script('$("#existing_db").prop("checked", true);');
} else {
$oPage->add_ready_script('$("#create_db").prop("checked", true);');
}
}
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
switch ($sCode) {
case 'check_db':
SetupUtils::AsyncCheckDB($oPage, $aParameters);
break;
}
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return
<<<EOF
if ($("#wiz_form").data("db_connection") === "error") return false;
var bRet = true;
bRet = ValidateField("db_name", true) && bRet;
bRet = ValidateField("db_new_name", true) && bRet;
bRet = ValidateField("db_prefix", true) && bRet;
return bRet;
EOF
;
}
}

View File

@@ -0,0 +1,123 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
require_once(APPROOT.'setup/sequencers/DataAuditSequencer.php');
/**
* @since 3.3.0
*/
class WizStepDataAudit extends WizStepInstall
{
public const SequencerClass = DataAuditSequencer::class;
public function GetTitle()
{
return 'Checking compatibility';
}
public function GetPossibleSteps()
{
return [WizStepSummary::class];
}
public function GetNextButtonLabel()
{
return 'Next';
}
public function CanMoveForward()
{
if ($this->CheckDependencies()) {
return true;
} else {
return false;
}
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
return new WizardState(WizStepSummary::class);
}
public function CanComeBack()
{
return false;
}
public function Display(WebPage $oPage)
{
$aInstallParams = $this->BuildConfig();
$this->AddProgressBar($oPage, 'Progress of the verification');
$sJSONData = json_encode($aInstallParams);
$oPage->add('<input type="hidden" id="installer_parameters" value="'.utils::EscapeHtml($sJSONData).'"/>');
$sAuthentToken = $this->oWizard->GetParameter('authent', '');
$oPage->add('<input type="hidden" id="authent_token" value="'.$sAuthentToken.'"/>');
$sApplicationUrl = $this->oWizard->GetParameter('application_url').'pages/exec.php?exec_module=combodo-data-feature-removal&exec_page=index.php';
$oPage->add('<input type="hidden" id="application_url" value="'.$sApplicationUrl.'"/>');
if (!$this->CheckDependencies()) {
$oPage->error($this->sDependencyIssue);
$oPage->add_ready_script(<<<JS
$("#wiz_form").data("installation_status", "error");
document.getElementById("setup_msg").innerText = "Unmet dependencies";
JS);
} else {
$oPage->add_ready_script(<<<JS
$("#wiz_form").data("installation_status", "not started");
ExecuteStep("");
JS);
}
}
protected function AddProgressErrorScript($oPage, $aRes)
{
if (isset($aRes['error_code']) && $aRes['error_code'] === DataAuditSequencer::DATA_AUDIT_FAILED) {
$oPage->add_ready_script(
<<<EOF
$('.ibo-setup--wizard--buttons-container tr td:nth-child(2)').before('<td style="text-align:center;"><button class="ibo-button ibo-is-alternative ibo-is-neutral" type="submit" name="operation" value="next"><span class="ibo-button--label">Ignore and continue</span></button></td>');
$('.ibo-setup--wizard--buttons-container tr td:nth-child(2)').after('<td style="text-align:center;"><a href="'+$('#application_url').val()+'"><button class="default ibo-button ibo-is-regular ibo-is-primary" type="button"><span class="ibo-button--label">Go to backoffice</span></button></a></td>');
$("#wiz_form").data("installation_status", "cleanup_needed");
$('#btn_next').hide();
EOF
);
}
}
public function JSCanMoveForward()
{
return 'return ["completed", "cleanup_needed"].indexOf($("#wiz_form").data("installation_status")) !== -1;';
}
public function JSCanMoveBackward()
{
return 'return ["not started", "error", "cleanup_needed"].indexOf($("#wiz_form").data("installation_status")) !== -1;';
}
}

View File

@@ -0,0 +1,258 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* Upgrade information
*/
class WizStepDetectedInfo extends WizardStep
{
protected $bCanMoveForward;
public function GetTitle()
{
return 'Upgrade Information';
}
public function GetPossibleSteps()
{
return [WizStepUpgradeMiscParams::class, WizStepLicense2::class];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$sUpgradeType = utils::ReadParam('upgrade_type');
$this->oWizard->SetParameter('mode', 'upgrade');
$this->oWizard->SetParameter('upgrade_type', $sUpgradeType);
$bDisplayLicense = $this->oWizard->GetParameter('display_license');
switch ($sUpgradeType) {
case 'keep-previous':
$sSourceDir = utils::ReadParam('relative_source_dir', '', false, 'raw_data');
$this->oWizard->SetParameter('source_dir', $this->oWizard->GetParameter('previous_version_dir').'/'.$sSourceDir);
$this->oWizard->SetParameter('datamodel_version', utils::ReadParam('datamodel_previous_version', '', false, 'raw_data'));
break;
case 'use-compatible':
$sDataModelPath = utils::ReadParam('datamodel_path', '', false, 'raw_data');
$this->oWizard->SetParameter('source_dir', $sDataModelPath);
$this->oWizard->SaveParameter('datamodel_version', '');
break;
default:
// Do nothing, maybe the user pressed the Back button
}
if ($bDisplayLicense) {
$aRet = new WizardState(WizStepLicense2::class);
} else {
$aRet = new WizardState(WizStepUpgradeMiscParams::class);
}
return $aRet;
}
/**
* @param WebPage $oPage
*
* @throws Exception
*/
public function Display(WebPage $oPage)
{
$oPage->add_style(
<<<EOF
#changes_summary {
max-height: 200px;
overflow: auto;
}
#changes_summary div {
width:100;
margin-top:0;
padding-top: 0.5em;
padding-left: 0;
}
#changes_summary div ul {
margin-left:0;
padding-left: 20px;
}
#changes_summary div.closed ul {
display:none;
}
#changes_summary div li {
list-style: none;
width: 100;
margin-left:0;
padding-left: 0em;
}
.title {
padding-left: 20px;
font-weight: bold;
cursor: pointer;
background: url(../images/minus.gif) 2px 2px no-repeat;
}
#changes_summary div.closed .title {
background: url(../images/plus.gif) 2px 2px no-repeat;
}
EOF
);
$this->bCanMoveForward = true;
$bDisplayLicense = true;
$sPreviousVersionDir = $this->oWizard->GetParameter('previous_version_dir', '');
$aInstalledInfo = SetupUtils::GetApplicationVersion($this->oWizard);
if ($aInstalledInfo === false) {
throw(new Exception('No previous version of '.ITOP_APPLICATION.' found in the supplied database. The upgrade cannot continue.'));
} elseif (strcasecmp($aInstalledInfo['product_name'], ITOP_APPLICATION) != 0) {
$oPage->p("<b>Warning: The installed products seem different. Are you sure that you want to upgrade {$aInstalledInfo['product_name']} with ".ITOP_APPLICATION."?</b>");
}
$sInstalledVersion = $aInstalledInfo['product_version'];
$sInstalledDataModelVersion = $aInstalledInfo['datamodel_version'];
$oPage->add("<h2>Information about the upgrade from version $sInstalledVersion to ".ITOP_VERSION_FULL."</h2>");
if ($sInstalledVersion == ITOP_VERSION_FULL) {
// Reinstalling the same version let's skip the license agreement...
$bDisplayLicense = false;
}
$this->oWizard->SetParameter('display_license', $bDisplayLicense); // Remember for later
$sCompatibleDMDir = SetupUtils::GetLatestDataModelDir();
if ($sCompatibleDMDir === false) {
// No compatible version exists... cannot upgrade. Either it is too old, or too new (downgrade !)
$this->bCanMoveForward = false;
$oPage->p("No datamodel directory found.");
} else {
$sUpgradeDMVersion = SetupUtils::GetDataModelVersion($sCompatibleDMDir);
$sPreviousSourceDir = isset($aInstalledInfo['source_dir']) ? $aInstalledInfo['source_dir'] : 'modules';
$aChanges = false;
if (is_dir($sPreviousVersionDir)) {
// Check if the previous version is a "genuine" one or not...
$aChanges = SetupUtils::CheckVersion($sInstalledDataModelVersion, $sPreviousVersionDir.'/'.$sPreviousSourceDir);
}
if (($aChanges !== false) && ((count($aChanges['added']) > 0) || (count($aChanges['removed']) > 0) || (count($aChanges['modified']) > 0))) {
// Some changes were detected, prompt the user to keep or discard them
$oPage->p("<img src=\"../images/error.png\"/>&nbsp;Some modifications were detected between the ".ITOP_APPLICATION." version in '$sPreviousVersionDir' and a genuine $sInstalledVersion version.");
$oPage->p("What do you want to do?");
$aWritableDirs = ['modules', 'portal'];
$aErrors = SetupUtils::CheckWritableDirs($aWritableDirs);
$sChecked = ($this->oWizard->GetParameter('upgrade_type') == 'keep-previous') ? ' checked ' : '';
$sDisabled = (count($aErrors) > 0) ? ' disabled ' : '';
$oPage->p('<input id="radio_upgrade_keep" type="radio" name="upgrade_type" value="keep-previous" '.$sChecked.$sDisabled.'/><label for="radio_upgrade_keep">&nbsp;Preserve the modifications of the installed version (the dashboards inside '.ITOP_APPLICATION.' may not be editable).</label>');
$oPage->add('<input type="hidden" name="datamodel_previous_version" value="'.utils::EscapeHtml($sInstalledDataModelVersion).'">');
$oPage->add('<input type="hidden" name="relative_source_dir" value="'.utils::EscapeHtml($sPreviousSourceDir).'">');
if (count($aErrors) > 0) {
$oPage->p("Cannot copy the installed version due to the following access rights issue(s):");
foreach ($aErrors as $sDir => $oCheckResult) {
$oPage->p('<img src="../images/error.png"/>&nbsp;'.$oCheckResult->sLabel);
}
}
$sChecked = ($this->oWizard->GetParameter('upgrade_type') == 'use-compatible') ? ' checked ' : '';
$oPage->p('<input id="radio_upgrade_convert" type="radio" name="upgrade_type" value="use-compatible" '.$sChecked.'/><label for="radio_upgrade_convert">&nbsp;Discard the modifications, use a standard '.$sUpgradeDMVersion.' data model.</label>');
$oPage->add('<input type="hidden" name="datamodel_path" value="'.utils::EscapeHtml($sCompatibleDMDir).'">');
$oPage->add('<input type="hidden" name="datamodel_version" value="'.utils::EscapeHtml($sUpgradeDMVersion).'">');
$oPage->add('<div id="changes_summary"><div class="closed"><span class="title">Details of the modifications</span><div>');
if (count($aChanges['added']) > 0) {
$oPage->add('<ul>New files added:');
foreach ($aChanges['added'] as $sFilePath => $void) {
$oPage->add('<li>'.$sFilePath.'</li>');
}
$oPage->add('</ul>');
}
if (count($aChanges['removed']) > 0) {
$oPage->add('<ul>Deleted files:');
foreach ($aChanges['removed'] as $sFilePath => $void) {
$oPage->add('<li>'.$sFilePath.'</li>');
}
$oPage->add('</ul>');
}
if (count($aChanges['modified']) > 0) {
$oPage->add('<ul>Modified files:');
foreach ($aChanges['modified'] as $sFilePath => $void) {
$oPage->add('<li>'.$sFilePath.'</li>');
}
$oPage->add('</ul>');
}
$oPage->add('</div></div></div>');
} else {
// No changes detected... or no way to tell because of the lack of a manifest or previous source dir
// Use the "compatible" datamodel as-is.
$sCompatibleDMDirToDisplay = utils::HtmlEntities($sCompatibleDMDir);
$sUpgradeDMVersionToDisplay = utils::HtmlEntities($sUpgradeDMVersion);
$oPage->add(
<<<HTML
<div class="message message-valid">The datamodel will be upgraded from version $sInstalledDataModelVersion to version $sUpgradeDMVersion.</div>
<input type="hidden" name="upgrade_type" value="use-compatible">
<input type="hidden" name="datamodel_path" value="$sCompatibleDMDirToDisplay">
<input type="hidden" name="datamodel_version" value="$sUpgradeDMVersionToDisplay">
HTML
);
}
$oPage->add_ready_script(
<<<EOF
$("#changes_summary .title").on('click', function() { $(this).parent().toggleClass('closed'); } );
$('input[name=upgrade_type]').on('click change', function() { WizardUpdateButtons(); });
EOF
);
$oMutex = new iTopMutex(
'cron'.$this->oWizard->GetParameter('db_name', '').$this->oWizard->GetParameter('db_prefix', ''),
$this->oWizard->GetParameter('db_server', ''),
$this->oWizard->GetParameter('db_user', ''),
$this->oWizard->GetParameter('db_pwd', ''),
$this->oWizard->GetParameter('db_tls_enabled', ''),
$this->oWizard->GetParameter('db_tls_ca', '')
);
if ($oMutex->IsLocked()) {
$oPage->add('<div class="message">'.ITOP_APPLICATION.' cron process is being executed on the target database. '.ITOP_APPLICATION.' cron process will be stopped during the setup execution.</div>');
}
}
}
public function CanMoveForward()
{
return $this->bCanMoveForward;
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return
<<<EOF
if ($("#radio_upgrade_keep").length == 0) return true;
bRet = ($('input[name=upgrade_type]:checked').length > 0);
return bRet;
EOF
;
}
}

View File

@@ -0,0 +1,169 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* Summary of the installation tasks
*/
class WizStepDone extends WizardStep
{
public function GetTitle()
{
return 'Done';
}
public function GetPossibleSteps()
{
return [];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
return new WizardState('');
}
public function Display(WebPage $oPage)
{
// Check if there are some manual steps required:
$aManualSteps = [];
$aAvailableModules = SetupUtils::AnalyzeInstallation($this->oWizard);
$sRootUrl = utils::GetAbsoluteUrlAppRoot(true);
$aSelectedModules = json_decode($this->oWizard->GetParameter('selected_modules'), true);
foreach ($aSelectedModules as $sModuleId) {
if (!empty($aAvailableModules[$sModuleId]['doc.manual_setup'])) {
$sUrl = $aAvailableModules[$sModuleId]['doc.manual_setup'];
$sManualStepUrl = utils::IsURL($sUrl) ? $sUrl : $sRootUrl.$sUrl;
$aManualSteps[$aAvailableModules[$sModuleId]['label']] = $sManualStepUrl;
}
}
$oPage->add('<div class="ibo-is-html-content">');
if (count($aManualSteps) > 0) {
$oPage->add("<h2>Manual operations required</h2>");
$oPage->p("In order to complete the installation, the following manual operations are required:");
foreach ($aManualSteps as $sModuleLabel => $sUrl) {
$oPage->p("<a href=\"$sUrl\" target=\"_blank\">Manual instructions for $sModuleLabel</a>");
}
$oPage->add("<h2>Congratulations for installing ".ITOP_APPLICATION."</h2>");
} else {
$oPage->add("<h2>Congratulations for installing ".ITOP_APPLICATION."</h2>");
$oPage->ok("The installation completed successfully.");
}
$bHasBackup = false;
if (($this->oWizard->GetParameter('mode', '') == 'upgrade') && $this->oWizard->GetParameter('db_backup', false) && $this->oWizard->GetParameter('authent', false)) {
$sBackupDestination = $this->oWizard->GetParameter('db_backup_path', '');
if (file_exists($sBackupDestination.'.tar.gz')) {
$bHasBackup = true;
// To mitigate security risks: pass only the filename without the extension, the download will add the extension itself
$oPage->p('Your backup is ready');
$oPage->p('<a style="background:transparent;" href="'.utils::GetAbsoluteUrlAppRoot(true).'setup/ajax.dataloader.php?operation=async_action&step_class=WizStepDone&params[backup]='.urlencode($sBackupDestination).'&authent='.$this->oWizard->GetParameter('authent', '').'" target="_blank"><img src="../images/icons/icons8-archive-folder.svg" style="border:0;vertical-align:middle;">&nbsp;Download '.basename($sBackupDestination).'</a>');
} else {
$oPage->p('<img src="../images/error.png"/>&nbsp;Warning: Backup creation failed !');
}
}
// Form goes here.. No back button since the job is done !
$oPage->add('<div id="placeholder" class="setup-end-placeholder">');
$oPage->add("<div><a class=\"ibo-svg-illustration--container\" title=\"Subscribe to Combodo Newsletter.\" href=\"https://www.combodo.com/newsletter-subscription?var_mode=recalcul\" target=\"_blank\">".file_get_contents(APPROOT.'images/illustrations/undraw_newsletter.svg')." Register now</a></div>");
$oPage->add("<div><a class=\"ibo-svg-illustration--container\" title=\"Get Professional Support from Combodo\" href=\"https://support.combodo.com\" target=\"_blank\">".file_get_contents(APPROOT.'images/illustrations/undraw_active_support.svg')."Get professional support</a></div>");
$oPage->add("<div><a class=\"ibo-svg-illustration--container\" title=\"Get Professional Training from Combodo\" href=\"http://www.combodo.com/training\" target=\"_blank\">".file_get_contents(APPROOT.'images/illustrations/undraw_education.svg')."Get professional training</a></div>");
$oPage->add('</div>');
$oPage->add('</div>');
$oConfig = new Config(utils::GetConfigFilePath());
$aParamValues = $this->oWizard->GetParamForConfigArray();
$oConfig->UpdateFromParams($aParamValues);
// Load the data model only, in order to load env-production/core/main.php to get the XML parameters (needed by GetModuleSettings below)
// But main.php may also contain classes (defined without any module), and thus requiring the full data model
// to be loaded to prevent "class not found" errors...
$oProductionEnv = new RunTimeEnvironment('production');
$oProductionEnv->InitDataModel($oConfig, true);
$sIframeUrl = $oConfig->GetModuleSetting('itop-hub-connector', 'setup_url', '');
$sSetupTokenFile = APPROOT.'data/.setup';
$sSetupToken = bin2hex(random_bytes(12));
file_put_contents($sSetupTokenFile, $sSetupToken);
$sIframeUrl .= "&setup_token=$sSetupToken";
if ($sIframeUrl != '') {
$oPage->add('<iframe id="fresh_content" frameborder="0" scrolling="auto" src="'.$sIframeUrl.'"></iframe>');
$oPage->add_script("
window.addEventListener('message', function(event) {
if (event.data === 'itophub_load_completed')
{
$('#placeholder').hide();
$('#fresh_content').show();
}
}, false);
");
}
$sForm = '<div class="ibo-setup--wizard--buttons-container" style="text-align:center"><form method="post" class="ibo-setup--enter-itop" action="'.$this->oWizard->GetParameter('application_url').'pages/UI.php">';
$sForm .= '<input type="hidden" name="auth_user" value="'.utils::EscapeHtml($this->oWizard->GetParameter('admin_user')).'">';
$sForm .= '<input type="hidden" name="auth_pwd" value="'.utils::EscapeHtml($this->oWizard->GetParameter('admin_pwd')).'">';
$sForm .= "<button id=\"enter_itop\" class=\"ibo-button ibo-is-regular ibo-is-primary\" type=\"submit\">Enter ".ITOP_APPLICATION."</button></div>";
$sForm .= '</form>';
$sForm = addslashes($sForm);
$oPage->add_ready_script("$('#wiz_form').append('$sForm');");
// avoid leaving in a dirty state
SetupUtils::ExitMaintenanceMode(false);
SetupUtils::ExitReadOnlyMode(false);
if (false === $bHasBackup) {
SetupUtils::EraseSetupToken();
}
}
public function CanMoveForward()
{
return false;
}
public function CanMoveBackward()
{
return false;
}
/**
* Tells whether this step of the wizard requires that the configuration file be writable
* @return bool True if the wizard will possibly need to modify the configuration at some point
*/
public function RequiresWritableConfig()
{
return false; //This step executes once the config was written and secured
}
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
SetupUtils::EraseSetupToken();
// For security reasons: add the extension now so that this action can be used to read *only* .tar.gz files from the disk...
$sBackupFile = $aParameters['backup'].'.tar.gz';
if (file_exists($sBackupFile)) {
// Make sure there is NO output at all before our content, otherwise the document will be corrupted
$sPreviousContent = ob_get_clean();
$oPage->SetContentType('application/gzip');
$oPage->SetContentDisposition('attachment', basename($sBackupFile));
$oPage->add(file_get_contents($sBackupFile));
}
}
}

View File

@@ -0,0 +1,190 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
require_once(APPROOT.'setup/sequencers/ApplicationInstallSequencer.php');
class WizStepInstall extends AbstractWizStepInstall
{
public const SequencerClass = ApplicationInstallSequencer::class;
public function GetTitle()
{
return 'Building iTop';
}
public function GetPossibleSteps()
{
return [WizStepDone::class];
}
/**
* Returns the label for the " Next >> " button
* @return string The label for the button
*/
public function GetNextButtonLabel()
{
return 'Continue';
}
public function CanMoveForward()
{
if ($this->CheckDependencies()) {
return true;
} else {
return false;
}
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
return new WizardState(WizStepDone::class);
}
protected function AddProgressBar(WebPage $oPage, string $sTitle = 'Progress of the operations')
{
$oPage->add('<fieldset id="installation_progress"><legend>'.$sTitle.'</legend>');
$oPage->add('<div id="progress_content">');
$oPage->LinkScriptFromAppRoot('setup/jquery.progression.js');
$oPage->add('<p class="center"><span id="setup_msg">Ready to start...</span></p><div style="display:block;margin-left: auto; margin-right:auto;" id="progress">0%</div>');
$oPage->add('</div>'); // progress_content
$oPage->add('</fieldset>');
$oPage->add("<div class=\"message message-error ibo-is-html-content\" style=\"display:none;\" id=\"setup_error\"></div>");
}
public function Display(WebPage $oPage)
{
$aInstallParams = $this->BuildConfig();
$this->AddProgressBar($oPage, 'Progress of the installation');
$sJSONData = json_encode($aInstallParams);
$oPage->add('<input type="hidden" id="installer_parameters" value="'.utils::EscapeHtml($sJSONData).'"/>');
$sAuthentToken = $this->oWizard->GetParameter('authent', '');
$oPage->add('<input type="hidden" id="authent_token" value="'.$sAuthentToken.'"/>');
if (!$this->CheckDependencies()) {
$oPage->error($this->sDependencyIssue);
$oPage->add_ready_script(<<<JS
$("#wiz_form").data("installation_status", "error");
document.getElementById("setup_msg").innerText = "Unmet dependencies";
JS);
return;
}
if ($this->oWizard->GetParameter('force-uninstall', false)) {
SetupLog::Warning("User disabled uninstallation checks");
}
$aExtensionsRemoved = json_decode($this->oWizard->GetParameter('removed_extensions'), true) ?? [];
$aExtensionsNotUninstallable = json_decode($this->oWizard->GetParameter('extensions_not_uninstallable'));
$aExtensionsForceUninstalled = [];
foreach ($aExtensionsRemoved as $sExtensionCode => $sLabel) {
if (in_array($sExtensionCode, $aExtensionsNotUninstallable)) {
$aExtensionsForceUninstalled[] = $sExtensionCode;
}
}
if (count($aExtensionsForceUninstalled)) {
SetupLog::Warning("Extensions uninstalled forcefully : ".implode(',', $aExtensionsForceUninstalled));
}
$oPage->add_ready_script(<<<JS
$("#wiz_form").data("installation_status", "not started");
ExecuteStep("");
JS);
}
/**
* @throws \Exception
*/
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
$oParameters = new PHPParameters();
$sStep = $aParameters['installer_step'];
$sJSONParameters = $aParameters['installer_config'];
$oParameters->LoadFromHash(json_decode($sJSONParameters, true /* bAssoc */));
$oInstaller = new (static::SequencerClass)($oParameters);
$aRes = $oInstaller->ExecuteStep($sStep);
if (($aRes['status'] != $oInstaller::ERROR) && ($aRes['next-step'] != '')) {
// Tell the web page to move the progress bar and to launch the next step
$sMessage = addslashes(utils::EscapeHtml($aRes['next-step-label']));
$oPage->add_ready_script(
<<<EOF
$("#wiz_form").data("installation_status", "running");
WizardUpdateButtons();
$('#setup_msg').html('$sMessage');
$('#progress').progression( {Current:{$aRes['percentage-completed']}, Maximum: 100} );
//$("#percentage").html('{$aRes['percentage-completed']} % completed<br/>{$aRes['next-step-label']}');
ExecuteStep('{$aRes['next-step']}');
EOF
);
} elseif ($aRes['status'] != $oInstaller::ERROR) {
// Installation complete, move to the next step of the wizard
$oPage->add_ready_script(
<<<EOF
$("#wiz_form").data("installation_status", "completed");
$('#progress').progression( {Current:100, Maximum: 100} );
WizardUpdateButtons();
$("#btn_next").off("click.install");
$("#btn_next").trigger('click');
EOF
);
} else {
//Error case
$sMessage = addslashes(utils::EscapeHtml($aRes['message']));
$sMessage = str_replace("\n", '<br>', $sMessage);
$oPage->add_ready_script(
<<<EOF
$("#wiz_form").data("installation_status", "error");
$("#progress .progress").addClass('progress-error');
WizardUpdateButtons();
$('#setup_error').html('$sMessage').show();
EOF
);
$this->AddProgressErrorScript($oPage, $aRes);
}
}
protected function AddProgressErrorScript($oPage, $aRes)
{
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return 'return $("#wiz_form").data("installation_status") === "completed";';
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveBackward()
{
return 'var sStatus = $("#wiz_form").data("installation_status"); return ((sStatus === "not started") || (sStatus === "error"));';
}
}

View File

@@ -0,0 +1,184 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* Miscellaneous Parameters (URL, Sample Data) when installing from scratch
*/
class WizStepInstallMiscParams extends AbstractWizStepMiscParams
{
public function GetTitle()
{
return 'Miscellaneous Parameters';
}
public function GetPossibleSteps()
{
return [WizStepModulesChoice::class];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$this->oWizard->SaveParameter('default_language', '');
$this->oWizard->SaveParameter('application_url', '');
$this->oWizard->SaveParameter('graphviz_path', '');
$this->oWizard->SaveParameter('sample_data', 'yes');
return new WizardState(WizStepModulesChoice::class, 'start_install');
}
public function Display(WebPage $oPage)
{
$sDefaultLanguage = $this->oWizard->GetParameter('default_language', $this->oWizard->GetParameter('admin_language'));
$sApplicationURL = $this->oWizard->GetParameter('application_url', utils::GetDefaultUrlAppRoot(true));
$sDefaultGraphvizPath = (strtolower(substr(PHP_OS, 0, 3)) === 'win') ? 'C:\\Program Files\\Graphviz\\bin\\dot.exe' : '/usr/bin/dot';
$sGraphvizPath = $this->oWizard->GetParameter('graphviz_path', $sDefaultGraphvizPath);
$sSampleData = $this->oWizard->GetParameter('sample_data', 'yes');
$oPage->add('<h2>Additional parameters</h2>');
$oPage->add('<fieldset>');
$oPage->add('<legend>Default Language</legend>');
$oPage->add('<table>');
$sSourceDir = APPROOT.'dictionaries/';
$aLanguages = SetupUtils::GetAvailableLanguages($sSourceDir);
$oPage->add('<tr><td>Default Language: </td><td>');
$oPage->add(SetupUtils::GetLanguageSelect($sSourceDir, 'default_language', $sDefaultLanguage));
$oPage->add('</td></tr>');
$oPage->add('</table>');
$oPage->add('</fieldset>');
$oPage->add('<fieldset>');
$oPage->add('<legend>Application URL</legend>');
$oPage->add('<table>');
$oPage->add('<tr><td>URL: </td><td><input id="application_url" class="ibo-input" name="application_url" type="text" size="35" maxlength="1024" value="'.utils::EscapeHtml($sApplicationURL).'" style="width: 100%;box-sizing: border-box;"><span id="v_application_url"/></td><tr>');
$oPage->add('</table>');
$oPage->add('<div class="message message-warning">Change the value above if the end-users will be accessing the application by another path due to a specific configuration of the web server.</div>');
$oPage->add('</fieldset>');
$oPage->add('<fieldset>');
$oPage->add('<legend>Path to Graphviz\' dot application</legend>');
$oPage->add('<table>');
$oPage->add('<tr><td>Path: </td><td><input id="graphviz_path" class="ibo-input" name="graphviz_path" type="text" size="35" maxlength="1024" value="'.utils::EscapeHtml($sGraphvizPath).'" style="width: 100%;box-sizing: border-box;"><span id="v_graphviz_path"/></td>');
$oPage->add('<td><i class="fas fa-question-circle setup-input--hint--icon" data-tooltip-content="Graphviz is required to display the impact analysis graph (i.e. impacts / depends on)."></i></td><tr>');
$oPage->add('</table>');
$oPage->add('<span id="graphviz_status"></span>');
$oPage->add('</fieldset>');
$oPage->add('<fieldset>');
$oPage->add('<legend>Sample Data</legend>');
$sChecked = ($sSampleData == 'yes') ? 'checked ' : '';
$oPage->p('<input id="sample_data_yes" name="sample_data" type="radio" value="yes" '.$sChecked.'><label for="sample_data_yes">&nbsp;I am installing a <b>demo or test</b> instance, populate the database with some demo data.');
$sChecked = ($sSampleData == 'no') ? 'checked ' : '';
$oPage->p('<input id="sample_data_no" name="sample_data" type="radio" value="no" '.$sChecked.'><label for="sample_data_no">&nbsp;I am installing a <b>production</b> instance, create an empty database to start from.');
$oPage->add('</fieldset>');
$sAuthentToken = $this->oWizard->GetParameter('authent', '');
$oPage->add('<input type="hidden" id="authent_token" value="'.$sAuthentToken.'"/>');
$oPage->add_ready_script(
<<<EOF
$('#application_url').on('change keyup', function() { WizardUpdateButtons(); } );
$('#graphviz_path').on('change keyup init', function() { WizardUpdateButtons(); WizardAsyncAction('check_graphviz', { graphviz_path: $('#graphviz_path').val(), authent: $('#authent_token').val()}); } ).trigger('init');
$('#btn_next').on('click', function() {
bRet = true;
if ($(this).attr('data-graphviz') != 'ok')
{
bRet = confirm('The impact analysis will not be displayed properly. Are you sure you want to continue?');
}
return bRet;
});
EOF
);
$this->AddUseSymlinksFlagOption($oPage);
}
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
switch ($sCode) {
case 'check_graphviz':
$sGraphvizPath = $aParameters['graphviz_path'];
$aCheck = SetupUtils::CheckGraphviz($sGraphvizPath);
// N°2214 logging TRACE results
$aTraceCheck = CheckResult::FilterCheckResultArray($aCheck, [CheckResult::TRACE]);
foreach ($aTraceCheck as $oTraceCheck) {
SetupLog::Ok($oTraceCheck->sLabel);
}
$aNonTraceCheck = array_diff($aCheck, $aTraceCheck);
foreach ($aNonTraceCheck as $oCheck) {
switch ($oCheck->iSeverity) {
case CheckResult::INFO:
$sStatus = 'ok';
$sInfoExplanation = $oCheck->sLabel;
$sMessage = json_encode('<div class="message message-valid">'.$sInfoExplanation.'</div>');
break;
default:
case CheckResult::ERROR:
case CheckResult::WARNING:
$sStatus = 'ko';
$sErrorExplanation = $oCheck->sLabel;
$sMessage = json_encode('<div class="message message-error">'.$sErrorExplanation.'</div>');
break;
}
if ($oCheck->iSeverity !== CheckResult::TRACE) {
$oPage->add_ready_script(
<<<JS
$("#graphviz_status").html($sMessage);
$('#btn_next').attr('data-graphviz', '$sStatus');
JS
);
}
}
break;
}
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return
<<<EOF
bRet = ($('#application_url').val() != '');
if (!bRet)
{
$("#v_application_url").html('<img src="../images/validation_error.png" title="This field cannot be empty"/>');
}
else
{
$("#v_application_url").html('');
}
bGraphviz = ($('#graphviz_path').val() != '');
if (!bGraphviz)
{
// Does not prevent to move forward
$("#v_graphviz_path").html('<img src="../images/validation_error.png" title="Impact analysis will not display properly"/>');
}
else
{
$("#v_graphviz_path").html('');
}
return bRet;
EOF
;
}
}

View File

@@ -0,0 +1,218 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* Second step of the iTop Installation Wizard: Install or Upgrade
*/
class WizStepInstallOrUpgrade extends WizardStep
{
public function GetTitle()
{
return 'Install or Upgrade choice';
}
public function GetPossibleSteps()
{
return [WizStepDetectedInfo::class, WizStepLicense::class];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$sNextStep = '';
$sInstallMode = utils::ReadParam('install_mode');
$this->oWizard->SaveParameter('previous_version_dir', '');
$this->oWizard->SaveParameter('db_server', '');
$this->oWizard->SaveParameter('db_user', '');
$this->oWizard->SaveParameter('db_pwd', '');
$this->oWizard->SaveParameter('db_name', '');
$this->oWizard->SaveParameter('db_prefix', '');
$this->oWizard->SaveParameter('db_tls_enabled', false);
$this->oWizard->SaveParameter('db_tls_ca', '');
if ($sInstallMode == 'install') {
$this->oWizard->SetParameter('install_mode', 'install');
$sFullSourceDir = SetupUtils::GetLatestDataModelDir();
$this->oWizard->SetParameter('source_dir', $sFullSourceDir);
$this->oWizard->SetParameter('datamodel_version', SetupUtils::GetDataModelVersion($sFullSourceDir));
$sNextStep = WizStepLicense::class;
} else {
$this->oWizard->SetParameter('install_mode', 'upgrade');
$sNextStep = WizStepDetectedInfo::class;
}
return new WizardState($sNextStep);
}
public function Display(WebPage $oPage)
{
$sInstallMode = $this->oWizard->GetParameter('install_mode', '');
$sDBServer = $this->oWizard->GetParameter('db_server', '');
$sDBUser = $this->oWizard->GetParameter('db_user', '');
$sDBPwd = $this->oWizard->GetParameter('db_pwd', '');
$sDBName = $this->oWizard->GetParameter('db_name', '');
$sDBPrefix = $this->oWizard->GetParameter('db_prefix', '');
$sTlsEnabled = $this->oWizard->GetParameter('db_tls_enabled', false);
$sTlsCA = $this->oWizard->GetParameter('db_tls_ca', '');
$sPreviousVersionDir = '';
if ($sInstallMode == '') {
$aPreviousInstance = SetupUtils::GetPreviousInstance(APPROOT);
if ($aPreviousInstance['found']) {
$sInstallMode = 'upgrade';
$sDBServer = $aPreviousInstance['db_server'];
$sDBUser = $aPreviousInstance['db_user'];
$sDBPwd = $aPreviousInstance['db_pwd'];
$sDBName = $aPreviousInstance['db_name'];
$sDBPrefix = $aPreviousInstance['db_prefix'];
$sTlsEnabled = $aPreviousInstance['db_tls_enabled'];
$sTlsCA = $aPreviousInstance['db_tls_ca'];
$this->oWizard->SaveParameter('graphviz_path', $aPreviousInstance['graphviz_path']);
$sPreviousVersionDir = APPROOT;
} else {
$sInstallMode = 'install';
}
}
$sPreviousVersionDir = $this->oWizard->GetParameter('previous_version_dir', $sPreviousVersionDir);
$sUpgradeInfoStyle = '';
if ($sInstallMode == 'install') {
$sUpgradeInfoStyle = ' style="display: none;" ';
}
$oPage->add('<div class="setup-content-title">What do you want to do?</div>');
$sChecked = ($sInstallMode == 'install') ? ' checked ' : '';
$oPage->p('<input id="radio_install" type="radio" name="install_mode" value="install" '.$sChecked.'/><label for="radio_install">&nbsp;Install a new '.ITOP_APPLICATION.'</label>');
$sChecked = ($sInstallMode == 'upgrade') ? ' checked ' : '';
$sDisabled = (($sInstallMode == 'install') && (empty($sPreviousVersionDir))) ? ' disabled' : '';
$oPage->p('<input id="radio_update" type="radio" name="install_mode" value="upgrade" '.$sChecked.$sDisabled.'/><label for="radio_update">&nbsp;Upgrade an existing '.ITOP_APPLICATION.' instance</label>');
$sUpgradeDir = utils::HtmlEntities($sPreviousVersionDir);
$oPage->add(
<<<HTML
<div id="upgrade_info"'.$sUpgradeInfoStyle.'>
<div class="setup-disk-location--input--container">Location on the disk:<input id="previous_version_dir_display" type="text" value="$sUpgradeDir" class="ibo-input" disabled>
<input type="hidden" name="previous_version_dir" value="$sUpgradeDir"></div>
HTML
);
SetupUtils::DisplayDBParameters(
$oPage,
false,
$sDBServer,
$sDBUser,
$sDBPwd,
$sDBName,
$sDBPrefix,
$sTlsEnabled,
$sTlsCA,
null
);
$sAuthentToken = $this->oWizard->GetParameter('authent', '');
$oPage->add('</div>');
$oPage->add('<input type="hidden" id="authent_token" value="'.$sAuthentToken.'"/>');
//$oPage->add('</fieldset>');
$oPage->add_ready_script(
<<<JS
$("#radio_update").on('change', function() { if (this.checked ) { $('#upgrade_info').show(); WizardUpdateButtons(); } else { $('#upgrade_info').hide(); } });
$("#radio_install").on('change', function() { if (this.checked ) { $('#upgrade_info').hide(); WizardUpdateButtons(); } else { $('#upgrade_info').show(); } });
JS
);
}
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
switch ($sCode) {
case 'check_path':
$sPreviousVersionDir = $aParameters['previous_version_dir'];
$aPreviousInstance = SetupUtils::GetPreviousInstance($sPreviousVersionDir);
if ($aPreviousInstance['found']) {
$sDBServer = utils::EscapeHtml($aPreviousInstance['db_server']);
$sDBUser = utils::EscapeHtml($aPreviousInstance['db_user']);
$sDBPwd = utils::EscapeHtml($aPreviousInstance['db_pwd']);
$sDBName = utils::EscapeHtml($aPreviousInstance['db_name']);
$sDBPrefix = utils::EscapeHtml($aPreviousInstance['db_prefix']);
$oPage->add_ready_script(
<<<EOF
$("#db_server").val('$sDBServer');
$("#db_user").val('$sDBUser');
$("#db_pwd").val('$sDBPwd');
$("#db_name").val('$sDBName');
$("#db_prefix").val('$sDBPrefix');
$("#db_pwd").trigger('change'); // Forces check of the DB connection
EOF
);
}
break;
case 'check_db':
SetupUtils::AsyncCheckDB($oPage, $aParameters);
break;
case 'check_backup':
$sDBBackupPath = $aParameters['db_backup_path'];
$fFreeSpace = SetupUtils::CheckDiskSpace($sDBBackupPath);
if ($fFreeSpace !== false) {
$sMessage = utils::EscapeHtml(SetupUtils::HumanReadableSize($fFreeSpace).' free in '.dirname($sDBBackupPath));
$oPage->add_ready_script(
<<<EOF
$("#backup_info").html('$sMessage');
EOF
);
} else {
$oPage->add_ready_script(
<<<EOF
$("#backup_info").html('');
EOF
);
}
break;
}
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return
<<<EOF
if ($("#radio_install").prop("checked"))
{
ValidateField("db_name", false);
ValidateField("db_new_name", false);
ValidateField("db_prefix", false);
return true;
}
else
{
var bRet = ($("#wiz_form").data("db_connection") !== "error");
bRet = ValidateField("db_name", true) && bRet;
bRet = ValidateField("db_new_name", true) && bRet;
bRet = ValidateField("db_prefix", true) && bRet;
return bRet;
}
EOF
;
}
}

View File

@@ -0,0 +1,133 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* License acceptation screen
*/
class WizStepLicense extends WizardStep
{
public function GetTitle()
{
return 'License Agreement';
}
public function GetPossibleSteps()
{
return [WizStepDBParams::class];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$this->oWizard->SaveParameter('accept_license', 'no');
return new WizardState(WizStepDBParams::class);
}
/**
* @return bool true if we need to display a GDPR confirmation
* @throws \Exception
* @since 2.7.7 3.0.2 3.1.0 N°5037 method creation
* @since 2.7.8 3.0.3 3.1.0 N°5758 rename from NeedsRgpdConsent to NeedsGdprConsent
*/
private function NeedsGdprConsent()
{
$sMode = $this->oWizard->GetParameter('install_mode');
if ($sMode !== 'install') {
return false;
}
$aModules = SetupUtils::AnalyzeInstallation($this->oWizard);
return SetupUtils::IsConnectableToITopHub($aModules);
}
/**
* @param WebPage $oPage
*/
public function Display(WebPage $oPage)
{
$aLicenses = SetupUtils::GetLicenses();
$oPage->add_style(
<<<CSS
fieldset ul {
max-height: min(30em, 40vh); /* Allow usage of the UI up to 150% zoom */
overflow: auto;
}
CSS
);
$oPage->add('<h2>Licenses agreements for the components of '.ITOP_APPLICATION.'</h2>');
$oPage->add_style('div a.no-arrow { background:transparent; padding-left:0;}');
$oPage->add_style('.toggle { cursor:pointer; text-decoration:underline; color:#1C94C4; }');
$oPage->add('<fieldset>');
$oPage->add('<legend>Components of '.ITOP_APPLICATION.'</legend>');
$oPage->add('<ul id="ibo-setup-licenses--components-list">');
$index = 0;
foreach ($aLicenses as $oLicense) {
$oPage->add('<li><b>'.$oLicense->product.'</b>, &copy; '.$oLicense->author.' is licensed under the <b>'.$oLicense->license_type.' license</b>. (<span class="toggle" id="toggle_'.$index.'">Details</span>)');
$oPage->add('<div id="license_'.$index.'" class="license_text ibo-is-html-content" style="display:none;overflow:auto;max-height:10em;font-size:12px;border:1px #696969 solid;margin-bottom:1em; margin-top:0.5em;padding:0.5em;"><pre>'.$oLicense->text.'</pre></div>');
$oPage->add_ready_script('$(".license_text a").attr("target", "_blank").addClass("no-arrow");');
$oPage->add_ready_script('$("#toggle_'.$index.'").on("click", function() { $("#license_'.$index.'").toggle(); } );');
$index++;
}
$oPage->add('</ul>');
$oPage->add('</fieldset>');
$sChecked = ($this->oWizard->GetParameter('accept_license', 'no') == 'yes') ? ' checked ' : '';
$oPage->add('<div class="setup-accept-licenses"><input class="check_select" type="checkbox" name="accept_license" id="accept" value="yes" '.$sChecked.'><label for="accept">I accept the terms of the licenses of the '.count($aLicenses).' components mentioned above.</label></div>');
if ($this->NeedsGdprConsent()) {
$oPage->add('<br>');
$oPage->add('<fieldset>');
$oPage->add('<legend>European General Data Protection Regulation</legend>');
$oPage->add('<div class="ibo-setup-licenses--components-list">'.ITOP_APPLICATION.' software is compliant with the processing of personal data according to the European General Data Protection Regulation (GDPR).<p></p>
By installing '.ITOP_APPLICATION.' you agree that some information will be collected by Combodo to help you manage your instances and for statistical purposes.
This data remains anonymous until it is associated to a user account on iTop Hub.</p>
<p>List of collected data available in our <a target="_blank" href="https://www.itophub.io/page/data-privacy">Data privacy section.</a></p><br></div>');
$oPage->add('<input type="checkbox" class="check_select" id="rgpd_consent">');
$oPage->add('<label for="rgpd_consent">&nbsp;I accept the processing of my personal data</label>');
$oPage->add('</fieldset>');
}
$oPage->add_ready_script('$(".check_select").on("click change", function() { WizardUpdateButtons(); });');
$oPage->add_script(
<<<JS
function isRgpdConsentOk(){
let eRgpdConsent = $("#rgpd_consent");
if(eRgpdConsent.length){
if(!eRgpdConsent[0].checked){
return false;
}
}
return true;
}
JS
);
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return 'return ($("#accept").prop("checked") && isRgpdConsentOk());';
}
}

View File

@@ -0,0 +1,35 @@
<?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
*/
/**
* License acceptation screen (when upgrading)
*/
class WizStepLicense2 extends WizStepLicense
{
public function GetPossibleSteps()
{
return [WizStepUpgradeMiscParams::class];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
return new WizardState(WizStepUpgradeMiscParams::class);
}
}

View File

@@ -0,0 +1,864 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
/**
* Choice of the modules to be installed
*/
class WizStepModulesChoice extends WizardStep
{
protected static string $SEP = '_';
protected bool $bUpgrade = false;
protected bool $bCanMoveForward = true;
protected ?Config $oConfig = null;
/**
*
* @var iTopExtensionsMap
*/
protected iTopExtensionsMap $oExtensionsMap;
private ?array $aSteps = null;
protected PhpExpressionEvaluator $oPhpExpressionEvaluator;
/**
* Whether we were able to load the choices from the database or not
* @var bool
*/
protected bool $bChoicesFromDatabase;
private array $aAnalyzeInstallationModules = [];
private ?MissingDependencyException $oMissingDependencyException = null;
public function __construct(WizardController $oWizard, $sCurrentState)
{
parent::__construct($oWizard, $sCurrentState);
$this->bChoicesFromDatabase = false;
$this->oExtensionsMap = new iTopExtensionsMap();
$sPreviousSourceDir = $this->oWizard->GetParameter('previous_version_dir', '');
$sConfigPath = null;
if (($sPreviousSourceDir !== '') && is_readable($sPreviousSourceDir.'/conf/production/config-itop.php')) {
$sConfigPath = $sPreviousSourceDir.'/conf/production/config-itop.php';
} elseif (is_readable(utils::GetConfigFilePath('production'))) {
$sConfigPath = utils::GetConfigFilePath('production');
}
// only called if the config file exists : we are updating a previous installation !
// WARNING : we can't load this config directly, as it might be from another directory with a different approot_url (N°2684)
if ($sConfigPath !== null) {
$this->oConfig = new Config($sConfigPath);
$aParamValues = $oWizard->GetParamForConfigArray();
$this->oConfig->UpdateFromParams($aParamValues);
$this->oExtensionsMap->LoadChoicesFromDatabase($this->oConfig);
$this->bChoicesFromDatabase = true;
}
// Sanity check (not stopper, to let developers go further...)
try {
$this->aAnalyzeInstallationModules = SetupUtils::AnalyzeInstallation($this->oWizard, true);
} catch (MissingDependencyException $e) {
$this->oMissingDependencyException = $e;
$this->aAnalyzeInstallationModules = SetupUtils::AnalyzeInstallation($this->oWizard);
}
}
public function GetTitle(): string
{
$aStepInfo = $this->GetStepInfo();
return $aStepInfo['title'] ?? 'Modules selection';
}
public function GetPossibleSteps()
{
return [WizStepModulesChoice::class, WizStepDataAudit::class, WizStepSummary::class];
}
public function GetAddedAndRemovedExtensions($aSelectedExtensions)
{
$aExtensionsAdded = [];
$aExtensionsRemoved = [];
$aExtensionsNotUninstallable = [];
foreach ($this->oExtensionsMap->GetAllExtensionsWithPreviouslyInstalled() as $oExtension) {
/* @var \iTopExtension $oExtension */
$bSelected = in_array($oExtension->sCode, $aSelectedExtensions);
if ($oExtension->bInstalled && !$bSelected) {
$aExtensionsRemoved[$oExtension->sCode] = $oExtension->sLabel;
if (!$oExtension->CanBeUninstalled()) {
$aExtensionsNotUninstallable[$oExtension->sCode] = true;
}
} elseif (!$oExtension->bInstalled && $bSelected) {
$aExtensionsAdded[$oExtension->sCode] = $oExtension->sLabel;
}
}
return [$aExtensionsAdded, $aExtensionsRemoved, $aExtensionsNotUninstallable];
}
public function IsDataAuditEnabled(): bool
{
$sPath = APPROOT.'env-production';
if (!is_dir($sPath)) {
SetupLog::Info("Reinstallation of an iTop from a backup (No env-production found). Setup data audit disabled");
return false;
}
return true;
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
// Accumulates the selected modules:
$index = $this->GetStepIndex();
// use json_encode:decode to store a hash array: step_id => array(input_name => selected_input_id)
$aSelectedChoices = json_decode($this->oWizard->GetParameter('selected_components', '{}'), true);
$aSelected = utils::ReadParam('choice', []);
$aSelectedChoices[$index] = $aSelected;
$this->oWizard->SetParameter('selected_components', json_encode($aSelectedChoices));
if ($this->GetStepInfo($index) == null) {
throw new Exception('Internal error: invalid step "'.$index.'" for the choice of modules.');
} elseif ($bMoveForward) {
if ($this->GetStepInfo(1 + $index) != null) {
return new WizardState(WizStepModulesChoice::class, (1 + $index));
} else {
// Exiting this step of the wizard, let's convert the selection into a list of modules
$aModules = [];
$aExtensions = [];
$sDisplayChoices = '<ul>';
for ($i = 0; $i <= $index; $i++) {
$aStepInfo = $this->GetStepInfo($i);
$sDisplayChoices .= $this->GetSelectedModules($aStepInfo, $aSelectedChoices[$i], $aModules, '', '', $aExtensions);
}
$sDisplayChoices .= '</ul>';
if (class_exists('CreateITILProfilesInstaller')) {
$this->oWizard->SetParameter('old_addon', true);
}
[$aExtensionsAdded, $aExtensionsRemoved, $aExtensionsNotUninstallable] = $this->GetAddedAndRemovedExtensions($aExtensions);
$this->oWizard->SetParameter('selected_modules', json_encode(array_keys($aModules)));
$this->oWizard->SetParameter('selected_extensions', json_encode($aExtensions));
$this->oWizard->SetParameter('display_choices', $sDisplayChoices);
$this->oWizard->SetParameter('extensions_added', json_encode($aExtensionsAdded));
$this->oWizard->SetParameter('removed_extensions', json_encode($aExtensionsRemoved));
$this->oWizard->SetParameter('extensions_not_uninstallable', json_encode(array_keys($aExtensionsNotUninstallable)));
$sMode = $this->oWizard->GetParameter('mode', 'install');
if ($sMode == 'install' || !$this->IsDataAuditEnabled()) {
return new WizardState(WizStepSummary::class);
} else {
return new WizardState(WizStepDataAudit::class);
}
}
}
//Unused when going backward
return new WizardState(WizStepModulesChoice::class, ($index - 1));
}
public function Display(WebPage $oPage)
{
$this->DisplayStep($oPage);
}
/**
* @param \SetupPage $oPage
*
* @throws \Exception
*/
protected function DisplayStep($oPage)
{
// Sanity check (not stopper, to let developers go further...)
if (! is_null($this->oMissingDependencyException)) {
$oPage->warning($this->oMissingDependencyException->getHtmlDesc(), $this->oMissingDependencyException->getMessage());
}
$this->bUpgrade = ($this->oWizard->GetParameter('install_mode') != 'install');
$aStepInfo = $this->GetStepInfo();
$oPage->add_style("div.choice { margin: 0.5em;}");
$oPage->add_style("div.choice a { text-decoration:none; font-weight: bold; color: #1C94C4 }");
$oPage->add_style("div.description { margin-left: 2em; }");
$oPage->add_style(".choice-disabled { color: #999; }");
$oPage->add_style("input.unremovable { accent-color: orangered;}");
$sManualInstallError = SetupUtils::CheckManualInstallDirEmpty(
$this->aAnalyzeInstallationModules,
$this->oWizard->GetParameter('extensions_dir', 'extensions')
);
if ($sManualInstallError !== '') {
$oPage->warning($sManualInstallError);
}
$oPage->add('<div class="module-selection-banner">');
$sBannerPath = isset($aStepInfo['banner']) ? $aStepInfo['banner'] : '';
if (!empty($sBannerPath)) {
if (substr($sBannerPath, 0, 1) == '/') {
// absolute path, means relative to APPROOT
$sBannerUrl = utils::GetDefaultUrlAppRoot(true).$sBannerPath;
} else {
// relative path: i.e. relative to the directory containing the XML file
$sFullPath = dirname($this->GetSourceFilePath()).'/'.$sBannerPath;
$sRealPath = realpath($sFullPath);
$sBannerUrl = utils::GetDefaultUrlAppRoot(true).str_replace(realpath(APPROOT), '', $sRealPath);
}
$oPage->add('<img src="'.$sBannerUrl.'"/>');
}
$sDescription = $aStepInfo['description'] ?? '';
$oPage->add('<span>'.$sDescription.'</span>');
$oPage->add('</div>');
// Build the default choices
$aDefaults = $this->GetDefaults($aStepInfo, $this->aAnalyzeInstallationModules);
$index = $this->GetStepIndex();
// retrieve the saved selection
// use json_encode:decode to store a hash array: step_id => array(input_name => selected_input_id)
$aParameters = json_decode($this->oWizard->GetParameter('selected_components', '{}'), true);
if (!isset($aParameters[$index])) {
$aParameters[$index] = $aDefaults;
}
$aSelectedComponents = $aParameters[$index];
$oPage->add('<div class="module-selection-body">');
$this->DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults);
$oPage->add('</div>');
$oPage->add_script(
<<<EOF
function CheckChoice(sChoiceId)
{
var oElement = $('#'+sChoiceId);
var bChecked = oElement.prop('checked');
var sId = sChoiceId.replace('choice', '');
if ((oElement.attr('type') == 'radio') && bChecked)
{
// Only the radio that is clicked is notified, let's warn the other radio buttons
sName = oElement.attr('name');
$('input[name="'+sName+'"]').each(function() {
var sRadioId = $(this).attr('id');
if ((sRadioId != sChoiceId) && (sRadioId != undefined))
{
CheckChoice(sRadioId);
}
});
}
$('#sub_choices'+sId).each(function() {
if (!bChecked)
{
$(this).addClass('choice-disabled');
}
else
{
$(this).removeClass('choice-disabled');
}
$('input', this).each(function() {
if (bChecked)
{
if ($(this).attr('data-disabled') != 'disabled')
{
// Only non-mandatory fields can be enabled
$(this).prop('disabled', false);
}
}
else
{
$(this).prop('disabled', true);
$(this).prop('checked', false);
}
});
});
}
EOF
);
$oPage->add_ready_script(
<<<EOF
$('.wiz-choice').on('change', function() { CheckChoice($(this).attr('id')); } );
$('.wiz-choice').trigger('change');
EOF
);
}
protected function GetDefaults($aInfo, $aModules, $sParentId = '')
{
$aDefaults = [];
if (!$this->bChoicesFromDatabase) {
$this->GuessDefaultsFromModules($aInfo, $aDefaults, $aModules, $sParentId);
} else {
$this->GetDefaultsFromDatabase($aInfo, $aDefaults, $sParentId);
}
return $aDefaults;
}
protected function GetDefaultsFromDatabase($aInfo, &$aDefaults, $sParentId)
{
$aOptions = isset($aInfo['options']) ? $aInfo['options'] : [];
foreach ($aOptions as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
if ($this->bUpgrade) {
if ($this->oExtensionsMap->IsMarkedAsChosen($aChoice['extension_code'])) {
$aDefaults[$sChoiceId] = $sChoiceId;
}
} elseif (isset($aChoice['default']) && $aChoice['default']) {
$aDefaults[$sChoiceId] = $sChoiceId;
}
// Recurse for sub_options (if any)
if (isset($aChoice['sub_options'])) {
$this->GetDefaultsFromDatabase($aChoice['sub_options'], $aDefaults, $sChoiceId);
}
}
$aAlternatives = $aInfo['alternatives'] ?? [];
$sChoiceName = null;
foreach ($aAlternatives as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
if ($sChoiceName == null) {
$sChoiceName = $sChoiceId; // All radios share the same name
}
if ($this->bUpgrade) {
if ($this->oExtensionsMap->IsMarkedAsChosen($aChoice['extension_code'])) {
$aDefaults[$sChoiceName] = $sChoiceId;
}
} elseif (isset($aChoice['default']) && $aChoice['default']) {
$aDefaults[$sChoiceName] = $sChoiceId;
}
// Recurse for sub_options (if any)
if (isset($aChoice['sub_options'])) {
$this->GetDefaultsFromDatabase($aChoice['sub_options'], $aDefaults, $sChoiceId);
}
}
}
/**
* Try to guess the user choices based on the current list of installed modules...
* @param array $aInfo
* @param array $aDefaults
* @param array $aModules
* @param string $sParentId
* @return array
*/
protected function GuessDefaultsFromModules($aInfo, &$aDefaults, $aModules, $sParentId = '')
{
$aRetScore = [];
$aScores = [];
$aOptions = isset($aInfo['options']) ? $aInfo['options'] : [];
foreach ($aOptions as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
$aScores[$sChoiceId] = [];
if (!$this->bUpgrade && isset($aChoice['default']) && $aChoice['default']) {
$aDefaults[$sChoiceId] = $sChoiceId;
}
if ($this->bUpgrade) {
// In upgrade mode, the defaults are the installed modules
foreach ($aChoice['modules'] as $sModuleId) {
if ($aModules[$sModuleId]['installed_version'] != '') {
// A module corresponding to this choice is installed
$aScores[$sChoiceId][$sModuleId] = true;
}
}
// Used for migration from 1.3.x or before
// Accept that the new version can have one new module than the previous version
// The option is still selected
$iSelected = count($aScores[$sChoiceId]);
$iNeeded = count($aChoice['modules']);
if (($iSelected > 0) && (($iNeeded - $iSelected) < 2)) {
// All the modules are installed, this choice is selected
$aDefaults[$sChoiceId] = $sChoiceId;
}
$aRetScore = array_merge($aRetScore, $aScores[$sChoiceId]);
}
if (isset($aChoice['sub_options'])) {
$aScores[$sChoiceId] = array_merge($aScores[$sChoiceId], $this->GuessDefaultsFromModules($aChoice['sub_options'], $aDefaults, $sChoiceId));
}
$index++;
}
$aAlternatives = isset($aInfo['alternatives']) ? $aInfo['alternatives'] : [];
$sChoiceName = null;
$sChoiceIdNone = null;
foreach ($aAlternatives as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
$aScores[$sChoiceId] = [];
if ($sChoiceName == null) {
$sChoiceName = $sChoiceId;
}
if (!$this->bUpgrade && isset($aChoice['default']) && $aChoice['default']) {
$aDefaults[$sChoiceName] = $sChoiceId;
}
if (isset($aChoice['sub_options'])) {
// By default (i.e. install-mode), sub options can only be checked if the parent option is checked by default
if ($this->bUpgrade || (isset($aChoice['default']) && $aChoice['default'])) {
$aScores[$sChoiceId] = $this->GuessDefaultsFromModules($aChoice['sub_options'], $aDefaults, $aModules, $sChoiceId);
}
}
$index++;
}
$iMaxScore = 0;
if ($this->bUpgrade && (count($aAlternatives) > 0)) {
// The installed choices have precedence over the 'default' choices
// In case several choices share the same base modules, let's weight the alternative choices
// based on their number of installed modules
$sChoiceName = null;
foreach ($aAlternatives as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
if ($sChoiceName == null) {
$sChoiceName = $sChoiceId;
}
if (array_key_exists('modules', $aChoice)) {
foreach ($aChoice['modules'] as $sModuleId) {
if ($aModules[$sModuleId]['installed_version'] != '') {
// A module corresponding to this choice is installed, increase the score of this choice
if (!isset($aScores[$sChoiceId])) {
$aScores[$sChoiceId] = [];
}
$aScores[$sChoiceId][$sModuleId] = true;
$iMaxScore = max($iMaxScore, count($aScores[$sChoiceId]));
}
}
//if (count($aScores[$sChoiceId]) == count($aChoice['modules']))
//{
// $iScore += 100; // Bonus for the parent when a choice is complete
//}
$aRetScore = array_merge($aRetScore, $aScores[$sChoiceId]);
}
$iMaxScore = max($iMaxScore, isset($aScores[$sChoiceId]) ? count($aScores[$sChoiceId]) : 0);
}
}
if ($iMaxScore > 0) {
$aNumericScores = [];
foreach ($aScores as $sChoiceId => $aModules) {
$aNumericScores[$sChoiceId] = count($aModules);
}
// The choice with the bigger score wins !
asort($aNumericScores, SORT_NUMERIC);
$aKeys = array_keys($aNumericScores);
$sBetterChoiceId = array_pop($aKeys);
$aDefaults[$sChoiceName] = $sBetterChoiceId;
}
// echo "Scores: <pre>".print_r($aScores, true)."</pre><br/>";
// echo "Defaults: <pre>".print_r($aDefaults, true)."</pre><br/>";
return $aRetScore;
}
private function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
{
if (!isset($this->oPhpExpressionEvaluator)) {
$this->oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
}
return $this->oPhpExpressionEvaluator;
}
/**
* Converts the list of selected "choices" into a list of "modules": take into account the selected and the mandatory modules
*
* @param array $aInfo Info about the "choice" array('options' => array(...), 'alternatives' => array(...))
* @param array $aSelectedChoices List of selected choices array('name' => 'selected_value_id')
* @param array $aModules Return parameter: List of selected modules array('module_id' => true)
* @param string $sParentId Used for recursion
*
* @return string A text representation of what will be installed
*/
public function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sParentId = '', $sDisplayChoices = '', &$aSelectedExtensions = null)
{
if ($sParentId == '') {
// Check once (before recursing) that the hidden modules are selected
foreach ($this->aAnalyzeInstallationModules as $sModuleId => $aModule) {
if (($sModuleId != ROOT_MODULE) && !isset($aModules[$sModuleId])) {
if (($aModule['category'] == 'authentication') || (!$aModule['visible'] && !isset($aModule['auto_select']))) {
$aModules[$sModuleId] = true;
$sDisplayChoices .= '<li><i>'.$aModule['label'].' (hidden)</i></li>';
}
}
}
}
$aOptions = $aInfo['options'] ?? [];
foreach ($aOptions as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
$aModuleInfo = [];
// Get the extension corresponding to the choice
foreach ($this->oExtensionsMap->GetAllExtensions() as $sExtensionVersion => $oExtension) {
if (utils::StartsWith($sExtensionVersion, $aChoice['extension_code'].'/')) {
$aModuleInfo = $oExtension->aModuleInfo;
break;
}
}
if ((isset($aChoice['mandatory']) && $aChoice['mandatory']) ||
(isset($aSelectedChoices[$sChoiceId]) && ($aSelectedChoices[$sChoiceId] == $sChoiceId))) {
$sDisplayChoices .= '<li>'.$aChoice['title'].'</li>';
if (isset($aChoice['modules'])) {
if (count($aChoice['modules']) === 0) {
throw new Exception('Extension '.$aChoice['extension_code'].' does not have any module associated');
}
foreach ($aChoice['modules'] as $sModuleId) {
$bSelected = true;
if (isset($aModuleInfo[$sModuleId])) {
// Test if module has 'auto_select'
$aCurrentModuleInfo = $aModuleInfo[$sModuleId];
if (isset($aCurrentModuleInfo['auto_select'])) {
// Check the module selection
try {
SetupInfo::SetSelectedModules($aModules);
$bSelected = $this->GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($aCurrentModuleInfo['auto_select']);
} catch (ModuleFileReaderException $e) {
//logged already
$bSelected = false;
}
}
}
if ($bSelected) {
$aModules[$sModuleId] = true; // store the Id of the selected module
SetupInfo::SetSelectedModules($aModules);
}
}
}
if ($aSelectedExtensions !== null) {
$aSelectedExtensions[] = $aChoice['extension_code'];
}
// Recurse only for selected choices
if (isset($aChoice['sub_options'])) {
$sDisplayChoices .= '<ul>';
$sDisplayChoices = $this->GetSelectedModules($aChoice['sub_options'], $aSelectedChoices, $aModules, $sChoiceId, $sDisplayChoices, $aSelectedExtensions);
$sDisplayChoices .= '</ul>';
}
$sDisplayChoices .= '</li>';
}
}
$aAlternatives = $aInfo['alternatives'] ?? [];
$sChoiceName = null;
foreach ($aAlternatives as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
if ($sChoiceName == null) {
$sChoiceName = $sChoiceId;
}
if ((isset($aChoice['mandatory']) && $aChoice['mandatory']) ||
(isset($aSelectedChoices[$sChoiceName]) && ($aSelectedChoices[$sChoiceName] == $sChoiceId))) {
$sDisplayChoices .= '<li>'.$aChoice['title'].'</li>';
if ($aSelectedExtensions !== null) {
$aSelectedExtensions[] = $aChoice['extension_code'];
}
if (isset($aChoice['modules'])) {
foreach ($aChoice['modules'] as $sModuleId) {
$aModules[$sModuleId] = true; // store the Id of the selected module
}
}
// Recurse only for selected choices
if (isset($aChoice['sub_options'])) {
$sDisplayChoices .= '<ul>';
$sDisplayChoices = $this->GetSelectedModules($aChoice['sub_options'], $aSelectedChoices, $aModules, $sChoiceId, $sDisplayChoices, $aSelectedExtensions);
$sDisplayChoices .= '</ul>';
}
$sDisplayChoices .= '</li>';
}
}
if ($sParentId == '') {
// Last pass (after all the user's choices are turned into "selected" modules):
// Process 'auto_select' modules for modules that are not already selected
do {
// Loop while new modules are added...
$bModuleAdded = false;
foreach ($this->aAnalyzeInstallationModules as $sModuleId => $aModule) {
if (($sModuleId != ROOT_MODULE) && !array_key_exists($sModuleId, $aModules) && isset($aModule['auto_select'])) {
try {
SetupInfo::SetSelectedModules($aModules);
$bSelected = $this->GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($aModule['auto_select']);
if ($bSelected) {
$aModules[$sModuleId] = true; // store the Id of the selected module
$sDisplayChoices .= '<li>'.$aModule['label'].' (auto_select)</li>';
$bModuleAdded = true;
}
} catch (ModuleFileReaderException $e) {
//logged already
$sDisplayChoices .= '<li><b>Warning: auto_select failed with exception ('.$e->getMessage().') for module "'.$sModuleId.'"</b></li>';
}
}
}
} while ($bModuleAdded);
}
return $sDisplayChoices;
}
protected function GetStepIndex()
{
switch ($this->sCurrentState) {
case 'start_install':
case 'start_upgrade':
$index = 0;
break;
default:
$index = (int)$this->sCurrentState;
}
return $index;
}
protected function GetStepInfo($idx = null)
{
$index = $idx ?? $this->GetStepIndex();
if (is_null($this->aSteps)) {
$this->oWizard->SetParameter('additional_extensions_modules', json_encode([])); // Default value, no additional extensions
if (@file_exists($this->GetSourceFilePath())) {
// Found an "installation.xml" file, let's use this definition for the wizard
$aParams = new XMLParameters($this->GetSourceFilePath());
$this->aSteps = $aParams->Get('steps', []);
if ($index + 1 >= count($this->aSteps)) {
//make sure we also cache next step as well
$aOptions = $this->oExtensionsMap->GetAllExtensionsOptionInfo();
// Display this step of the wizard only if there is something to display
if (count($aOptions) > 0) {
$this->aSteps[] = [
'title' => 'Extensions',
'description' => '<h2>Select additional extensions to install. You can launch the installation again to install new extensions or remove installed ones.</h2>',
'banner' => '/images/icons/icons8-puzzle.svg',
'options' => $aOptions,
];
$this->oWizard->SetParameter('additional_extensions_modules', json_encode($aOptions));
}
}
} else {
$aOptions = $this->oExtensionsMap->GetAllExtensionsOptionInfo();
// No wizard configuration provided, build a standard one with just one big list. All items are mandatory, only works when there are no conflicted modules.
$this->aSteps = [
[
'title' => 'Modules Selection',
'description' => '<h2>Select the modules to install. You can launch the installation again to install new modules, but you cannot remove already installed modules.</h2>',
'banner' => '/images/icons/icons8-apps-tab.svg',
'options' => $aOptions,
],
];
}
}
return $this->aSteps[$index] ?? null;
}
public function ComputeChoiceFlags(array $aChoice, string $sChoiceId, array $aSelectedComponents, bool $bAllDisabled, bool $bDisableUninstallCheck, bool $bUpgradeMode)
{
$oITopExtension = $this->oExtensionsMap->GetFromExtensionCode($aChoice['extension_code']);
//If the extension is missing from disk, it won't exist in the ExtensionsMap, thus returning null
$bCanBeUninstalled = isset($aChoice['uninstallable']) ? $aChoice['uninstallable'] === true || $aChoice['uninstallable'] === 'yes' : $oITopExtension->CanBeUninstalled();
$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId);
$bMissingFromDisk = isset($aChoice['missing']) && $aChoice['missing'] === true;
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']);
$bInstalled = $bMissingFromDisk || $oITopExtension->bInstalled;
$bChecked = $bSelected;
$bDisabled = false;
if ($bMissingFromDisk) {
$bDisabled = true;
$bChecked = false;
} elseif ($bMandatory) {
$bDisabled = true;
$bChecked = true;
} elseif ($bInstalled && !$bCanBeUninstalled && !$bDisableUninstallCheck) {
$bChecked = true;
$bDisabled = true;
}
if ($bAllDisabled) {
$bDisabled = true;
}
if (isset($aChoice['sub_options'])) {
$aOptions = $aChoice['sub_options']['options'] ?? [];
foreach ($aOptions as $index => $aSubChoice) {
$sSubChoiceId = $sChoiceId.self::$SEP.$index;
$aSubFlags = $this->ComputeChoiceFlags($aSubChoice, $sSubChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $bUpgradeMode);
if ($aSubFlags['checked']) {
$bChecked = true;
if ($aSubFlags['disabled']) {
//If some sub options are enabled and cannot be disabled, this choice should also cannot be disabled since it would disable all its sub options
$bDisabled = true;
}
}
}
}
return [
'uninstallable' => $bCanBeUninstalled,
'missing' => $bMissingFromDisk,
'installed' => $bInstalled,
'disabled' => $bDisabled,
'checked' => $bChecked,
];
}
protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults, $sParentId = '', $bAllDisabled = false)
{
$aOptions = $aStepInfo['options'] ?? [];
$aAlternatives = $aStepInfo['alternatives'] ?? [];
$bDisableUninstallCheck = (bool)$this->oWizard->GetParameter('force-uninstall', false);
foreach ($aOptions as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
$sDataId = 'data-id="'.utils::EscapeHtml($aChoice['extension_code']).'"';
$sId = utils::EscapeHtml($aChoice['extension_code']);
$aFlags = $this->ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $this->bUpgrade);
$sTooltip = '';
$sUnremovable = '';
if ($aFlags['missing']) {
$sTooltip .= '<div class="setup-extension-tag removed">source removed</div>';
}
if ($aFlags['installed']) {
$sTooltip .= '<div class="setup-extension-tag checked installed">installed</div>';
$sTooltip .= '<div class="setup-extension-tag unchecked tobeuninstalled">to be uninstalled</div>';
} else {
$sTooltip .= '<div class="setup-extension-tag checked tobeinstalled">to be installed</div>';
$sTooltip .= '<div class="setup-extension-tag unchecked notinstalled">not installed</div>';
}
if (!$aFlags['uninstallable']) {
$sTooltip .= '<div class="setup-extension-tag notuninstallable">cannot be uninstalled</div>';
}
if ($aFlags['disabled'] && !$aFlags['checked'] && !$aFlags['uninstallable'] && !$bDisableUninstallCheck) {
$this->bCanMoveForward = false;//Disable "Next"
}
$sChecked = $aFlags['checked'] ? ' checked ' : '';
$sDisabled = $aFlags['disabled'] ? ' disabled data-disabled="disabled" ' : '';
$sMissingModule = $aFlags['missing'] ? 'setup-extension--missing' : '';
$sHiddenInput = $aFlags['disabled'] && $aFlags['checked'] ? '<input type="hidden" name="choice['.$sChoiceId.']" value="'.$sChoiceId.'"/>' : '';
$oPage->add('<div class="choice '.$sMissingModule.'" '.$sDataId.'><input class="wiz-choice '.$sUnremovable.'" id="'.$sId.'" name="choice['.$sChoiceId.']" type="checkbox" value="'.$sChoiceId.'" '.$sDisabled.$sChecked.'/>'.$sHiddenInput.'&nbsp;');
$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $aFlags['disabled'], $sTooltip);
$oPage->add('</div>');
}
$sChoiceName = null;
$sDisabled = '';
$bDisabled = false;
$sChoiceIdNone = null;
foreach ($aAlternatives as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
if ($sChoiceName == null) {
$sChoiceName = $sChoiceId; // All radios share the same name
}
$bIsDefault = array_key_exists($sChoiceName, $aDefaults) && ($aDefaults[$sChoiceName] == $sChoiceId);
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || ($this->bUpgrade && $bIsDefault);
if ($bMandatory || $bAllDisabled) {
// One choice is mandatory, all alternatives are disabled
$sDisabled = ' disabled data-disabled="disabled"';
$bDisabled = true;
}
if ((!isset($aChoice['sub_options']) || (count($aChoice['sub_options']) == 0)) && (!isset($aChoice['modules']) || (count($aChoice['modules']) == 0))) {
$sChoiceIdNone = $sChoiceId; // the "None" / empty choice
}
}
if (!array_key_exists($sChoiceName, $aDefaults) || ($aDefaults[$sChoiceName] == $sChoiceIdNone)) {
// The "none" choice does not disable the selection !!
$sDisabled = '';
$bDisabled = false;
}
foreach ($aAlternatives as $index => $aChoice) {
$sAttributes = '';
$sChoiceId = $sParentId.self::$SEP.$index;
$sDataId = 'data-id="'.utils::EscapeHtml($aChoice['extension_code']).'"';
$sId = utils::EscapeHtml($aChoice['extension_code']);
if ($sChoiceName == null) {
$sChoiceName = $sChoiceId; // All radios share the same name
}
$bIsDefault = array_key_exists($sChoiceName, $aDefaults) && ($aDefaults[$sChoiceName] == $sChoiceId);
$bSelected = isset($aSelectedComponents[$sChoiceName]) && ($aSelectedComponents[$sChoiceName] == $sChoiceId);
if (!isset($aSelectedComponents[$sChoiceName]) && ($sChoiceIdNone != null)) {
// No choice selected, select the "None" option
$bSelected = ($sChoiceId == $sChoiceIdNone);
}
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || ($this->bUpgrade && $bIsDefault);
if ($bSelected) {
$sAttributes = ' checked ';
}
$sHidden = '';
if ($bMandatory && $bDisabled) {
$sAttributes = ' checked ';
$sHidden = '<input type="hidden" name="choice['.$sChoiceName.']" value="'.$sChoiceId.'"/>';
}
$oPage->add('<div class="choice" '.$sDataId.'><input class="wiz-choice" id="'.$sId.'" name="choice['.$sChoiceName.']" type="radio"'.$sAttributes.' value="'.$sChoiceId.'"'.$sDisabled.'/>'.$sHidden.'&nbsp;');
$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled && !$bSelected);
$oPage->add('</div>');
}
}
protected function DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled = false, $sTooltip = '')
{
$sMoreInfo = (isset($aChoice['more_info']) && ($aChoice['more_info'] != '')) ? '<a class="setup--wizard-choice--more-info" target="_blank" href="'.$aChoice['more_info'].'">More information</a>' : '';
$sSourceLabel = $aChoice['source_label'] ?? '';
$sId = utils::EscapeHtml($aChoice['extension_code']);
$oPage->add('<label class="setup--wizard-choice--label" for="'.$sId.'">'.$sSourceLabel.'<b>'.utils::EscapeHtml($aChoice['title']).'</b>'.'&nbsp;'.$sTooltip.'</label> '.$sMoreInfo.'');
$sDescription = isset($aChoice['description']) ? utils::EscapeHtml($aChoice['description']) : '';
$oPage->add('<div class="setup--wizard-choice--description description">'.$sDescription.'<span id="sub_choices'.$sId.'">');
if (isset($aChoice['sub_options'])) {
$this->DisplayOptions($oPage, $aChoice['sub_options'], $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled);
}
$oPage->add('</span></div>');
}
protected function GetSourceFilePath()
{
$sSourceDir = $this->oWizard->GetParameter('source_dir');
return $sSourceDir.'/installation.xml';
}
public function CanMoveForward()
{
return true;
}
public function JSCanMoveForward()
{
return $this->bCanMoveForward ? 'return true;' : 'return false;';
}
public function GetNextButtonLabel()
{
if (!$this->bCanMoveForward) {
return 'Non-uninstallable extension missing';
}
if ($this->GetStepInfo(1 + $this->GetStepIndex()) === null && $this->IsDataAuditEnabled()) {
return 'Check compatibility';
}
return 'Next';
}
}

View File

@@ -0,0 +1,264 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* Summary of the installation tasks
*/
class WizStepSummary extends AbstractWizStepInstall
{
public function GetTitle()
{
$sMode = $this->oWizard->GetParameter('mode', 'install');
if ($sMode == 'install') {
return 'Ready to install';
} else {
return 'Ready to upgrade';
}
}
public function GetPossibleSteps()
{
return [WizStepInstall::class];
}
/**
* Returns the label for the " Next >> " button
* @return string The label for the button
*/
public function GetNextButtonLabel()
{
return 'Install';
}
public function CanMoveForward()
{
if ($this->CheckDependencies()) {
return true;
} else {
return false;
}
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$this->oWizard->SaveParameter('db_backup', false);
$this->oWizard->SaveParameter('db_backup_path', '');
return new WizardState(WizStepInstall::class);
}
public function Display(WebPage $oPage)
{
$aInstallParams = $this->BuildConfig();
$sMode = $aInstallParams['mode'];
$sDestination = ITOP_APPLICATION.(($sMode == 'install') ? ' version '.ITOP_VERSION.' is about to be installed ' : ' is about to be upgraded ');
$sDBDescription = ' <b>existing</b> database <b>'.$aInstallParams['database']['name'].'</b>';
if (($sMode == 'install') && ($this->oWizard->GetParameter('create_db') == 'yes')) {
$sDBDescription = ' <b>new</b> database <b>'.$aInstallParams['database']['name'].'</b>';
}
$sDestination .= 'into the '.$sDBDescription.' on the server <b>'.$aInstallParams['database']['server'].'</b>.';
$oPage->add('<h2>'.$sDestination.'</h2>');
$oPage->add('<fieldset id="summary"><legend>Installation Parameters</legend>');
$oPage->add('<div id="params_summary">');
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Extensions to be installed</span>');
$aExtensionsAdded = json_decode($this->oWizard->GetParameter('extensions_added'), true);
if (count($aExtensionsAdded) > 0) {
$sExtensionsAdded = '<ul>';
foreach ($aExtensionsAdded as $sExtensionCode => $sLabel) {
$sExtensionsAdded .= '<li>'.$sLabel.'</li>';
}
$sExtensionsAdded .= '</ul>';
} else {
$sExtensionsAdded = '<ul><li>No extension added.</li></ul>';
}
$oPage->add($sExtensionsAdded);
$oPage->add('</div>');
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Extensions to be uninstalled</span>');
$aExtensionsRemoved = json_decode($this->oWizard->GetParameter('removed_extensions'), true) ?? [];
$aExtensionsNotUninstallable = json_decode($this->oWizard->GetParameter('extensions_not_uninstallable'));
if (count($aExtensionsRemoved) > 0) {
$sExtensionsRemoved = '<ul>';
foreach ($aExtensionsRemoved as $sExtensionCode => $sLabel) {
if (in_array($sExtensionCode, $aExtensionsNotUninstallable)) {
$sExtensionsRemoved .= '<li>'.$sLabel.' (forced uninstallation)</li>';
} else {
$sExtensionsRemoved .= '<li>'.$sLabel.'</li>';
}
}
$sExtensionsRemoved .= '</ul>';
} else {
$sExtensionsRemoved = '<ul><li>No extension removed.</li></ul>';
}
$oPage->add($sExtensionsRemoved);
$oPage->add('</div>');
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Database Parameters</span><ul>');
$oPage->add('<li>Server Name: '.$aInstallParams['database']['server'].'</li>');
$oPage->add('<li>DB User Name: '.$aInstallParams['database']['user'].'</li>');
$oPage->add('<li>DB user password: ***</li>');
if (($sMode == 'install') && ($this->oWizard->GetParameter('create_db') == 'yes')) {
$oPage->add('<li>Database Name: '.$aInstallParams['database']['name'].' (will be created)</li>');
} else {
$oPage->add('<li>Database Name: '.$aInstallParams['database']['name'].'</li>');
}
if ($aInstallParams['database']['prefix'] != '') {
$oPage->add('<li>Prefix for the '.ITOP_APPLICATION.' tables: '.$aInstallParams['database']['prefix'].'</li>');
} else {
$oPage->add('<li>Prefix for the '.ITOP_APPLICATION.' tables: none</li>');
}
$oPage->add('</ul></div>');
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Data Model Configuration</span>');
$oPage->add($this->oWizard->GetParameter('display_choices'));
$oPage->add('</div>');
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Other Parameters</span><ul>');
if ($sMode == 'install') {
$oPage->add('<li>Default language: '.$aInstallParams['language'].'</li>');
}
$oPage->add('<li>URL to access the application: '.$aInstallParams['url'].'</li>');
$oPage->add('<li>Graphviz\' dot path: '.$aInstallParams['graphviz_path'].'</li>');
if ($aInstallParams['sample_data']) {
$oPage->add('<li>Sample data will be loaded into the database.</li>');
}
if ($aInstallParams['old_addon']) {
$oPage->add('<li>Compatibility mode: Using the version 1.2 of the UserRightsProfiles add-on.</li>');
}
$oPage->add('</ul></div>');
if ($sMode == 'install') {
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Administrator Account</span><ul>');
$oPage->add('<li>Login: '.$aInstallParams['admin_account']['user'].'</li>');
$oPage->add('<li>Password: '.$aInstallParams['admin_account']['pwd'].'</li>');
$oPage->add('<li>Language: '.$aInstallParams['admin_account']['language'].'</li>');
$oPage->add('</ul></div>');
}
$aMiscOptions = $aInstallParams['options'];
if (count($aMiscOptions) > 0) {
$oPage->add('<div class="closed"><span class="title">Miscellaneous Options</span><ul>');
foreach ($aMiscOptions as $sKey => $sValue) {
$oPage->add('<li>'.$sKey.': '.$sValue.'</li>');
}
$oPage->add('</ul></div>');
}
if (isset($aMiscOptions['generate_config'])) {
$oDoc = new DOMDocument('1.0', 'UTF-8');
$oDoc->preserveWhiteSpace = false;
$oDoc->formatOutput = true;
$oParams = new PHPParameters();
$oParams->LoadFromHash($aInstallParams);
$oParams->ToXML($oDoc, null, 'installation');
$sXML = $oDoc->saveXML();
$oPage->add('<div class="closed"><span class="title">XML Config file</span><ul><pre>');
$oPage->add(utils::EscapeHtml($sXML));
$oPage->add('</pre></ul></div>');
}
$oPage->add('</div>'); // params_summary
$oPage->add('</fieldset>');
if (!$this->CheckDependencies()) {
$oPage->error($this->sDependencyIssue);
}
if ($sMode !== 'install') {
$bDBBackup = $this->oWizard->GetParameter('db_backup', false);
$sDefaultBackupPath = utils::GetDataPath().'backups/manual/setup-'.date('Y-m-d_H_i');
$sDBBackupPath = $this->oWizard->GetParameter('db_backup_path', $sDefaultBackupPath);
$sMySQLBinDir = $this->oWizard->GetParameter('mysql_bindir', null);
$aPreviousInstance = SetupUtils::GetPreviousInstance(APPROOT);
if ($aPreviousInstance['found']) {
$sMySQLBinDir = $aPreviousInstance['mysql_bindir'];
$this->oWizard->SaveParameter('mysql_bindir', $aPreviousInstance['mysql_bindir']);
}
$aBackupChecks = SetupUtils::CheckBackupPrerequisites($sDBBackupPath, $sMySQLBinDir);
$bCanBackup = true;
$sMySQLDumpMessage = '';
foreach ($aBackupChecks as $oCheck) {
switch ($oCheck->iSeverity) {
case CheckResult::ERROR:
$bCanBackup = false;
$sMySQLDumpMessage .= '<div class="message message-error"><span class="message-title">Error:</span>'.$oCheck->sLabel.'</div>';
break;
case CheckResult::TRACE:
SetupLog::Ok($oCheck->sLabel);
break;
default:
$sMySQLDumpMessage .= '<div class="message message-valid"><span class="message-title">Success:</span>'.$oCheck->sLabel.'</div>';
break;
}
}
$sChecked = ($bCanBackup && $bDBBackup) ? ' checked ' : '';
$sDisabled = $bCanBackup ? '' : ' disabled ';
$oPage->add('<br/>');
$oPage->add('<input id="db_backup" type="checkbox" name="db_backup" '.$sChecked.$sDisabled.' value="1"/><label for="db_backup">Backup the '.ITOP_APPLICATION.' database before upgrading</label>');
$oPage->add('<div class="setup-backup--input--container">Save the backup to:<input id="db_backup_path" class="ibo-input" type="text" name="db_backup_path" '.$sDisabled.'value="'.utils::EscapeHtml($sDBBackupPath).'"/></div>');
$fFreeSpace = SetupUtils::CheckDiskSpace($sDBBackupPath);
$sMessage = '';
if ($fFreeSpace !== false) {
$sMessage .= SetupUtils::HumanReadableSize($fFreeSpace).' free in '.dirname($sDBBackupPath);
}
$oPage->add($sMySQLDumpMessage.'<span id="backup_info" style="font-size:small;color:#696969;">'.$sMessage.'</span>');
}
$sAuthentToken = $this->oWizard->GetParameter('authent', '');
$oPage->add('<input type="hidden" id="authent_token" value="'.$sAuthentToken.'"/>');
$oPage->add_ready_script(
<<<JS
$("#db_backup_path").on('change keyup', function() { WizardAsyncAction('check_backup', { db_backup_path: $('#db_backup_path').val() }); });
$("#params_summary div").addClass('closed');
$("#params_summary .title").on('click', function() { $(this).parent().toggleClass('closed'); } );
JS
);
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return 'return true;';
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveBackward()
{
return 'return true;';
}
}

View File

@@ -0,0 +1,159 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* Miscellaneous Parameters (URL...) in case of upgrade
*/
class WizStepUpgradeMiscParams extends AbstractWizStepMiscParams
{
public function GetTitle()
{
return 'Miscellaneous Parameters';
}
public function GetPossibleSteps()
{
return [WizStepModulesChoice::class];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$this->oWizard->SaveParameter('application_url', '');
$this->oWizard->SaveParameter('graphviz_path', '');
$this->oWizard->SaveParameter('force-uninstall', false);
return new WizardState(WizStepModulesChoice::class, 'start_upgrade');
}
public function Display(WebPage $oPage)
{
$sApplicationURL = $this->oWizard->GetParameter('application_url', utils::GetAbsoluteUrlAppRoot(true)); //Preserve existing configuration (except for the str_replace based joker $SERVER_NAME$ which is lost)
$sDefaultGraphvizPath = (strtolower(substr(PHP_OS, 0, 3)) === 'win') ? 'C:\\Program Files\\Graphviz\\bin\\dot.exe' : '/usr/bin/dot';
$sGraphvizPath = $this->oWizard->GetParameter('graphviz_path', $sDefaultGraphvizPath);
$oPage->add('<h2>Additional parameters</h2>');
$oPage->add('<fieldset>');
$oPage->add('<legend>Application URL</legend>');
$oPage->add('<table>');
$oPage->add('<tr><td>URL: </td><td><input id="application_url" class="ibo-input" name="application_url" type="text" size="35" maxlength="1024" value="'.utils::EscapeHtml($sApplicationURL).'" style="width: 100%;box-sizing: border-box;"><span id="v_application_url"/></td><tr>');
$oPage->add('</table>');
$oPage->add('<div class="message message-warning">Change the value above if the end-users will be accessing the application by another path due to a specific configuration of the web server.</div>');
$oPage->add('</fieldset>');
$oPage->add('<fieldset>');
$oPage->add('<legend>Path to Graphviz\' dot application</legend>');
$oPage->add('<table>');
$oPage->add('<tr><td>Path: </td><td><input id="graphviz_path" class="ibo-input" name="graphviz_path" type="text" size="35" maxlength="1024" value="'.utils::EscapeHtml($sGraphvizPath).'" style="width: 100%;box-sizing: border-box;"><span id="v_graphviz_path"/></td>');
$oPage->add('<td><i class="fas fa-question-circle setup-input--hint--icon" data-tooltip-content="Graphviz is required to display the impact analysis graph (i.e. impacts / depends on)."></i></td><tr>');
$oPage->add('</table>');
$oPage->add('<span id="graphviz_status"></span>');
$oPage->add('</fieldset>');
$sAuthentToken = $this->oWizard->GetParameter('authent', '');
$oPage->add('<input type="hidden" id="authent_token" value="'.$sAuthentToken.'"/>');
$oPage->add_ready_script(
<<<EOF
$('#application_url').on('change keyup', function() { WizardUpdateButtons(); } );
$('#graphviz_path').on('change keyup init', function() { WizardUpdateButtons(); WizardAsyncAction('check_graphviz', { graphviz_path: $('#graphviz_path').val(), authent: $('#authent_token').val() }); } ).trigger('init');
$('#btn_next').on('click', function() {
bRet = true;
if ($(this).attr('data-graphviz') != 'ok')
{
bRet = confirm('The impact analysis will not be displayed properly. Are you sure you want to continue?');
}
return bRet;
});
EOF
);
$this->AddUseSymlinksFlagOption($oPage);
$this->AddForceUninstallFlagOption($oPage);
}
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
switch ($sCode) {
case 'check_graphviz':
$sGraphvizPath = $aParameters['graphviz_path'];
$aCheck = SetupUtils::CheckGraphviz($sGraphvizPath);
// N°2214 logging TRACE results
$aTraceCheck = CheckResult::FilterCheckResultArray($aCheck, [CheckResult::TRACE]);
foreach ($aTraceCheck as $oTraceCheck) {
SetupLog::Ok($oTraceCheck->sLabel);
}
$aNonTraceCheck = array_diff($aCheck, $aTraceCheck);
foreach ($aNonTraceCheck as $oCheck) {
switch ($oCheck->iSeverity) {
case CheckResult::INFO:
$sStatus = 'ok';
$sInfoExplanation = $oCheck->sLabel;
$sMessage = json_encode('<div class="message message-valid">'.$sInfoExplanation.'</div>');
break;
default:
case CheckResult::ERROR:
case CheckResult::WARNING:
$sStatus = 'ko';
$sErrorExplanation = $oCheck->sLabel;
$sMessage = json_encode('<div class="message message-error">'.$sErrorExplanation.'</div>');
break;
}
$oPage->add_ready_script(
<<<JS
$("#graphviz_status").html($sMessage);
$('#btn_next').attr('data-graphviz', '$sStatus');
JS
);
}
break;
}
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return
<<<EOF
bRet = ($('#application_url').val() != '');
if (!bRet)
{
$("#v_application_url").html('<img src="../images/validation_error.png" title="This field cannot be empty"/>');
}
else
{
$("#v_application_url").html('');
}
bGraphviz = ($('#graphviz_path').val() != '');
if (!bGraphviz)
{
// Does not prevent to move forward
$("#v_graphviz_path").html('<img src="../images/validation_error.png" title="Impact analysis will not display properly"/>');
}
else
{
$("#v_graphviz_path").html('');
}
return bRet;
EOF
;
}
}

View File

@@ -0,0 +1,138 @@
<?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
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* First step of the iTop Installation Wizard: Welcome screen, requirements
*/
class WizStepWelcome extends WizardStep
{
protected $bCanMoveForward;
public function GetTitle()
{
return 'Welcome to '.ITOP_APPLICATION.' version '.ITOP_VERSION;
}
/**
* Returns the label for the " Next >> " button
* @return string The label for the button
*/
public function GetNextButtonLabel()
{
return 'Continue';
}
public function GetPossibleSteps()
{
return [WizStepInstallOrUpgrade::class];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$sUID = SetupUtils::CreateSetupToken();
$this->oWizard->SetParameter('authent', $sUID);
return new WizardState(WizStepInstallOrUpgrade::class);
}
public function Display(WebPage $oPage)
{
// Store the misc_options for the future...
$aMiscOptions = utils::ReadParam('option', [], false, 'raw_data');
$sMiscOptions = $this->oWizard->GetParameter('misc_options', json_encode($aMiscOptions));
$this->oWizard->SetParameter('misc_options', $sMiscOptions);
$oPage->add("<!--[if lt IE 11]><div id=\"old_ie\"></div><![endif]-->");
$oPage->add_ready_script(
<<<EOF
if ($('#old_ie').length > 0)
{
alert("Internet Explorer version 10 or older is NOT supported! (Check that IE is not running in compatibility mode)");
}
EOF
);
$oPage->add('<h1>'.ITOP_APPLICATION.' Installation Wizard</h1>');
$aResults = SetupUtils::CheckPhpAndExtensions();
$this->bCanMoveForward = true;
$aInfo = [];
$aWarnings = [];
$aErrors = [];
foreach ($aResults as $oCheckResult) {
switch ($oCheckResult->iSeverity) {
case CheckResult::ERROR:
$aErrors[] = $oCheckResult->sLabel;
$this->bCanMoveForward = false;
break;
case CheckResult::WARNING:
$aWarnings[] = $oCheckResult->sLabel;
break;
case CheckResult::INFO:
$aInfo[] = $oCheckResult->sLabel;
break;
case CheckResult::TRACE:
SetupLog::Ok($oCheckResult->sLabel);
break;
}
}
$sStyle = 'style="display:none;overflow:auto;"';
$sToggleButtons = '<button type="button" id="show_details" class="ibo-button ibo-is-alternative ibo-is-neutral" onclick="$(\'#details\').toggle(); $(this).toggle(); $(\'#hide_details\').toggle();"><span class="ibo-button--icon fa fa-caret-down"></span><span class="ibo-button--label">Show details</span></button><button type="button" id="hide_details" class="ibo-button ibo-is-alternative ibo-is-neutral" style="display:none;" onclick="$(\'#details\').toggle(); $(this).toggle(); $(\'#show_details\').toggle();"><span class="ibo-button--icon fa fa-caret-up"></span><span class="ibo-button--label">Hide details</span></button>';
if (count($aErrors) > 0) {
$sStyle = 'overflow:auto;"';
$sTitle = count($aErrors).' Error(s), '.count($aWarnings).' Warning(s).';
$sH2Class = 'text-error';
} elseif (count($aWarnings) > 0) {
$sTitle = count($aWarnings).' Warning(s) '.$sToggleButtons;
$sH2Class = 'text-warning';
} else {
$sTitle = 'Ok. '.$sToggleButtons;
$sH2Class = 'text-valid';
}
$oPage->add(
<<<HTML
<h2 class="message">Prerequisites validation: <span class="$sH2Class">$sTitle</span></h2>
<div id="details" $sStyle>
HTML
);
foreach ($aErrors as $sText) {
$oPage->error($sText);
}
foreach ($aWarnings as $sText) {
$oPage->warning($sText);
}
foreach ($aInfo as $sText) {
$oPage->ok($sText);
}
$oPage->add('</div>');
if (!$this->bCanMoveForward) {
$oPage->p('Sorry, the installation cannot continue. Please fix the errors and reload this page to launch the installation again.');
$oPage->p('<button type="button" onclick="window.location.reload()">Reload</button>');
}
$oPage->add_ready_script('CheckDirectoryConfFilesPermissions("'.utils::GetItopVersionWikiSyntax().'")');
}
public function CanMoveForward()
{
return $this->bCanMoveForward;
}
}

View File

@@ -0,0 +1,21 @@
<?php
class WizardState
{
public function __construct($sNextStep, $sCurrentState = '')
{
$this->sNextStep = $sNextStep;
$this->sCurrentState = $sCurrentState;
}
private string $sNextStep;
private string $sCurrentState;
public function GetNextStep()
{
return $this->sNextStep;
}
public function GetState()
{
return $this->sCurrentState;
}
}

View File

@@ -0,0 +1,379 @@
<?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
*/
/**
* All the steps of the iTop installation wizard
*
* Steps order (can be retrieved using \WizardController::DumpStructure) :
*
* WizStepWelcome
* WizStepInstallOrUpgrade
* + +
* | |
* v +----->
* WizStepLicense WizStepDetectedInfo
* WizStepDBParams + +
* WizStepAdminAccount | |
* WizStepInstallMiscParams v +------>
* + WizStepLicense2 +--> WizStepUpgradeMiscParams
* | +
* +---> <-----------------------------------+
* WizStepModulesChoice
* WizStepSummary
* WizStepDone
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* Abstract class to build "steps" for the wizard controller
* If a step needs to maintain an internal "state" (for complex steps)
* then it's up to the derived class to implement the behavior based on
* the internal 'sCurrentState' variable.
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
abstract class WizardStep
{
/**
* A reference to the WizardController
* @var WizardController
*/
protected $oWizard;
/**
* Current 'state' of the wizard step. Simple 'steps' can ignore it
* @var string
*/
protected $sCurrentState;
protected $bDependencyCheck = null;
protected $sDependencyIssue = null;
protected function CheckDependencies()
{
if (is_null($this->bDependencyCheck)) {
$aSelectedModules = json_decode($this->oWizard->GetParameter('selected_modules'), true);
$this->bDependencyCheck = true;
try {
SetupUtils::AnalyzeInstallation($this->oWizard, true, $aSelectedModules);
} catch (MissingDependencyException $e) {
$this->bDependencyCheck = false;
$this->sDependencyIssue = $e->getHtmlDesc();
}
}
return $this->bDependencyCheck;
}
public function __construct(WizardController $oWizard, $sCurrentState)
{
$this->oWizard = $oWizard;
$this->sCurrentState = $sCurrentState;
}
public function GetState()
{
return $this->sCurrentState;
}
/**
* Displays the wizard page for the current class/state
* The page can contain any number of "<input/>" fields, but no "<form>...</form>" tag
* The name of the input fields (and their id if one is supplied) MUST NOT start with "_"
* (this is reserved for the wizard's own parameters)
* @return void
*/
abstract public function Display(WebPage $oPage);
/**
* Processes the page's parameters and (if moving forward) returns the next step/state to be displayed
* @param bool $bMoveForward True if the wizard is moving forward 'Next >>' button pressed, false otherwise
* @return hash array('class' => $sNextClass, 'state' => $sNextState)
*/
abstract public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState;
/**
* Returns the list of possible steps from this step forward
* @return array Array of strings (step classes)
*/
abstract public function GetPossibleSteps();
/**
* Returns title of the current step
* @return string The title of the wizard page for the current step
*/
abstract public function GetTitle();
/**
* Tells whether the parameters are Ok to move forward
* @return boolean True to move forward, false to stey on the same step
*/
public function ValidateParams()
{
return true;
}
/**
* Tells whether this step/state is the last one of the wizard (dead-end)
* @return boolean True if the 'Next >>' button should be displayed
*/
public function CanMoveForward()
{
return true;
}
/**
* Tells whether the user will come back to this step/state if he click on "Back"
* @return boolean True if the 'Back' button should display this step
*/
public function CanComeBack()
{
return true;
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return 'return true;';
}
/**
* Returns the label for the " Next >> " button
* @return string The label for the button
*/
public function GetNextButtonLabel()
{
return 'Next';
}
/**
* Tells whether this step/state allows to go back or not
* @return boolean True if the '<< Back' button should be displayed
*/
public function CanMoveBackward()
{
return true;
}
/**
* Tells whether the "Back" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveBackward()
{
return 'return true;';
}
/**
* Tells whether this step of the wizard requires that the configuration file be writable
* @return bool True if the wizard will possibly need to modify the configuration at some point
*/
public function RequiresWritableConfig()
{
return true;
}
/**
* Overload this function to implement asynchronous action(s) (AJAX)
* @param string $sCode The code of the action (if several actions need to be distinguished)
* @param hash $aParameters The action's parameters name => value
*/
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
}
}
/*
* Example of a simple Setup Wizard with some parameters to store
* the installation mode (install | upgrade) and a simple asynchronous
* (AJAX) action.
*
* The setup wizard is executed by the following code:
*
* $oWizard = new WizardController('Step1');
* $oWizard->Run();
*
class Step1 extends WizardStep
{
public function GetTitle()
{
return 'Welcome';
}
public function GetPossibleSteps()
{
return array(Step2::class, Step2bis::class);
}
public function ProcessParams($bMoveForward = true)
{
$sNextStep = '';
$sInstallMode = utils::ReadParam('install_mode');
if ($sInstallMode == 'install')
{
$this->oWizard->SetParameter('install_mode', 'install');
$sNextStep = Step2::class;
}
else
{
$this->oWizard->SetParameter('install_mode', 'upgrade');
$sNextStep = Step2bis::class;
}
return array('class' => $sNextStep, 'state' => '');
}
public function Display(WebPage $oPage)
{
$oPage->p('This is Step 1!');
$sInstallMode = $this->oWizard->GetParameter('install_mode', 'install');
$sChecked = ($sInstallMode == 'install') ? ' checked ' : '';
$oPage->p('<input type="radio" name="install_mode" value="install"'.$sChecked.'/> Install');
$sChecked = ($sInstallMode == 'upgrade') ? ' checked ' : '';
$oPage->p('<input type="radio" name="install_mode" value="upgrade"'.$sChecked.'/> Upgrade');
}
}
class Step2 extends WizardStep
{
public function GetTitle()
{
return 'Installation Parameters';
}
public function GetPossibleSteps()
{
return array(Step3::class);
}
public function ProcessParams($bMoveForward = true)
{
return array('class' => Step3::class, 'state' => '');
}
public function Display(WebPage $oPage)
{
$oPage->p('This is Step 2! (Installation)');
}
}
class Step2bis extends WizardStep
{
public function GetTitle()
{
return 'Upgrade Parameters';
}
public function GetPossibleSteps()
{
return array(Step2ter::class);
}
public function ProcessParams($bMoveForward = true)
{
$sUpgradeInfo = utils::ReadParam('upgrade_info');
$this->oWizard->SetParameter('upgrade_info', $sUpgradeInfo);
$sAdditionalUpgradeInfo = utils::ReadParam('additional_upgrade_info');
$this->oWizard->SetParameter('additional_upgrade_info', $sAdditionalUpgradeInfo);
return array('class' => Step2ter::class, 'state' => '');
}
public function Display(WebPage $oPage)
{
$oPage->p('This is Step 2bis! (Upgrade)');
$sUpgradeInfo = $this->oWizard->GetParameter('upgrade_info', '');
$oPage->p('Type your name here: <input type="text" id="upgrade_info" name="upgrade_info" value="'.$sUpgradeInfo.'" size="20"/><span id="v_upgrade_info"></span>');
$sAdditionalUpgradeInfo = $this->oWizard->GetParameter('additional_upgrade_info', '');
$oPage->p('The installer replies: <input type="text" name="additional_upgrade_info" value="'.$sAdditionalUpgradeInfo.'" size="20"/>');
$oPage->add_ready_script("$('#upgrade_info').change(function() {
$('#v_upgrade_info').html('<img src=\"../images/indicator.gif\"/>');
WizardAsyncAction('', { upgrade_info: $('#upgrade_info').val() }); });");
}
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
usleep(300000); // 300 ms
$sName = $aParameters['upgrade_info'];
$sReply = addslashes("Hello ".$sName);
$oPage->add_ready_script(
<<<EOF
$("#v_upgrade_info").html('');
$("input[name=additional_upgrade_info]").val("$sReply");
EOF
);
}
}
class Step2ter extends WizardStep
{
public function GetTitle()
{
return 'Additional Upgrade Info';
}
public function GetPossibleSteps()
{
return array(Step3::class);
}
public function ProcessParams($bMoveForward = true)
{
return array('class' => Step3::class, 'state' => '');
}
public function Display(WebPage $oPage)
{
$oPage->p('This is Step 2ter! (Upgrade)');
}
}
class Step3 extends WizardStep
{
public function GetTitle()
{
return 'Installation Complete';
}
public function GetPossibleSteps()
{
return array();
}
public function ProcessParams($bMoveForward = true)
{
return array('class' => '', 'state' => '');
}
public function Display(WebPage $oPage)
{
$oPage->p('This is the FINAL Step');
}
public function CanMoveForward()
{
return false;
}
}
End of the example */

View File

@@ -0,0 +1,21 @@
<?php
require_once(APPROOT.'setup/wizardsteps/WizardState.php');
require_once(APPROOT.'setup/wizardsteps/WizardStep.php');
require_once(APPROOT.'setup/wizardsteps/AbstractWizStepInstall.php');
require_once(APPROOT.'setup/wizardsteps/WizStepWelcome.php');
require_once(APPROOT.'setup/wizardsteps/WizStepInstallOrUpgrade.php');
require_once(APPROOT.'setup/wizardsteps/WizStepDetectedInfo.php');
require_once(APPROOT.'setup/wizardsteps/WizStepLicense.php');
require_once(APPROOT.'setup/wizardsteps/WizStepLicense2.php');
require_once(APPROOT.'setup/wizardsteps/AbstractWizStepMiscParams.php');
require_once(APPROOT.'setup/wizardsteps/WizStepAdminAccount.php');
require_once(APPROOT.'setup/wizardsteps/WizStepInstall.php');
require_once(APPROOT.'setup/wizardsteps/WizStepDataAudit.php');
require_once(APPROOT.'setup/wizardsteps/WizStepDBParams.php');
require_once(APPROOT.'setup/wizardsteps/WizStepDone.php');
require_once(APPROOT.'setup/wizardsteps/WizStepInstallMiscParams.php');
require_once(APPROOT.'setup/wizardsteps/WizStepModulesChoice.php');
require_once(APPROOT.'setup/wizardsteps/WizStepSummary.php');
require_once(APPROOT.'setup/wizardsteps/WizStepUpgradeMiscParams.php');
require_once(APPROOT.'setup/wizardcontroller.class.inc.php');

View File

@@ -1,7 +1,7 @@
includes:
- php-includes/set-php-version-from-process.php # Workaround to set PHP version to the on running the CLI
# for an explanation of the baseline concept, see: https://phpstan.org/user-guide/baseline
#baseline HERE DO NOT REMOVE FOR CI
#baseline HERE DO NOT REMOVE FOR CI
parameters:
level: 0

View File

@@ -188,6 +188,9 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
CMDBSource::DropTable("priv_module_install");
CMDBSource::Query("CREATE TABLE $sNewDB.priv_module_install SELECT * FROM $sPreviousDB.priv_module_install");
CMDBSource::DropTable("priv_extension_install");
CMDBSource::Query("CREATE TABLE $sNewDB.priv_extension_install SELECT * FROM $sPreviousDB.priv_extension_install");
$this->debug("Custom environment '$sTestEnv' is ready!");
} else {
$this->debug("Custom environment '$sTestEnv' READY BUILT:");

View File

@@ -498,6 +498,34 @@ class MetaModelTest extends ItopDataTestCase
'Purge 10 items with a max_chunk_size of 1000 (default value) should be perfomed in 1 step' => [1000, 3],
];
}
public function testGetCreatedIn_UnknownClass()
{
$this->expectExceptionMessage("Cannot find class module");
$this->expectException(CoreException::class);
MetaModel::GetModuleName('GABUZOMEU');
}
public function testGetCreatedIn_ClassComingFromCorePhpFile()
{
$this->assertEquals('core', MetaModel::GetModuleName('BackgroundTask'));
}
public function testGetCreatedIn_ClassComingFromCorePhpFile2()
{
$this->assertEquals('core', MetaModel::GetModuleName('lnkActionNotificationToContact'));
}
public function testGetCreatedIn_ClassComingFromModulePhpFile()
{
$this->assertEquals('itop-attachments', MetaModel::GetModuleName('CMDBChangeOpAttachmentAdded'));
}
public function testGetCreatedIn_ClassComingFromXmlDataModelFile()
{
$this->assertEquals('authent-ldap', MetaModel::GetModuleName('UserLDAP'));
}
}
abstract class Wizzard

View File

@@ -0,0 +1,60 @@
<?php
class ApplicationInstallSequencerFake extends ApplicationInstallSequencer
{
public function __construct(Parameters $oParams)
{
$this->oParams = $oParams;
}
protected function DoLogParameters($sPrefix = 'install-', $sOperation = '')
{
}
protected function DoCopy($aCopies)
{
}
protected function DoBackup($sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null)
{
}
protected function DoCompile($aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null)
{
}
protected function DoUpdateDBSchema($aSelectedModules)
{
}
protected function AfterDBCreate(
$aAdminParams,
$aSelectedModules
) {
}
protected function DoLoadFiles(
$aSelectedModules,
$bSampleData = false
) {
}
protected function DoCreateConfig(
$sPreviousConfigFile,
$sDataModelVersion,
$aSelectedModuleCodes,
$aSelectedExtensionCodes,
$sInstallComment = null
) {
}
protected function EnterReadOnlyMode()
{
}
protected function ExitReadOnlyMode()
{
}
}

View File

@@ -0,0 +1,65 @@
<?php
class DataAuditSequencerFake extends DataAuditSequencer
{
public function __construct(Parameters $oParams)
{
$this->oParams = $oParams;
}
protected function DoCopy($aCopies)
{
}
protected function DoBackup($sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null)
{
}
protected function DoCompile($aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null)
{
}
protected function DoUpdateDBSchema($aSelectedModules)
{
}
protected function AfterDBCreate(
$aAdminParams,
$aSelectedModules
) {
}
protected function DoLoadFiles(
$aSelectedModules,
$bSampleData = false
) {
}
protected function DoCreateConfig(
$sPreviousConfigFile,
$sDataModelVersion,
$aSelectedModuleCodes,
$aSelectedExtensionCodes,
$sInstallComment = null
) {
}
protected function DoSetupAudit()
{
}
protected function DoCleanup()
{
}
protected function EnterReadOnlyMode()
{
}
protected function ExitReadOnlyMode()
{
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use ApplicationInstallSequencerFake;
use DataAuditSequencerFake;
use PHPParameters;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use SetupUtils;
class StepSequencerTest extends ItopTestCase
{
protected function setUp(): void
{
static::LoadRequiredItopFiles();
parent::setUp();
$this->RequireOnceItopFile('/setup/sequencers/ApplicationInstallSequencer.php');
$this->RequireOnceItopFile('/setup/sequencers/DataAuditSequencer.php');
$this->RequireOnceItopFile('/setup/parameters.class.inc.php');
$this->RequireOnceItopFile('/setup/setuputils.class.inc.php');
require_once __DIR__.'/ApplicationInstallSequencerFake.php';
require_once __DIR__.'/DataAuditSequencerFake.php';
}
public function testApplicationInstallSequencer()
{
$oParams = new PHPParameters();
$oParams->LoadFromHash([]);
$oInstallSequencer = new ApplicationInstallSequencerFake($oParams);
$oInstallSequencer->ExecuteAllSteps();
$aStepSequence = [
'',
'copy',
'compile',
'db-schema',
'after-db-create',
'load-data',
'create-config',
];
$this->AssertStepsHistoryIs($aStepSequence, $oInstallSequencer);
}
public function testDataAuditSequencer()
{
$oParams = new PHPParameters();
$oParams->LoadFromHash([]);
$oInstallSequencer = new DataAuditSequencerFake($oParams);
$oInstallSequencer->ExecuteAllSteps();
$aStepSequence = [
'',
'compile',
'write-config',
'setup-audit',
'cleanup',
];
$this->AssertStepsHistoryIs($aStepSequence, $oInstallSequencer);
}
protected function AssertStepsHistoryIs($aExpectedStepSequence, $oSequencer)
{
$aHistory = $oSequencer->GetHistory();
$aStepSequence = [];
foreach ($aHistory as $aStep) {
$aStepSequence[] = $aStep['step'];
}
$this->assertEquals($aExpectedStepSequence, $aStepSequence, 'The step sequence should be '.implode(',', $aExpectedStepSequence));
}
}

View File

@@ -4,7 +4,8 @@ class WizStepModulesChoiceFake extends WizStepModulesChoice
{
public function __construct(WizardController $oWizard, $sCurrentState)
{
$this->oWizard = $oWizard;
$this->sCurrentState = $sCurrentState;
}
public function setExtensionMap(iTopExtensionsMap $oMap)

View File

@@ -3,13 +3,16 @@
namespace Combodo\iTop\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use ItopExtensionsMap;
use iTopExtensionsMap;
use iTopExtensionsMapFake;
use ModuleDiscovery;
use WizardController;
use WizStepModulesChoiceFake;
use XMLParameters;
class WizStepModulesChoiceTest extends ItopTestCase
{
private WizStepModulesChoiceFake $oStep;
protected function setUp(): void
{
parent::setUp();
@@ -17,7 +20,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
require_once __DIR__.'/iTopExtensionsMapFake.php';
require_once __DIR__.'/WizStepModulesChoiceFake.php';
$this->oStep = new \WizStepModulesChoiceFake(new WizardController('', ''), '');
$this->oStep = new WizStepModulesChoiceFake(new WizardController('', ''), '');
ModuleDiscovery::ResetCache();
}
@@ -36,6 +39,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
'uninstallable' => true,
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
@@ -56,6 +60,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
'uninstallable' => true,
],
'bCurrentSelected' => true,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
@@ -64,6 +69,63 @@ class WizStepModulesChoiceTest extends ItopTestCase
'checked' => true,
],
],
'A missing extension should be disabled and unchecked' => [
'aExtensionsOnDiskOrDb' => [
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'missing' => true,
'uninstallable' => true,
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => true,
'installed' => true,
'disabled' => true,
'checked' => false,
],
],
'A missing extension should always be disabled and unchecked, even when mandatory' => [
'aExtensionsOnDiskOrDb' => [
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => true,
'missing' => true,
'uninstallable' => true,
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => true,
'installed' => true,
'disabled' => true,
'checked' => false,
],
],
'A missing extension should always be disabled and unchecked, even when non-uninstallable' => [
'aExtensionsOnDiskOrDb' => [
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => true,
'missing' => true,
'uninstallable' => false,
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => false,
'missing' => true,
'installed' => true,
'disabled' => true,
'checked' => false,
],
],
'An installed but not selected extension should not be checked and be enabled' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
@@ -76,6 +138,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
'uninstallable' => true,
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
@@ -96,6 +159,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
'uninstallable' => false,
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => false,
'missing' => false,
@@ -104,6 +168,27 @@ class WizStepModulesChoiceTest extends ItopTestCase
'checked' => true,
],
],
'An installed non uninstallable extension should be enabled if the "disable uninstallation check" flag is set' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => true,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => false,
],
'bCurrentSelected' => true,
'bDisableUninstallChecks' => true,
'aExpectedFlags' => [
'uninstallable' => false,
'missing' => false,
'installed' => true,
'disabled' => false,
'checked' => true,
],
],
'A mandatory extension should be checked and disabled' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
@@ -116,6 +201,28 @@ class WizStepModulesChoiceTest extends ItopTestCase
'uninstallable' => true,
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => true,
'checked' => true,
],
],
'A mandatory extension should be checked and disabled even if the "disable uninstallation check" flag is set' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => true,
'uninstallable' => true,
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => true,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
@@ -148,6 +255,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
],
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
@@ -180,6 +288,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
],
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
@@ -212,6 +321,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
],
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
@@ -244,6 +354,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
],
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
@@ -258,10 +369,10 @@ class WizStepModulesChoiceTest extends ItopTestCase
/**
* @dataProvider ProviderComputeChoiceFlags
*/
public function testComputeChoiceFlags($aExtensionsOnDiskOrDb, $aWizardStepDefinition, $bIsCurrentSelected, $aExpectedFlags)
public function testComputeChoiceFlags($aExtensionsOnDiskOrDb, $aWizardStepDefinition, $bIsCurrentSelected, $bDisableUninstallChecks, $aExpectedFlags)
{
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsOnDiskOrDb));
$aFlags = $this->oStep->ComputeChoiceFlags($aWizardStepDefinition, '_0', $bIsCurrentSelected ? ['_0' => '_0'] : [], false, false, true);
$aFlags = $this->oStep->ComputeChoiceFlags($aWizardStepDefinition, '_0', $bIsCurrentSelected ? ['_0' => '_0'] : [], false, $bDisableUninstallChecks, true);
$this->assertEquals($aExpectedFlags, $aFlags);
}
@@ -350,4 +461,431 @@ class WizStepModulesChoiceTest extends ItopTestCase
$this->assertEquals($aExpectedRemovedList, $aRemovedList);
}
public function testGetStepInfo_PackageWithoutInstallationXML()
{
$aExtensionsOnDiskOrDb = self::GivenExtensionsOnDisk();
$oWizStepModulesChoice = $this->GivenWizStepModulesChoiceWithoutXmlInstallation($aExtensionsOnDiskOrDb);
$expected = [
'title' => 'Modules Selection',
'description' => '<h2>Select the modules to install. You can launch the installation again to install new modules, but you cannot remove already installed modules.</h2>',
'banner' => '/images/icons/icons8-apps-tab.svg',
'options' => $aExtensionsOnDiskOrDb,
];
$this->CallAndCheckTwice($oWizStepModulesChoice, null, $expected);
$this->CallAndCheckTwice($oWizStepModulesChoice, 1, null);
}
private function GivenWizStepModulesChoiceWithoutXmlInstallation(array $aExtensionsOnDiskOrDb): WizStepModulesChoiceFake
{
$oExtensionsMap = $this->createMock(iTopExtensionsMap::class);
$oExtensionsMap->expects($this->once())
->method('GetAllExtensionsOptionInfo')
->willReturn($aExtensionsOnDiskOrDb);
$oWizard = new WizardController('', '');
$oWizStepModulesChoice = new WizStepModulesChoiceFake($oWizard, '');
$oWizStepModulesChoice->setExtensionMap($oExtensionsMap);
return $oWizStepModulesChoice;
}
public static function PackageWithInstallationXMLProvider()
{
require_once __DIR__.'/../../../../approot.inc.php';
require_once APPROOT.'setup/parameters.class.inc.php';
$aUsecases = [];
$aUsecases["[no step] with extensions"] = [
'iGetStepInfoIdxArg' => null,
'expected' => self::GetStep(0),
];
for ($i = 0; $i < 4; $i++) {
$aUsecases["[step $i] with extensions"] = [
'iGetStepInfoIdxArg' => $i,
'expected' => self::GetStep($i),
];
}
$aUsecases["[step 6] with extensions => NO STEP ANYMORE"] = [
'iGetStepInfoIdxArg' => 6,
'expected' => null,
'iGetAllExtensionsOptionInfoCallCount' => 1,
];
return $aUsecases;
}
/**
* @dataProvider PackageWithInstallationXMLProvider
*/
public function testGetStepInfo_PackageWithInstallationXMLWithExtensions($iGetStepInfoIdxArg, $expected, $iGetAllExtensionsOptionInfoCallCount = 0)
{
$aExtensionsOnDiskOrDb = self::GivenExtensionsOnDisk();
$oWizStepModulesChoice = $this->GivenWizStepModulesChoiceWithXmlInstallation($aExtensionsOnDiskOrDb, $iGetAllExtensionsOptionInfoCallCount);
$this->CallAndCheckTwice($oWizStepModulesChoice, $iGetStepInfoIdxArg, $expected);
}
public function testGetStepInfo_PackageWithInstallationXML_AfterLastStepWithExtensions()
{
$expected = [
'title' => 'Extensions',
'description' => '<h2>Select additional extensions to install. You can launch the installation again to install new extensions or remove installed ones.</h2>',
'banner' => '/images/icons/icons8-puzzle.svg',
'options' => self::GivenExtensionsOnDisk(),
];
$aExtensionsOnDiskOrDb = self::GivenExtensionsOnDisk();
$oWizStepModulesChoice = $this->GivenWizStepModulesChoiceWithXmlInstallation($aExtensionsOnDiskOrDb, 1);
$this->CallAndCheckTwice($oWizStepModulesChoice, 5, $expected);
}
public function testGetStepInfo_PackageWithInstallationXMLAfterLastStepWithoutExtensions()
{
$oWizStepModulesChoice = $this->GivenWizStepModulesChoiceWithXmlInstallation([], 1);
$this->CallAndCheckTwice($oWizStepModulesChoice, 5, null);
}
public function testGetStepInfo_PackageWithInstallationXML_MakeSureNextStepIsAlsoCached()
{
$aExtensionsOnDiskOrDb = self::GivenExtensionsOnDisk();
$oWizStepModulesChoice = $this->GivenWizStepModulesChoiceWithXmlInstallation($aExtensionsOnDiskOrDb, 1);
$this->CallAndCheckTwice($oWizStepModulesChoice, 4, self::GetStep(4));
$expected = [
'title' => 'Extensions',
'description' => '<h2>Select additional extensions to install. You can launch the installation again to install new extensions or remove installed ones.</h2>',
'banner' => '/images/icons/icons8-puzzle.svg',
'options' => $aExtensionsOnDiskOrDb,
];
$this->CallAndCheckTwice($oWizStepModulesChoice, 5, $expected);
}
private static function GivenExtensionsOnDisk(): array
{
return [
'itop-ext-added1' => [
'installed' => false,
],
'itop-ext-added2' => [
'installed' => false,
],
];
}
private function GivenWizStepModulesChoiceWithXmlInstallation(array $aExtensionsOnDiskOrDb, $iGetAllExtensionsOptionInfoCallCount): WizStepModulesChoiceFake
{
$oExtensionsMap = $this->createMock(iTopExtensionsMap::class);
$oExtensionsMap->expects($this->exactly($iGetAllExtensionsOptionInfoCallCount))
->method('GetAllExtensionsOptionInfo')
->willReturn($aExtensionsOnDiskOrDb);
$oWizard = new WizardController('', '');
//needed to find installation.xml
$oWizard->SetParameter('source_dir', __DIR__.'/ressources');
$oWizStepModulesChoice = new WizStepModulesChoiceFake($oWizard, '');
$oWizStepModulesChoice->setExtensionMap($oExtensionsMap);
return $oWizStepModulesChoice;
}
private function CallAndCheckTwice($oStep, $iGetStepInfoIdxArg, $expected)
{
$aRes = $this->InvokeNonPublicMethod(WizStepModulesChoiceFake::class, 'GetStepInfo', $oStep, [$iGetStepInfoIdxArg]);
$this->assertEquals($expected, $aRes, "step:".$iGetStepInfoIdxArg);
$aRes = $this->InvokeNonPublicMethod(WizStepModulesChoiceFake::class, 'GetStepInfo', $oStep, [$iGetStepInfoIdxArg]);
$this->assertEquals($expected, $aRes, "(2nd call) step:".$iGetStepInfoIdxArg);
}
private static function GetStep($index)
{
$aParams = new XMLParameters(__DIR__.'/ressources/installation.xml');
$aSteps = $aParams->Get('steps', []);
return $aSteps[$index] ?? null;
}
public function ProviderGetSelectedModules()
{
return [
'No extension selected' => [
'aSelected' => [],
'aExpectedModules' => [],
'aExpectedExtensions' => [],
],
'One extension selected' => [
'aSelected' => ['_0' => '_0'],
'aExpectedModules' => ['combodo-sample-module' => true],
'aExpectedExtensions' => ['combodo-sample'],
],
'More extensions selected' => [
'aSelected' => ['_0' => '_0', '_1' => '_1'],
'aExpectedModules' => ['combodo-sample-module' => true, 'combodo-test-moduleA' => true, 'combodo-test-moduleB' => true],
'aExpectedExtensions' => ['combodo-sample', 'combodo-test'],
],
];
}
/**
* @dataProvider ProviderGetSelectedModules
*/
public function testGetSelectedModules($aSelectedExtensions, $aExpectedModules, $aExpectedExtensions)
{
$aExtensionsMapData = [
'combodo-sample' => [
'installed' => false,
],
'combodo-test' => [
'installed' => false,
],
];
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsMapData));
$aStepInfo = [
'title' => 'Extensions',
'description' => '',
'banner' => '',
'options' => [
[
'extension_code' => 'combodo-sample',
'title' => 'Sample extension',
'description' => '',
'more_info' => '',
'default' => true,
'modules' => [
'combodo-sample-module',
],
'mandatory' => false,
'source_label' => '',
'uninstallable' => true,
'missing' => false,
],
[
'extension_code' => 'combodo-test',
'title' => 'Test extension',
'description' => '',
'more_info' => '',
'default' => true,
'modules' => [
'combodo-test-moduleA',
'combodo-test-moduleB',
],
'mandatory' => false,
'source_label' => '',
'uninstallable' => true,
'missing' => false,
],
],
];
$aModules = [];
$aExtensions = [];
$this->oStep->GetSelectedModules($aStepInfo, $aSelectedExtensions, $aModules, '', '', $aExtensions);
$this->assertEquals($aExpectedModules, $aModules);
$this->assertEquals($aExpectedExtensions, $aExtensions);
}
public function testGetSelectedModulesShouldAlwaysSelectMandatoryExtension()
{
$aSelectedExtensions = ['_0' => '_0'];
$aExtensionsMapData = [
'combodo-sample' => [
'installed' => true,
],
];
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsMapData));
$aStepInfo = [
'title' => 'Extensions',
'description' => '',
'banner' => '',
'options' => [
[
'extension_code' => 'combodo-sample',
'title' => 'Sample extension',
'description' => '',
'more_info' => '',
'default' => true,
'modules' => [
'combodo-sample-module',
],
'mandatory' => false,
'source_label' => '',
'uninstallable' => true,
'missing' => true,
],
],
];
$aExpectedModules = ['combodo-sample-module' => true];
$aExpectedExtensions = ['combodo-sample'];
$aModules = [];
$aExtensions = [];
$this->oStep->GetSelectedModules($aStepInfo, $aSelectedExtensions, $aModules, '', '', $aExtensions);
$this->assertEquals($aExpectedModules, $aModules);
$this->assertEquals($aExpectedExtensions, $aExtensions);
}
public function testGetSelectedModulesShouldShouldParseAutoSelectCondition()
{
//the 'auto_select' parameter, contrary to its name, deselect the module if its result is false
$aSelectedExtensions = ['_0' => '_0'];
$aExtensionsMapData = [
'combodo-sample' => [
'installed' => true,
'module_info' => [
'combodo-sample-module' => [
'auto_select' => 'true && false',
],
],
],
];
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsMapData));
$aStepInfo = [
'title' => 'Extensions',
'description' => '',
'banner' => '',
'options' => [
[
'extension_code' => 'combodo-sample',
'title' => 'Sample extension',
'description' => '',
'more_info' => '',
'default' => true,
'modules' => [
'combodo-sample-module',
],
'mandatory' => false,
'source_label' => '',
'uninstallable' => true,
'missing' => true,
],
],
];
$aExpectedModules = [];
$aExpectedExtensions = ['combodo-sample'];
$aModules = [];
$aExtensions = [];
$this->oStep->GetSelectedModules($aStepInfo, $aSelectedExtensions, $aModules, '', '', $aExtensions);
$this->assertEquals($aExpectedModules, $aModules);
$this->assertEquals($aExpectedExtensions, $aExtensions);
}
public function testGetSelectedModulesWithSubOptions()
{
$aSelectedExtensions = ['_0' => '_0', '_0_0' => '_0_0'];
$aExtensionsMapData = [
'combodo-sample' => [
'installed' => false,
],
'combodo-sub-sample' => [
'installed' => false,
],
];
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsMapData));
$aStepInfo = [
'options' => [
[
'extension_code' => 'combodo-sample',
'title' => 'Sample extension',
'description' => '',
'more_info' => '',
'default' => true,
'modules' => [
'combodo-sample-module',
],
'mandatory' => false,
'source_label' => '',
'uninstallable' => true,
'missing' => false,
'sub_options' => [
'options' => [
[
'extension_code' => 'combodo-sub-sample',
'title' => 'Sample sub extension',
'description' => '',
'more_info' => '',
'default' => true,
'modules' => [
'combodo-sub-sample-module',
],
'mandatory' => false,
'source_label' => '',
'uninstallable' => true,
'missing' => false,
],
],
],
],
],
];
$aExpectedModules = ['combodo-sample-module' => true, 'combodo-sub-sample-module' => true];
$aExpectedExtensions = ['combodo-sample', 'combodo-sub-sample'];
$aModules = [];
$aExtensions = [];
$this->oStep->GetSelectedModules($aStepInfo, $aSelectedExtensions, $aModules, '', '', $aExtensions);
$this->assertEquals($aExpectedModules, $aModules);
$this->assertEquals($aExpectedExtensions, $aExtensions);
}
public function testGetSelectedModulesShouldThrowAnExceptionWhenAnySelectedExtensionDoesNotHaveAnyAssociatedModules()
{
$aExtensionsMapData = [
'combodo-sample' => [
'installed' => false,
],
];
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsMapData));
//GetSelectedModules
$aStepInfo = [
'title' => 'Extensions',
'description' => '',
'banner' => '',
'options' => [
[
'extension_code' => 'combodo-sample',
'title' => 'Sample extension',
'description' => '',
'more_info' => '',
'default' => true,
'modules' => [],
'mandatory' => false,
'source_label' => '',
'uninstallable' => true,
'missing' => false,
],
],
];
$aModules = [];
$aExtensions = [];
$this->expectException('Exception');
$this->expectExceptionMessage('Extension combodo-sample does not have any module associated');
$this->oStep->GetSelectedModules($aStepInfo, ['_0' => '_0'], $aModules, '', '', $aExtensions);
}
}

View File

@@ -0,0 +1,69 @@
<?php
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class iTopExtensionTest extends ItopTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('/setup/unattended-install/InstallationFileService.php');
ModuleDiscovery::ResetCache();
}
public function testCanBeUninstalledDefaultValueIsTrue()
{
$oExtension = new iTopExtension();
$this->assertTrue($oExtension->CanBeUninstalled(), 'An extension should be uninstallable by default.');
}
public function testCanBeUninstalledReturnTrueWhenAllModulesCanBeUninstalled()
{
$oExtension = new iTopExtension();
$oExtension->aModuleInfo['combodo-test-yes1'] = [
'uninstallable' => 'yes',
];
$oExtension->aModuleInfo['combodo-test-yes2'] = [
'uninstallable' => 'yes',
];
$this->assertTrue($oExtension->CanBeUninstalled(), 'An extension should be considered uninstallable if all of its modules are uninstallable.');
}
public function testCanBeUninstalledReturnFalseWhenAtLeastOneModuleCannotBeUninstalled()
{
$oExtension = new iTopExtension();
$oExtension->aModuleInfo['combodo-test-yes'] = [
'uninstallable' => 'yes',
];
$oExtension->aModuleInfo['combodo-test-no'] = [
'uninstallable' => 'no',
];
$this->assertFalse($oExtension->CanBeUninstalled(), 'An extension should be considered non-uninstallable if at least one of its modules is not uninstallable.');
}
public function testCanBeUninstalledAnyValueDifferentThanYesIsConsideredFalse()
{
$oExtension = new iTopExtension();
$oExtension->aModuleInfo['combodo-test-maybe'] = [
'uninstallable' => 'maybe',
];
$this->assertFalse($oExtension->CanBeUninstalled(), 'Any value in the uninstallable flag different than yes should be considered false.');
}
public function testCanBeUninstalledExtensionValueOverwriteModulesValue()
{
$oExtension = new iTopExtension();
$oExtension->bCanBeUninstalled = true;
$oExtension->aModuleInfo['combodo-test-no'] = [
'uninstallable' => 'no',
];
$this->assertTrue($oExtension->CanBeUninstalled(), 'The uninstallable flag provided in the extension should prevail over those defined in the modules.');
$oExtension = new iTopExtension();
$oExtension->bCanBeUninstalled = false;
$oExtension->aModuleInfo['combodo-test-yes'] = [
'uninstallable' => 'yes',
];
$this->assertFalse($oExtension->CanBeUninstalled(), 'The uninstallable flag provided in the extension should prevail over those defined in the modules.');
}
}

View File

@@ -21,7 +21,7 @@ class iTopExtensionsMapFake extends iTopExtensionsMap
$oExtension->aModules = $aExtension['modules'] ?? [];
$oExtension->bCanBeUninstalled = $aExtension['uninstallable'] ?? null;
$oExtension->sVersion = $aExtension['version'] ?? '1.0.0';
$oExtension->aModuleInfo = [];
$oExtension->aModuleInfo = $aExtension['module_info'] ?? [];
$oMap->AddExtension($oExtension);
}
return $oMap;

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