Compare commits

..

97 Commits

Author SHA1 Message Date
Timothee
aedc81fb90 N°9010 Refactor ComputeChoiceFlags 2026-02-13 15:12:11 +01:00
Timothee
c3cc07316e N°9010 Add explanation 2026-02-03 11:54:30 +01:00
Timothee
06b0cad74f N°9010 Fix flags for missing extensions 2026-02-03 11:50:06 +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
af2c3b02bc N°8764 - skip audit when itop reinstalled on top of a backup without env-production to read previous datamodel + phpdoc fix 2026-01-14 10:13:10 +01:00
odain
cfc933b92b Merge branch 'feature/8981-prepare' into feature/uninstallation 2026-01-13 16:33:10 +01:00
odain
0582ae1038 setup: phpstan level 2
setup: phpstan level 2
2026-01-13 16:31:46 +01:00
odain
13c18b611c setup: phpstan level 1 2026-01-13 16:29:44 +01:00
odain
8d2e0761e0 setup: phpstan level 0 2026-01-13 16:29:44 +01:00
odain
1e15eb1161 N°8764 halt setup wizard at data issue - 2nd review 2026-01-13 16:29:44 +01:00
odain
7df59427cb N°8764 halt setup wizard at data issue - review
- 2 types of SetupAudit constructors
- setup wizard new step management enhancement
- change SetupAudit GetIssue API behaviour
2026-01-12 15:55:35 +01:00
odain
d647d92acf ci: test enhancement => use GetTemporaryFilePath 2026-01-12 15:55:35 +01:00
odain
693e40b9c7 enhance test sdk: add GetTemporaryFilePath 2026-01-12 15:55:35 +01:00
odain
c3c2135ecc N°7629: log changed 2026-01-12 15:55:35 +01:00
odain
63e473e6d0 N°8981: test ModuleDiscovery filtered by removed extensions 2026-01-12 15:55:35 +01:00
odain
3a3f5736c0 N°8764 - fix first install 2026-01-12 15:55:35 +01:00
odain
0996e8fae0 N°8764 - stop setup and display data to cleanup message 2026-01-12 15:55:35 +01:00
odain
43bd621731 N°7629: improve InterfaceDiscovery::GetCandidateClasses robustness 2026-01-12 15:55:35 +01:00
odain
d1e91087b9 N°8764 - Halt setup if database is not compatible with an uninstallation 2026-01-12 15:55:35 +01:00
odain
73cb58a131 N°9085 - Cannot install itop without a portal 2026-01-12 15:52:55 +01:00
odain
d1fde89129 N°8981: fix broken setup wizard due to invalid array passed 2026-01-08 07:55:54 +01:00
odain
1f4b96798a Merge branch 'feature/8981-prepare' into feature/uninstallation 2026-01-07 20:39:46 +01:00
odain
9768ffb19d N°8981: ModuleInstallationRepository dedicated to module installation queries
N°8981: ModuleInstallationRepository dedicated to module installation queries

fix renaming
2026-01-07 20:15:40 +01:00
odain
e55bbf728b N°8981: review cleanup on module filtering due to extensoin removal 2026-01-07 19:48:34 +01:00
odain
5f2604c610 N°8981: be able to remove extension during setup even when present on disk 2026-01-07 19:37:05 +01:00
odain
57b3610100 N°8981 setup wizard: reuse AnalyzeInstallation result in wizard module choice step 2026-01-07 19:37:05 +01:00
odain
9f3b8ec964 N°8981 setup wizard: optimize IsConnectableToITopHub use (perf)
N°8981: revert IsConnectableToITopHub
2026-01-07 19:37:00 +01:00
odain
193c980057 N°8981 setup wizard: static removal + cleanup 2026-01-07 10:33:11 +01:00
odain
fdfe9224c3 N°8981 : add type in function 2026-01-07 10:33:11 +01:00
odain
774fe22ece N°8981 setup wizard: cache GetParamForConfigArray 2026-01-07 10:33:11 +01:00
odain
27b0f64328 N°8981 setup wizard: cleanup main actions + remove static when possible 2026-01-07 10:33:11 +01:00
odain
8ad4670a2f N°8981 setup wizard: cleanup config use in setup wizard 2026-01-07 10:33:11 +01:00
odain
f2e682c07c test SDK enhancement: be able to compare moduleinstallation
ci: add log and hopefully fix hub test

ci: fix all test that rely on CustomDataTC
2026-01-07 10:33:11 +01:00
odain
83973d102f N°8981: repair previous setup cleanup (broken setups) 2026-01-07 10:33:11 +01:00
odain
85e28931f5 N°8981: prepare hub connector test cover
sdk test enhancement : add call itop api

fix ci

ci: fix broken tests

ci: cover hub setup on compile and launch steps

code style

ci: fix ModuleDiscoveryTest redundant class + add logs to investigate ci setup issues

ci: fix log during setup tests
2026-01-07 10:33:11 +01:00
odain
d33ca81198 N°4789 - do not call empty ModuleInstallerAPI class + logs
code style
2026-01-07 10:33:11 +01:00
odain
1ef4462517 N°8981 - cleanup/simplify setup
fix RecordInstallation due to previous refactoring
2026-01-07 10:33:10 +01:00
odain
a2b0ad6c11 ci: fix broken LoginTest 2026-01-07 10:33:10 +01:00
Timothee
7844c9ffd0 N°8864 Passing array instead of html 2026-01-06 14:29:23 +01:00
Timothee
1c45a4c49e N°9010 Improve tests readability 2026-01-06 08:17:31 +01:00
Timmy38
8f47ca00a7 N°8864 list extensions installation in setup recap 2026-01-06 08:09:22 +01:00
Timothee
f9a1b444ab N°9010 Setup wizard : manage multiple level extension choice 2025-12-23 16:05:23 +01:00
odain
3b38dac8c6 Merge branch 'develop' into feature/uninstallation 2025-12-19 18:45:34 +01:00
odain
3d46fe6ef1 Merge branch 'support/3.2' into develop 2025-12-19 18:44:55 +01:00
odain
4dba47798c ci: fix broken test due to POST CURL file not sent 2025-12-19 18:35:42 +01:00
Anne-Cath
9480c4053d N°8786 - configuration: allow conditions on the allowed_login_types field - fix tests 2025-12-19 16:37:47 +01:00
odain
b967fb7f20 Merge branch 'develop' into feature/uninstallation 2025-12-19 16:17:12 +01:00
odain
9ea197148c Merge branch 'support/3.2' into develop 2025-12-19 16:17:00 +01:00
odain
49a7f3118d ci: fix Array to String Conversion in CallUrl with POST array of array 2025-12-19 16:12:53 +01:00
odain
195038c941 Merge branch 'develop' into feature/uninstallation 2025-12-19 15:48:56 +01:00
odain
bf80b5dca2 Merge branch 'support/3.2' into develop 2025-12-19 15:46:00 +01:00
odain
09afcb229c ci: fix callUrl with posted params 2025-12-19 11:11:03 +01:00
odain
92f843f676 Merge branch 'support/3.2' into develop 2025-12-19 10:39:24 +01:00
odain
3df4ddc696 ci: fix code style 2025-12-19 10:39:15 +01:00
odain
d552727c55 ci: fix code style 2025-12-19 10:38:48 +01:00
odain
1fe401c102 ci: rename CallItopUrl by CallUrl + cleanup 2025-12-19 10:01:37 +01:00
odain
a4b0b6e855 ci: rename CallItopUrl by CallUrl + cleanup 2025-12-19 09:54:43 +01:00
odain
9a8d87e2b5 Merge branch 'develop' into feature/uninstallation 2025-12-18 14:02:45 +01:00
odain
545028d68a Merge branch 'support/3.2' into develop 2025-12-18 13:51:54 +01:00
odain-cbd
6cb08ba361 Add few APIs to ease tests (#788)
* add few APIs to ease tests

* code style

* log testname via IssueLog only through ItopDataTestCase

* code style

* ci: phpunit fix fatal error
2025-12-18 13:31:48 +01:00
Molkobain
b3cd79605d Merge remote-tracking branch 'refs/remotes/origin/support/3.2' into develop 2025-12-17 22:22:36 +01:00
Molkobain
f9db405343 N°6644 - PHP Static Analysis: Update PHPStan to v2.1 2025-12-17 22:21:43 +01:00
Molkobain
4f1f144c51 N°6644 - PHP Static Analysis: Update configuration to:
- Ignore compiled folders other than "env-production"
 - Ignore Lempar.php as its content isn't valid PHP and it won't be included in the baseline
2025-12-17 22:04:49 +01:00
Molkobain
ef8ade5d20 📝 Update unit tests documentation 2025-12-17 18:36:18 +01:00
v-dumas
cef4a52081 N°8950 - Fix color of implementation status 2025-12-17 12:21:54 +01:00
v-dumas
dc9fb2d693 N°8950 - Add obsolescence rule on Contract, Service and ServiceSubcategory 2025-12-17 12:21:54 +01:00
v-dumas
e9ffbe5b09 N°8950 - Add colors on 'status' on non-CI classes 2025-12-17 12:21:54 +01:00
v-dumas
9c792a601f N°8951 - Add colors on 'status' field on CIs 2025-12-17 12:21:54 +01:00
Molkobain
cda6c1dfa8 Merge remote-tracking branch 'refs/remotes/origin/support/3.2' into develop 2025-12-17 11:09:22 +01:00
Molkobain
0b242d872a N°6644 - Tests: Restore proper CI branch, unit tests run and fix a typo 2025-12-17 11:06:36 +01:00
Molkobain
c144c80663 Merge remote-tracking branch 'refs/remotes/origin/support/3.2' into develop 2025-12-17 10:56:31 +01:00
Molkobain
d9261b8342 N°6644 - Tests: Add static analysis for PHP (#536) 2025-12-17 10:45:53 +01:00
odain
385302c44c ci: fix nightly 2025-12-08 10:51:28 +01:00
odain
b563f113d0 add combodo-data-feature-removal 2025-12-08 10:38:30 +01:00
odain
715d9d3b1c less logs during setup 2025-12-05 17:37:09 +01:00
odain
7a6c2bc6a4 ci: fix with package 2025-12-05 17:36:57 +01:00
odain
8919184ef9 ci: adapt test to run also with itop packages 2025-12-05 11:36:19 +01:00
odain
7bfaebe4db ci: fix merge and issues with LoadChoicesFromDatabase 2025-12-04 18:40:21 +01:00
odain
8e10cf9b72 N°8760 : fix broken setup
ci: fix merge
2025-12-04 18:28:47 +01:00
odain
9f3d7d2c36 N°8760: revert dry removal audit and use real file deletion again
code formatting
2025-12-04 17:19:18 +01:00
odain
ae980e365d N°8760 :fix ModuleInstallation db query + refactor query in ModuleInstallationService
extensionmap cleanup

fix setup broken
2025-12-04 17:19:18 +01:00
odain
a2b01b3ed4 N°8760 - setupaudit and dry removal API review with Romain - avoid file deletion
N°8760 - be able to simulate extension removal by oerriding GetExtensionMap

be able to simulate SetupAudit errors in Setups for integration tests

fix rebase
2025-12-04 17:19:18 +01:00
odain
9cdc707bc5 N°8760 - Audit uninstall of extensions that declare classes - first prototype
N°8760 - Audit uninstall of extensions that declare classes - be able to trace DM classes created_in

N°8760 - be able to test with additional extensions installed in test SDK

N°8760 - provide a service dedicated to extension removal

N°8760 - compute all rules by default

add comment

adapt audit to both extension and mtp
2025-12-04 17:19:18 +01:00
odain
76178c16b8 N°8760: code legacy refactoring around module computation (AnalyzeInstallation)
AnalyzeInstallation refactoring

fix ci call to ModuleInstallationService

ci: fix code style

get rid of itop_version

refactoring: make code simpler

get rid of unused name_db

refactoring setup: rename version_db by installed_version

refactoring setup: rename version_code by available_version

code cleanup: avoid useless runtimeenv instanciation
2025-12-04 17:19:18 +01:00
106 changed files with 16835 additions and 2099 deletions

View File

@@ -2418,6 +2418,7 @@ class Config
public function SetAllowedLoginTypes($aAllowedLoginTypes)
{
$this->m_sAllowedLoginTypes = implode('|', $aAllowedLoginTypes);
$this->Set('allowed_login_types', implode('|', $aAllowedLoginTypes));
}
/**
@@ -2684,14 +2685,13 @@ class Config
*
* @param array $aParamValues
* @param ?string $sModulesDir
* @param bool $bPreserveModuleSettings
*
* @return void The current object is modified directly
*
* @throws \Exception
* @throws \CoreException
*/
public function UpdateFromParams($aParamValues, $sModulesDir = null, $bPreserveModuleSettings = false)
public function UpdateFromParams($aParamValues, $sModulesDir = null)
{
if (isset($aParamValues['application_path'])) {
$this->Set('app_root_url', $aParamValues['application_path']);
@@ -2739,7 +2739,10 @@ class Config
} else {
$aSelectedModules = null;
}
$this->UpdateIncludes($sModulesDir, $aSelectedModules);
if (! is_null($sModulesDir)) {
$this->UpdateIncludes($sModulesDir, $aSelectedModules);
}
if (isset($aParamValues['source_dir'])) {
$this->Set('source_dir', $aParamValues['source_dir']);
@@ -2757,12 +2760,8 @@ class Config
*
* @throws Exception
*/
public function UpdateIncludes($sModulesDir, $aSelectedModules = null)
public function UpdateIncludes(string $sModulesDir, $aSelectedModules = null)
{
if ($sModulesDir === null) {
return;
}
// Initialize the arrays below with default values for the application...
$oEmptyConfig = new Config('dummy_file', false); // Do NOT load any config file, just set the default values
$aAddOns = $oEmptyConfig->GetAddOns();

View File

@@ -462,6 +462,19 @@ abstract class MetaModel
return call_user_func([$sClass, 'GetClassDescription'], $sClass);
}
/**
* @param string $sClass
*
* @return string
* @throws \CoreException
*/
final public static function GetCreatedIn($sClass)
{
self::_check_subclass($sClass);
return self::$m_aClassParams[$sClass]["created_in"] ?? "";
}
/**
* @param string $sClass
*
@@ -3145,6 +3158,7 @@ abstract class MetaModel
$aMandatParams = [
"category" => "group classes by modules defining their visibility in the UI",
"key_type" => "autoincrement | string",
//"created_in" => "module_name where class is defined",
"name_attcode" => "define which attribute is the class name, may be an array of attributes (format specified in the dictionary as 'Class:myclass/Name' => '%1\$s %2\$s...'",
"state_attcode" => "define which attribute is representing the state (object lifecycle)",
"reconc_keys" => "define the attributes that will 'almost uniquely' identify an object in batch processes",

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;

View File

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

View File

@@ -323,18 +323,38 @@
<value id="production">
<code>production</code>
<rank>30</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="implementation">
<code>implementation</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="stock">
<code>stock</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-neutral-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-neutral-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="obsolete">
<code>obsolete</code>
<rank>40</rank>
<style>
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sql>status</sql>
@@ -6897,14 +6917,29 @@
<value id="production">
<code>production</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="implementation">
<code>implementation</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="obsolete">
<code>obsolete</code>
<rank>30</rank>
<style>
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sql>status</sql>

View File

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

View File

@@ -135,7 +135,7 @@ class RunTimeEnvironmentCoreUpdater extends RunTimeEnvironment
$aAvailableModules[$oModule->GetName()] = $oModule;
}
// TODO check the auto-selected modules here
foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) {
foreach ($this->GetExtensionMap()->GetAllExtensions() as $oExtension) {
if ($oExtension->bMarkedAsChosen) {
foreach ($oExtension->aModules as $sModuleName) {
if (!isset($aRet[$sModuleName]) && isset($aAvailableModules[$sModuleName])) {

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Set>
<DataFlowType alias="DataFlowType" id="1">
<name>http</name>
</DataFlowType>
<DataFlowType alias="DataFlowType" id="2">
<name>https</name>
</DataFlowType>
<DataFlowType alias="DataFlowType" id="3">
<name>ftp</name>
</DataFlowType>
<DataFlowType alias="DataFlowType" id="4">
<name>sftp</name>
</DataFlowType>
<DataFlowType alias="DataFlowType" id="5">
<name>AS2</name>
</DataFlowType>
<DataFlowType alias="DataFlowType" id="6">
<name>X.400</name>
</DataFlowType>
</Set>

View File

@@ -1,483 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
<constants>
</constants>
<classes>
<class id="DataFlow" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dataflow</db_table>
<style>
<icon>images/icons8-sorting-arrows-horizontal.svg</icon>
</style>
<naming>
<attributes>
<attribute id="name"/>
<attribute id="source_id_friendlyname"/>
<attribute id="destination_id_friendlyname"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name"/>
<attribute id="destination_id"/>
<attribute id="org_id"/>
<attribute id="source_id"/>
<attribute id="flowtype_id"/>
</attributes>
</reconciliation>
<obsolescence>
<condition>status='inactive'</condition>
</obsolescence>
<fields_semantic>
<state_attribute>status</state_attribute>
</fields_semantic>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
<field id="org_id" xsi:type="AttributeExternalKey">
<sql>org_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>Organization</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<tracking_level>all</tracking_level>
</field>
<field id="source_id" xsi:type="AttributeExternalKey">
<sql>source_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>SoftwareInstance</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<tracking_level>all</tracking_level>
</field>
<field id="destination_id" xsi:type="AttributeExternalKey">
<sql>destination_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>SoftwareInstance</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<tracking_level>all</tracking_level>
</field>
<field id="flowtype_id" xsi:type="AttributeExternalKey">
<sql>flowtype_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>true</is_null_allowed>
<target_class>FlowType</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<tracking_level>all</tracking_level>
</field>
<field id="description" xsi:type="AttributeText">
<sql>description</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="status" xsi:type="AttributeEnum">
<sql>status</sql>
<values>
<value id="active">
<code>active</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="inactive">
<code>inactive</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sort_type>label</sort_type>
<default_value>active</default_value>
<is_null_allowed>false</is_null_allowed>
<display_style>list</display_style>
<tracking_level>all</tracking_level>
</field>
<field id="business_criticity" xsi:type="AttributeEnum">
<sort_type>rank</sort_type>
<values>
<value id="high">
<code>high</code>
<rank>10</rank>
</value>
<value id="medium">
<code>medium</code>
<rank>20</rank>
</value>
<value id="low">
<code>low</code>
<rank>30</rank>
</value>
</values>
<sql>business_criticity</sql>
<default_value>low</default_value>
<is_null_allowed>false</is_null_allowed>
<display_style>list</display_style>
</field>
<field id="execution_frequency" xsi:type="AttributeEnum">
<sort_type>rank</sort_type>
<values>
<value id="real_time">
<code>real_time</code>
<rank>10</rank>
</value>
<value id="hourly">
<code>hourly</code>
<rank>20</rank>
</value>
<value id="daily">
<code>daily</code>
<rank>30</rank>
</value>
<value id="weekly">
<code>weekly</code>
<rank>40</rank>
</value>
<value id="monthly">
<code>monthly</code>
<rank>50</rank>
</value>
<value id="yearly">
<code>yearly</code>
<rank>60</rank>
</value>
</values>
<sql>execution_frequency</sql>
<default_value>daily</default_value>
<is_null_allowed>false</is_null_allowed>
<display_style>list</display_style>
</field>
<field id="documents_list" xsi:type="AttributeLinkedSetIndirect">
<linked_class>lnkDocumentToFunctionalCI</linked_class>
<ext_key_to_me>functionalci_id</ext_key_to_me>
<count_min>0</count_min>
<count_max>0</count_max>
<ext_key_to_remote>document_id</ext_key_to_remote>
<duplicates/>
</field>
</fields>
<methods/>
<presentation>
<list>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="source_id">
<rank>20</rank>
</item>
<item id="destination_id">
<rank>30</rank>
</item>
<item id="flowtype_id">
<rank>40</rank>
</item>
<item id="business_criticity">
<rank>50</rank>
</item>
</items>
</list>
<search>
<items>
<item id="org_id">
<rank>10</rank>
</item>
<item id="source_id">
<rank>20</rank>
</item>
<item id="destination_id">
<rank>30</rank>
</item>
<item id="status">
<rank>40</rank>
</item>
</items>
</search>
<details>
<items>
<item id="col:col1">
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="source_id">
<rank>20</rank>
</item>
<item id="destination_id">
<rank>30</rank>
</item>
<item id="org_id">
<rank>40</rank>
</item>
<item id="status">
<rank>50</rank>
</item>
<item id="flowtype_id">
<rank>60</rank>
</item>
<item id="execution_frequency">
<rank>70</rank>
</item>
<item id="business_criticity">
<rank>80</rank>
</item>
</items>
<rank>10</rank>
</item>
<item id="col:col2">
<items>
<item id="description">
<rank>10</rank>
</item>
</items>
<rank>20</rank>
</item>
<item id="documents_list">
<rank>80</rank>
</item>
</items>
</details>
<default_search>
<items>
<item id="org_id">
<rank>10</rank>
</item>
<item id="source_id">
<rank>20</rank>
</item>
<item id="destination_id">
<rank>30</rank>
</item>
<item id="flowtype_id">
<rank>40</rank>
</item>
<item id="status">
<rank>50</rank>
</item>
</items>
</default_search>
<summary>
<items>
<item id="org_id">
<rank>10</rank>
</item>
<item id="description">
<rank>20</rank>
</item>
</items>
</summary>
</presentation>
<relations>
<relation id="flow">
<neighbours>
<neighbour id="destination">
<attribute>destination_id</attribute>
<direction>both</direction>
</neighbour>
</neighbours>
</relation>
</relations>
</class>
<class id="lnkDocumentToDataFlow" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<is_link>1</is_link>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>lnkdocumenttodataflow</db_table>
<db_key_field>id</db_key_field>
<db_final_class_field/>
<naming>
<attributes>
<attribute id="document_id_friendlyname"/>
<attribute id="dataflow_id_friendlyname"/>
</attributes>
</naming>
<style>
<icon/>
</style>
<reconciliation>
<attributes>
<attribute id="dataflow_id"/>
<attribute id="document_id"/>
</attributes>
</reconciliation>
<uniqueness_rules>
<rule id="no_duplicate">
<attributes>
<attribute id="document_id"/>
<attribute id="dataflow_id"/>
</attributes>
<filter><![CDATA[]]></filter>
<disabled>false</disabled>
<is_blocking>true</is_blocking>
</rule>
</uniqueness_rules>
</properties>
<fields>
<field id="dataflow_id" xsi:type="AttributeExternalKey">
<sql>dataflow_id</sql>
<target_class>DataFlow</target_class>
<is_null_allowed>false</is_null_allowed>
<on_target_delete>DEL_AUTO</on_target_delete>
</field>
<field id="document_id" xsi:type="AttributeExternalKey">
<sql>document_id</sql>
<target_class>Document</target_class>
<is_null_allowed>false</is_null_allowed>
<on_target_delete>DEL_AUTO</on_target_delete>
</field>
</fields>
<methods/>
<presentation>
<details>
<items>
<item id="document_id">
<rank>10</rank>
</item>
<item id="dataflow_id">
<rank>20</rank>
</item>
</items>
</details>
<search>
<items>
<item id="dataflow_id">
<rank>10</rank>
</item>
<item id="document_id">
<rank>20</rank>
</item>
</items>
</search>
<list>
<items>
<item id="dataflow_id">
<rank>10</rank>
</item>
<item id="document_id">
<rank>20</rank>
</item>
</items>
</list>
</presentation>
</class>
<class id="DataFlowType" _delta="define">
<parent>Typology</parent>
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dataflowtype</db_table>
<naming>
<attributes>
<attribute id="name"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name"/>
<attribute id="finalclass"/>
</attributes>
</reconciliation>
</properties>
<fields/>
<methods/>
<presentation>
<list>
<items>
<item id="finalclass">
<rank>10</rank>
</item>
</items>
</list>
<search>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</details>
</presentation>
</class>
<class id="SoftwareInstance" _delta="must_exist">
<relations>
<relation id="flow" _delta="define">
<neighbours>
<neighbour id="flow">
<query_down><![CDATA[SELECT DataFlow WHERE source_id = :this->id]]></query_down>
<query_up><![CDATA[SELECT SoftwareInstance WHERE id = :this->source_id]]></query_up>
<direction>both</direction>
</neighbour>
</neighbours>
</relation>
</relations>
</class>
<class id="Group" _delta="must_exist">
<relations>
<relation id="flow" _delta="define">
<neighbours>
<neighbour id="flow">
<query_down><![CDATA[SELECT DataFlow AS F JOIN SoftwareInstance AS SI ON F.source_id = SI.id JOIN lnkFunctionalCIToGroup AS G ON G.functionalci_id = SI.id WHERE G.group_id=:this->id ]]></query_down>
<query_up></query_up>
<direction>down</direction>
</neighbour>
</neighbours>
</relation>
</relations>
</class>
</classes>
<menus>
<menu id="ConfigManagementOverview" xsi:type="DashboardMenuNode" _delta="must_exist">
<definition>
<cells>
<cell id="3" delta="must_exist">
<dashlets>
<dashlet id="DataFlow_20" xsi:type="DashletBadge" _delta="define">
<rank>20</rank>
<class>DataFlow</class>
</dashlet>
</dashlets>
</cell>
</cells>
</definition>
</menu>
</menus>
<user_rights>
<groups>
<group id="Configuration">
<classes>
<class id="DataFlow" _delta="define"/>
<class id="DataFlowType" _delta="define"/>
</classes>
</group>
</groups>
<profiles>
</profiles>
</user_rights>
</itop_design>

View File

@@ -1,72 +0,0 @@
<?php
/**
* Module combodo-flow-map
*
* @copyright Copyright (C) 2013 XXXXX
* @license http://opensource.org/licenses/AGPL-3.0
*/
Dict::Add('EN US', 'English', 'English', [
'Relation:flow/Description' => 'Flow maps',
'Relation:flow/DownStream' => 'Sent flow...',
'Relation:flow/UpStream' => 'Received flow...',
'Class:DataFlow' => 'Flow',
'Class:DataFlow+' => 'For application flow for example',
'Class:DataFlow/Name' => '%1$s from %2$s to %3$s',
'Class:DataFlow/Attribute:name' => 'Name',
'Class:DataFlow/Attribute:name_id+' => 'Data that are transferred',
'Class:DataFlow/Attribute:source_id' => 'Source',
'Class:DataFlow/Attribute:source_id+' => 'Source Ci of the flow',
'Class:DataFlow/Attribute:destination_id' => 'Destination',
'Class:DataFlow/Attribute:destination_id+' => 'Destination Ci for the flow',
'Class:DataFlow/Attribute:type_id' => 'Flow type',
'Class:DataFlow/Attribute:type_id+' => 'Typology of Flow.',
'Class:DataFlow/Attribute:description' => 'Description',
'Class:DataFlow/Attribute:description+' => '',
'Class:DataFlow/Attribute:status' => 'Status',
'Class:DataFlow/Attribute:status+' => '',
'Class:DataFlow/Attribute:status/Value:active' => 'active',
'Class:DataFlow/Attribute:status/Value:inactive' => 'inactive',
'Class:DataFlow/Attribute:org_id' => 'Organization',
'Class:DataFlow/Attribute:org_id+' => '',
'Class:DataFlow/Attribute:business_criticity' => 'Business criticality',
'Class:DataFlow/Attribute:business_criticity+' => '',
'Class:DataFlow/Attribute:business_criticity/Value:high' => 'high',
'Class:DataFlow/Attribute:business_criticity/Value:high+' => '',
'Class:DataFlow/Attribute:business_criticity/Value:low' => 'low',
'Class:DataFlow/Attribute:business_criticity/Value:low+' => '',
'Class:DataFlow/Attribute:business_criticity/Value:medium' => 'medium',
'Class:DataFlow/Attribute:business_criticity/Value:medium+' => '',
'Class:DataFlow/Attribute:execution_frequency' => 'Execution frequency',
'Class:DataFlow/Attribute:execution_frequency+' => 'How often the data flow is executed',
'Class:DataFlow/Attribute:execution_frequency/Value:real_time' => 'real-time',
'Class:DataFlow/Attribute:execution_frequency/Value:real_time+' => '',
'Class:DataFlow/Attribute:execution_frequency/Value:hourly' => 'hourly',
'Class:DataFlow/Attribute:execution_frequency/Value:hourly+' => '',
'Class:DataFlow/Attribute:execution_frequency/Value:daily' => 'daily',
'Class:DataFlow/Attribute:execution_frequency/Value:daily+' => '',
'Class:DataFlow/Attribute:execution_frequency/Value:weekly' => 'weekly',
'Class:DataFlow/Attribute:execution_frequency/Value:weekly+' => '',
'Class:DataFlow/Attribute:execution_frequency/Value:monthly' => 'monthly',
'Class:DataFlow/Attribute:execution_frequency/Value:monthly+' => '',
'Class:DataFlow/Attribute:execution_frequency/Value:yearly' => 'yearly',
'Class:DataFlow/Attribute:execution_frequency/Value:yearly+' => '',
/*
'Class:DataFlow/Attribute:source_id_friendlyname' => 'source_id_friendlyname',
'Class:DataFlow/Attribute:source_id_friendlyname+' => 'Full name',
'Class:DataFlow/Attribute:source_id_finalclass_recall' => 'source_id->CI sub-class',
'Class:DataFlow/Attribute:source_id_finalclass_recall+' => 'Name of the final class',
'Class:DataFlow/Attribute:source_id_obsolescence_flag' => 'source_id->Obsolete',
'Class:DataFlow/Attribute:source_id_obsolescence_flag+' => 'Computed dynamically on other attributes',
'Class:DataFlow/Attribute:destination_id_friendlyname' => 'destination_id_friendlyname',
'Class:DataFlow/Attribute:destination_id_friendlyname+' => 'Full name',
'Class:DataFlow/Attribute:destination_id_finalclass_recall' => 'destination_id->CI sub-class',
'Class:DataFlow/Attribute:destination_id_finalclass_recall+' => 'Name of the final class',
'Class:DataFlow/Attribute:destination_id_obsolescence_flag' => 'destination_id->Obsolete',
'Class:DataFlow/Attribute:destination_id_obsolescence_flag+' => 'Computed dynamically on other attributes',
*/
]);

View File

@@ -1,71 +0,0 @@
<?php
/**
* Module combodo-flow-map
*
* @copyright Copyright (C) 2013 XXXXX
* @license http://opensource.org/licenses/AGPL-3.0
*/
Dict::Add('FR FR', 'French', 'Français', [
'Relation:flow/Description' => 'Carte des flux de données',
'Relation:flow/DownStream' => 'Flux sortants...',
'Relation:flow/UpStream' => 'Flux reçus...',
'Class:DataFlow' => 'Flux de Données',
'Class:DataFlow+' => 'Modélise les données transférées entre instances d\'application',
'Class:DataFlow/Name' => '%1$s de %2$s à %3$s',
'Class:DataFlow/Attribute:name' => 'Nom',
'Class:DataFlow/Attribute:name_id+' => 'Type de données transferées',
'Class:DataFlow/Attribute:source_id' => 'Source',
'Class:DataFlow/Attribute:source_id+' => 'Instance d\application à la source du flux de données',
'Class:DataFlow/Attribute:destination_id' => 'Destinataire',
'Class:DataFlow/Attribute:destination_id+' => 'Destinataire des données, à choisir parmi les instances d\'application',
'Class:DataFlow/Attribute:type_id' => 'Type de flux',
'Class:DataFlow/Attribute:type_id+' => 'Typologie du flux',
'Class:DataFlow/Attribute:description' => 'Description',
'Class:DataFlow/Attribute:description+' => '',
'Class:DataFlow/Attribute:status' => 'Etat',
'Class:DataFlow/Attribute:status+' => '',
'Class:DataFlow/Attribute:status/Value:active' => 'actif',
'Class:DataFlow/Attribute:status/Value:inactive' => 'inactif',
'Class:DataFlow/Attribute:org_id' => 'Organisation',
'Class:DataFlow/Attribute:org_id+' => '',
'Class:DataFlow/Attribute:business_criticity' => 'Criticité',
'Class:DataFlow/Attribute:business_criticity+' => '',
'Class:DataFlow/Attribute:business_criticity/Value:high' => 'haute',
'Class:DataFlow/Attribute:business_criticity/Value:high+' => '',
'Class:DataFlow/Attribute:business_criticity/Value:low' => 'basse',
'Class:DataFlow/Attribute:business_criticity/Value:low+' => '',
'Class:DataFlow/Attribute:business_criticity/Value:medium' => 'moyenne',
'Class:DataFlow/Attribute:business_criticity/Value:medium+' => '',
'Class:DataFlow/Attribute:execution_frequency' => 'Fréquence d\'exécution',
'Class:DataFlow/Attribute:execution_frequency+' => 'À quelle fréquence le transfert de données est-il exécuté',
'Class:DataFlow/Attribute:execution_frequency/Value:real_time' => 'temps réel',
'Class:DataFlow/Attribute:execution_frequency/Value:real_time+' => '',
'Class:DataFlow/Attribute:execution_frequency/Value:hourly' => 'horaire',
'Class:DataFlow/Attribute:execution_frequency/Value:hourly+' => '',
'Class:DataFlow/Attribute:execution_frequency/Value:daily' => 'journalière',
'Class:DataFlow/Attribute:execution_frequency/Value:daily+' => '',
'Class:DataFlow/Attribute:execution_frequency/Value:weekly' => 'hebdomadaire',
'Class:DataFlow/Attribute:execution_frequency/Value:weekly+' => '',
'Class:DataFlow/Attribute:execution_frequency/Value:monthly' => 'mensuelle',
'Class:DataFlow/Attribute:execution_frequency/Value:monthly+' => '',
'Class:DataFlow/Attribute:execution_frequency/Value:yearly' => 'annuelle',
'Class:DataFlow/Attribute:execution_frequency/Value:yearly+' => '',
/*
'Class:DataFlow/Attribute:source_id_friendlyname' => 'source_id_friendlyname',
'Class:DataFlow/Attribute:source_id_friendlyname+' => 'Nom complet',
'Class:DataFlow/Attribute:source_id_finalclass_recall' => 'source_id->CI sub-class',
'Class:DataFlow/Attribute:source_id_finalclass_recall+' => 'Classe finale',
'Class:DataFlow/Attribute:source_id_obsolescence_flag' => 'source_id->Obsolete',
'Class:DataFlow/Attribute:source_id_obsolescence_flag+' => 'Computed dynamically on other attributes',
'Class:DataFlow/Attribute:destination_id_friendlyname' => 'destination_id_friendlyname',
'Class:DataFlow/Attribute:destination_id_friendlyname+' => 'Nom complet',
'Class:DataFlow/Attribute:destination_id_finalclass_recall' => 'destination_id->CI sub-class',
'Class:DataFlow/Attribute:destination_id_finalclass_recall+' => 'Classe finale',
'Class:DataFlow/Attribute:destination_id_obsolescence_flag' => 'destination_id->Obsolete',
'Class:DataFlow/Attribute:destination_id_obsolescence_flag+' => 'Computed dynamically on other attributes',
*/
]);

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="mv_DwPz_GcV~datTQ_sP3a" x1="27.258" x2="38.501" y1="18.189" y2="44.314" gradientTransform="rotate(90 23.5 24)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3a)" d="M14,41.19V37h14c0.552,0,1-0.448,1-1v-4c0-0.552-0.448-1-1-1H14v-4.19 c0-0.72-0.87-1.08-1.379-0.571L5.92,32.939c-0.586,0.586-0.586,1.536,0,2.121l6.701,6.701C13.13,42.271,14,41.91,14,41.19z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3b" x1="32.674" x2="34.456" y1="9.581" y2="13.722" gradientTransform="rotate(90 23.5 24)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3b)" d="M35,36v-4c0-0.552,0.448-1,1-1l0,0c0.552,0,1,0.448,1,1v4c0,0.552-0.448,1-1,1l0,0 C35.448,37,35,36.552,35,36z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3c" x1="32.674" x2="34.456" y1="5.581" y2="9.722" gradientTransform="rotate(90 23.5 24)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3c)" d="M39,36v-4c0-0.552,0.448-1,1-1l0,0c0.552,0,1,0.448,1,1v4c0,0.552-0.448,1-1,1l0,0 C39.448,37,39,36.552,39,36z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3d" x1="32.674" x2="34.456" y1="13.581" y2="17.722" gradientTransform="rotate(90 23.5 24)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3d)" d="M31,36v-4c0-0.552,0.448-1,1-1h0c0.552,0,1,0.448,1,1v4c0,0.552-0.448,1-1,1h0 C31.448,37,31,36.552,31,36z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3e" x1="551.258" x2="562.501" y1="-252.291" y2="-226.167" gradientTransform="rotate(-90 421.24 151.26)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1ea2e4"/><stop offset="1" stop-color="#32bdef"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3e)" d="M33,7.81V12H19c-0.552,0-1,0.448-1,1v4c0,0.552,0.448,1,1,1h14v4.19 c0,0.72,0.87,1.08,1.379,0.571l6.701-6.701c0.586-0.586,0.586-1.536,0-2.121l-6.701-6.701C33.87,6.729,33,7.09,33,7.81z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3f" x1="556.674" x2="558.456" y1="-260.899" y2="-256.759" gradientTransform="rotate(-90 421.24 151.26)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1ea2e4"/><stop offset="1" stop-color="#32bdef"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3f)" d="M12,13v4c0,0.552-0.448,1-1,1h0c-0.552,0-1-0.448-1-1v-4c0-0.552,0.448-1,1-1h0 C11.552,12,12,12.448,12,13z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3g" x1="556.674" x2="558.456" y1="-264.899" y2="-260.759" gradientTransform="rotate(-90 421.24 151.26)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1ea2e4"/><stop offset="1" stop-color="#32bdef"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3g)" d="M8,13v4c0,0.552-0.448,1-1,1h0c-0.552,0-1-0.448-1-1v-4c0-0.552,0.448-1,1-1h0 C7.552,12,8,12.448,8,13z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3h" x1="556.674" x2="558.456" y1="-256.899" y2="-252.758" gradientTransform="rotate(-90 421.24 151.26)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1ea2e4"/><stop offset="1" stop-color="#32bdef"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3h)" d="M16,13v4c0,0.552-0.448,1-1,1h0c-0.552,0-1-0.448-1-1v-4c0-0.552,0.448-1,1-1h0 C15.552,12,16,12.448,16,13z"/></svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

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

View File

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

View File

@@ -186,9 +186,7 @@ function collect_configuration()
// iTop modules
$oConfig = MetaModel::GetConfig();
$sLatestInstallationDate = CMDBSource::QueryToScalar("SELECT max(installed) FROM ".$oConfig->Get('db_subname')."priv_module_install");
// Get the latest installed modules, without the "root" ones (iTop version and datamodel version)
$aInstalledModules = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install WHERE installed = '".$sLatestInstallationDate."' AND parent_id != 0");
$aInstalledModules = ModuleInstallationRepository::GetInstance()->ReadFromDB($oConfig);
foreach ($aInstalledModules as $aDBInfo) {
$aConfiguration['itop_modules'][$aDBInfo['name']] = $aDBInfo['version'];

View File

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

View File

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

View File

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

View File

@@ -76,6 +76,9 @@
<attribute id="finalclass"/>
</attributes>
</reconciliation>
<obsolescence>
<condition><![CDATA[status='obsolete']]></condition>
</obsolescence>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
@@ -176,17 +179,32 @@
<field id="status" xsi:type="AttributeEnum">
<sort_type>rank</sort_type>
<values>
<value id="production">
<code>production</code>
<rank>20</rank>
</value>
<value id="implementation">
<code>implementation</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="production">
<code>production</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="obsolete">
<code>obsolete</code>
<rank>30</rank>
<style>
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sql>status</sql>
@@ -1133,6 +1151,9 @@ public function PrefillSearchForm(&$aContextParam)
<attribute id="organization_name"/>
</attributes>
</reconciliation>
<obsolescence>
<condition><![CDATA[status='obsolete']]></condition>
</obsolescence>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
@@ -1184,17 +1205,32 @@ public function PrefillSearchForm(&$aContextParam)
<field id="status" xsi:type="AttributeEnum">
<sort_type>rank</sort_type>
<values>
<value id="production">
<code>production</code>
<rank>20</rank>
</value>
<value id="implementation">
<code>implementation</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="production">
<code>production</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="obsolete">
<code>obsolete</code>
<rank>30</rank>
<style>
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sql>status</sql>
@@ -1537,6 +1573,9 @@ public function PrefillSearchForm(&$aContextParam)
<attribute id="service_name"/>
</attributes>
</reconciliation>
<obsolescence>
<condition><![CDATA[status='obsolete']]></condition>
</obsolescence>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
@@ -1585,17 +1624,32 @@ public function PrefillSearchForm(&$aContextParam)
<field id="status" xsi:type="AttributeEnum">
<sort_type>rank</sort_type>
<values>
<value id="production">
<code>production</code>
<rank>20</rank>
</value>
<value id="implementation">
<code>implementation</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="production">
<code>production</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="obsolete">
<code>obsolete</code>
<rank>30</rank>
<style>
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sql>status</sql>

View File

@@ -76,6 +76,9 @@
<attribute id="finalclass"/>
</attributes>
</reconciliation>
<obsolescence>
<condition><![CDATA[status='obsolete']]></condition>
</obsolescence>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
@@ -176,17 +179,32 @@
<field id="status" xsi:type="AttributeEnum">
<sort_type>rank</sort_type>
<values>
<value id="production">
<code>production</code>
<rank>20</rank>
</value>
<value id="implementation">
<code>implementation</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="production">
<code>production</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="obsolete">
<code>obsolete</code>
<rank>30</rank>
<style>
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sql>status</sql>
@@ -1122,6 +1140,9 @@ public function PrefillSearchForm(&$aContextParam)
<attribute id="organization_name"/>
</attributes>
</reconciliation>
<obsolescence>
<condition><![CDATA[status='obsolete']]></condition>
</obsolescence>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
@@ -1173,17 +1194,32 @@ public function PrefillSearchForm(&$aContextParam)
<field id="status" xsi:type="AttributeEnum">
<sort_type>rank</sort_type>
<values>
<value id="production">
<code>production</code>
<rank>20</rank>
</value>
<value id="implementation">
<code>implementation</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="production">
<code>production</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="obsolete">
<code>obsolete</code>
<rank>30</rank>
<style>
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sql>status</sql>
@@ -1548,6 +1584,9 @@ public function PrefillSearchForm(&$aContextParam)
<attribute id="service_name"/>
</attributes>
</reconciliation>
<obsolescence>
<condition><![CDATA[status='obsolete']]></condition>
</obsolescence>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
@@ -1596,17 +1635,32 @@ public function PrefillSearchForm(&$aContextParam)
<field id="status" xsi:type="AttributeEnum">
<sort_type>rank</sort_type>
<values>
<value id="production">
<code>production</code>
<rank>20</rank>
</value>
<value id="implementation">
<code>implementation</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="production">
<code>production</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="obsolete">
<code>obsolete</code>
<rank>30</rank>
<style>
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sql>status</sql>

View File

@@ -1486,14 +1486,29 @@
<value id="draft">
<code>draft</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="published">
<code>published</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="obsolete">
<code>obsolete</code>
<rank>30</rank>
<style>
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sql>status</sql>

View File

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

View File

@@ -43,18 +43,38 @@
<value id="production">
<code>production</code>
<rank>30</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="implementation">
<code>implementation</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="stock">
<code>stock</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-neutral-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-neutral-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="obsolete">
<code>obsolete</code>
<rank>40</rank>
<style>
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sql>status</sql>

View File

@@ -26,23 +26,12 @@ use Composer\Semver\VersionParser;
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
@@ -320,24 +309,6 @@ class InstalledVersions
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
@@ -354,9 +325,7 @@ class InstalledVersions
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
@@ -364,14 +333,11 @@ class InstalledVersions
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
if (strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $required;
self::$installedIsLocalDir = true;
$copiedLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}

View File

@@ -3,7 +3,7 @@
'name' => 'combodo/itop',
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '19d062aa830b6d6c7d17ac4046fc9ee2c5e3fab1',
'reference' => '469afdb2f9aea1b6e078a2a5bb12f09a969d60e0',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -22,7 +22,7 @@
'combodo/itop' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '19d062aa830b6d6c7d17ac4046fc9ee2c5e3fab1',
'reference' => '469afdb2f9aea1b6e078a2a5bb12f09a969d60e0',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),

View File

@@ -0,0 +1,130 @@
<?php
require_once __DIR__.'/ModuleInstallationRepository.php';
class AnalyzeInstallation
{
private static AnalyzeInstallation $oInstance;
private ?array $aAvailableModules = null;
protected function __construct()
{
}
final public static function GetInstance(): AnalyzeInstallation
{
if (!isset(self::$oInstance)) {
self::$oInstance = new AnalyzeInstallation();
}
return self::$oInstance;
}
final public static function SetInstance(?AnalyzeInstallation $oInstance): void
{
self::$oInstance = $oInstance;
}
/**
* Analyzes the current installation and the possibilities
*
* @param null|Config $oConfig Defines the target environment (DB)
* @param mixed $modulesPath Either a single string or an array of absolute paths
* @param bool $bAbortOnMissingDependency ...
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
*
* @return array Array with the following format:
* array =>
* 'iTop' => array(
* 'installed_version' => ... (could be empty in case of a fresh install)
* 'available_version => ...
* )
* <module_name> => array(
* 'installed_version' => ...
* 'available_version' => ...
* 'install' => array(
* 'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
* 'message' => ...
* )
* 'uninstall' => array(
* 'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
* 'message' => ...
* )
* 'label' => ...
* 'dependencies' => array(<module1>, <module2>, ...)
* 'visible' => true | false
* )
* )
* @throws \Exception
*/
public function AnalyzeInstallation(?Config $oConfig, mixed $modulesPath, bool $bAbortOnMissingDependency = false, ?array $aModulesToLoad = null)
{
$aRes = [
ROOT_MODULE => [
'installed_version' => '',
'available_version' => ITOP_VERSION_FULL,
'name_code' => ITOP_APPLICATION,
],
];
$aDirs = is_array($modulesPath) ? $modulesPath : [$modulesPath];
if (! is_null($this->aAvailableModules)) {
//test only
$aAvailableModules = $this->aAvailableModules;
} else {
$aAvailableModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
}
foreach ($aAvailableModules as $sModuleId => $aModuleInfo) {
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
$aModuleInfo['installed_version'] = '';
$aModuleInfo['available_version'] = $sModuleVersion;
if ($aModuleInfo['mandatory']) {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_MANDATORY,
'message' => 'the module is part of the application',
];
} else {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
$aRes[$sModuleName] = $aModuleInfo;
}
$aCurrentlyInstalledModules = ModuleInstallationRepository::GetInstance()->ReadComputeInstalledModules($oConfig);
// Adjust the list of proposed modules
foreach ($aCurrentlyInstalledModules as $sModuleName => $aModuleDB) {
if ($sModuleName == ROOT_MODULE) {
$aRes[$sModuleName]['installed_version'] = $aModuleDB['version'];
continue;
}
if (!array_key_exists($sModuleName, $aRes)) {
// A module was installed, it is not proposed in the new build... skip
continue;
}
$aRes[$sModuleName]['installed_version'] = $aModuleDB['version'];
if ($aRes[$sModuleName]['mandatory']) {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_IMPOSSIBLE,
'message' => 'the module is part of the application',
];
} else {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
}
return $aRes;
}
}

View File

@@ -0,0 +1,238 @@
<?php
class ModuleInstallationRepository
{
private static ModuleInstallationRepository $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): ModuleInstallationRepository
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModuleInstallationRepository();
}
return self::$oInstance;
}
final public static function SetInstance(?ModuleInstallationRepository $oInstance): void
{
self::$oInstance = $oInstance;
}
private ?array $aSelectInstall = null;
/**
* @param \Config|null $oConfig
* @return array
*/
public function ReadComputeInstalledModules(?Config $oConfig): array
{
$aSelectInstall = [];
try {
$aSelectInstall = $this->ReadFromDB($oConfig);
} catch (MySQLException $e) {
// No database or erroneous information
}
return $this->ComputeInstalledModules($aSelectInstall);
}
/**
* @param \Config|null $oConfig
* @return array
* @throws \MySQLException
* @throws \MySQLQueryHasNoResultException
*/
public function ReadFromDB(?Config $oConfig): array
{
if (is_null($oConfig)) {
return [];
}
if (! is_null($this->aSelectInstall)) {
//test only
return $this->aSelectInstall;
}
CMDBSource::InitFromConfig($oConfig);
//read db module installations
$tableWithPrefix = $this->GetTableWithPrefix($oConfig);
$iRootId = CMDBSource::QueryToScalar("SELECT max(parent_id) FROM $tableWithPrefix");
// Get the latest installed modules, without the "root" ones (iTop version and datamodel version)
$sSQL = <<<SQL
SELECT * FROM $tableWithPrefix
WHERE
parent_id='$iRootId'
OR id='$iRootId'
SQL;
return CMDBSource::QueryToArray($sSQL);
}
private function GetTableWithPrefix(Config $oConfig)
{
$sPrefix = $oConfig->Get('db_subname');
if (utils::IsNullOrEmptyString($sPrefix)) {
return "priv_module_install";
}
return "{$sPrefix}priv_module_install";
}
/**
* @param \Config $oConfig
*
* @return array|false
*/
public function GetApplicationVersion(Config $oConfig)
{
try {
CMDBSource::InitFromConfig($oConfig);
$tableWithPrefix = $this->GetTableWithPrefix($oConfig);
$sSQLQuery = "SELECT * FROM $tableWithPrefix";
$aSelectInstall = CMDBSource::QueryToArray($sSQLQuery);
} catch (MySQLException $e) {
// No database or erroneous information
SetupLog::Error(
'Can not connect to the database',
null,
[
'host' => $oConfig->Get('db_host'),
'user' => $oConfig->Get('db_user'),
'pwd:' => $oConfig->Get('db_pwd'),
'db name' => $oConfig->Get('db_name'),
'msg' => $e->getMessage(),
]
);
return false;
}
$aResult = [];
// Scan the list of installed modules to get the version of the 'ROOT' module which holds the main application version
foreach ($aSelectInstall as $aInstall) {
$sModuleVersion = $aInstall['version'];
if ($sModuleVersion == '') {
// Though the version cannot be empty in iTop 2.0, it used to be possible
// therefore we have to put something here or the module will not be considered
// as being installed
$sModuleVersion = '0.0.0';
}
if ($aInstall['parent_id'] == 0) {
if ($aInstall['name'] == DATAMODEL_MODULE) {
$aResult['datamodel_version'] = $sModuleVersion;
$aComments = json_decode($aInstall['comment'], true);
if (is_array($aComments)) {
$aResult = array_merge($aResult, $aComments);
}
} else {
$aResult['product_name'] = $aInstall['name'];
$aResult['product_version'] = $sModuleVersion;
}
}
}
if (!array_key_exists('datamodel_version', $aResult)) {
// Versions prior to 2.0 did not record the version of the datamodel
// so assume that the datamodel version is equal to the application version
$aResult['datamodel_version'] = $aResult['product_version'];
}
SetupLog::Info(__METHOD__, null, ["product_name" => $aResult['product_name'], "product_version" => $aResult['product_version']]);
return count($aResult) == 0 ? false : $aResult;
}
private function ComputeInstalledModules(array $aSelectInstall): array
{
$aInstallByModule = []; // array of <module> => array ('installed' => timestamp, 'version' => <version>)
//module installation datetime is mostly the same for all modules
//unless there was issue recording things in DB
$sFirstDatetime = null;
$iFirstTime = -1;
foreach ($aSelectInstall as $aInstall) {
//$aInstall['comment']; // unsused
$sDatetime = $aInstall['installed'];
if (is_null($sFirstDatetime)) {
$sFirstDatetime = $sDatetime;
$iFirstTime = strtotime($sDatetime);
$iInstalled = $iFirstTime;
} elseif ($sDatetime === $sFirstDatetime) {
$iInstalled = $iFirstTime;
} else {
$sDatetime = $aInstall['installed'];
$iInstalled = strtotime($sDatetime);
}
$sModuleName = $aInstall['name'];
$sModuleVersion = $aInstall['version'];
if ($sModuleVersion == '') {
// Though the version cannot be empty in iTop 2.0, it used to be possible
// therefore we have to put something here or the module will not be considered
// as being installed
$sModuleVersion = '0.0.0';
}
if ($aInstall['parent_id'] == 0) {
$aInstallByModule[ROOT_MODULE] = [
'installed_version' => $sModuleVersion,
'installed' => $iInstalled,
'version' => $sModuleVersion,
];
} else {
$aInstallByModule[$sModuleName] = [
'installed' => $iInstalled,
'version' => $sModuleVersion,
];
}
}
return $aInstallByModule;
}
/**
* Return previous module installation. offset is applied on parent_id.
* @param $iOffset: by default (offset=0) returns current installation
* @return array
*/
public static function GetPreviousModuleInstallationsByOffset(int $iOffset = 0): array
{
$oFilter = DBObjectSearch::FromOQL('SELECT ModuleInstallation AS mi WHERE mi.parent_id=0 AND mi.name!="datamodel"');
$oSet = new DBObjectSet($oFilter, ['installed' => false]); // Most recent first
$oSet->SetLimit($iOffset + 1);
$iParentId = 0;
while (!is_null($oModuleInstallation = $oSet->Fetch())) {
/** @var \DBObject $oModuleInstallation */
if ($iOffset == 0) {
$iParentId = $oModuleInstallation->Get('id');
break;
}
$iOffset--;
}
if ($iParentId === 0) {
IssueLog::Error("no ITOP_APPLICATION ModuleInstallation found", null, ['offset' => $iOffset]);
throw new \Exception("no ITOP_APPLICATION ModuleInstallation found");
}
$oFilter = DBObjectSearch::FromOQL("SELECT ModuleInstallation AS mi WHERE mi.id=$iParentId OR mi.parent_id=$iParentId");
$oSet = new DBObjectSet($oFilter); // Most recent first
$aRawValues = $oSet->ToArrayOfValues();
$aValues = [];
foreach ($aRawValues as $aRawValue) {
$aValue = [];
foreach ($aRawValue as $sAliasAttCode => $sValue) {
// remove 'mi.' from AttCode
$sAttCode = substr($sAliasAttCode, 3);
$aValue[$sAttCode] = $sValue;
}
$aValues[] = $aValue;
}
return $aValues;
}
}

View File

@@ -127,7 +127,7 @@ header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past
/**
* Main program
*/
$sOperation = Utils::ReadParam('operation', '');
$sOperation = utils::ReadParam('operation', '');
try {
SetupUtils::CheckSetupToken();
@@ -164,7 +164,7 @@ try {
break;
case 'toggle_use_symbolic_links':
$sUseSymbolicLinks = Utils::ReadParam('bUseSymbolicLinks', false);
$sUseSymbolicLinks = utils::ReadParam('bUseSymbolicLinks', false);
$bUseSymbolicLinks = ($sUseSymbolicLinks === 'true');
MFCompiler::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
echo "toggle useSymbolicLinks flag : $bUseSymbolicLinks";

View File

@@ -17,9 +17,13 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Setup\FeatureRemoval\InplaceSetupAudit;
use Combodo\iTop\Setup\FeatureRemoval\ModelReflectionSerializer;
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/feature_removal/InplaceSetupAudit.php';
/**
* The base class for the installation process.
@@ -46,6 +50,8 @@ class ApplicationInstaller
protected $oParams;
protected static $bMetaModelStarted = false;
protected Config $oConfig;
/**
* @param \Parameters $oParams
*
@@ -57,9 +63,9 @@ class ApplicationInstaller
$this->oParams = $oParams;
$aParamValues = $oParams->GetParamForConfigArray();
$oConfig = new Config();
$oConfig->UpdateFromParams($aParamValues, null);
utils::SetConfig($oConfig);
$this->oConfig = new Config();
$this->oConfig->UpdateFromParams($aParamValues);
utils::SetConfig($this->oConfig);
}
/**
@@ -215,7 +221,7 @@ class ApplicationInstaller
$aPreinstall = $this->oParams->Get('preinstall');
$aCopies = $aPreinstall['copies'] ?? [];
self::DoCopy($aCopies);
$this->DoCopy($aCopies);
$sReport = "Copying...";
$aResult = [
@@ -238,11 +244,8 @@ class ApplicationInstaller
// __DB__-%Y-%m-%d
$sDestination = $aPreinstall['backup']['destination'];
$sSourceConfigFile = $aPreinstall['backup']['configuration_file'];
$aDBParams = $this->oParams->GetParamForConfigArray();
$oTempConfig = new Config();
$oTempConfig->UpdateFromParams($aDBParams);
$sMySQLBinDir = $this->oParams->Get('mysql_bindir', null);
self::DoBackup($oTempConfig, $sDestination, $sSourceConfigFile, $sMySQLBinDir);
$this->DoBackup($sDestination, $sSourceConfigFile, $sMySQLBinDir);
$aResult = [
'status' => self::OK,
@@ -257,9 +260,9 @@ class ApplicationInstaller
$aSelectedModules = $this->oParams->Get('selected_modules');
$sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest');
$sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions');
$sTargetEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
$aMiscOptions = $this->oParams->Get('options', []);
$aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []);
$sSkipDataAudit = $this->oParams->Get('skip-data-audit', '');
$bUseSymbolicLinks = null;
if ((isset($aMiscOptions['symlinks']) && $aMiscOptions['symlinks'])) {
@@ -272,40 +275,49 @@ class ApplicationInstaller
}
$aParamValues = $this->oParams->GetParamForConfigArray();
self::DoCompile(
$bIsSetupDataAuditEnabled = $this->IsSetupDataAuditEnabled($sSkipDataAudit, $aParamValues);
$this->DoCompile(
$aRemovedExtensionCodes,
$aSelectedModules,
$sSourceDir,
$sExtensionDir,
$sTargetDir,
$sTargetEnvironment,
$bUseSymbolicLinks,
$aParamValues
$bIsSetupDataAuditEnabled,
$bUseSymbolicLinks
);
if ($bIsSetupDataAuditEnabled) {
$sNextStep = 'setup-audit';
$sNextStepLabel = 'Checking data consistency with the new data model';
} else {
$sNextStep = 'db-schema';
$sNextStepLabel = 'Updating database schema';
}
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => $sNextStep,
'next-step-label' => $sNextStepLabel,
'percentage-completed' => 40,
];
break;
case 'setup-audit':
$this->DoSetupAudit();
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => 'db-schema',
'next-step-label' => 'Updating database schema',
'percentage-completed' => 40,
'percentage-completed' => 50,
];
break;
case 'db-schema':
$aSelectedModules = $this->oParams->Get('selected_modules', []);
$sTargetEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
$aParamValues = $this->oParams->GetParamForConfigArray();
$bOldAddon = $this->oParams->Get('old_addon', false);
$sUrl = $this->oParams->Get('url', '');
self::DoUpdateDBSchema(
$aSelectedModules,
$sTargetDir,
$aParamValues,
$sTargetEnvironment,
$bOldAddon,
$sUrl
$this->DoUpdateDBSchema(
$aSelectedModules
);
$aResult = [
@@ -318,25 +330,17 @@ class ApplicationInstaller
break;
case 'after-db-create':
$sTargetEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
$aParamValues = $this->oParams->GetParamForConfigArray();
$aAdminParams = $this->oParams->Get('admin_account');
$sAdminUser = $aAdminParams['user'];
$sAdminPwd = $aAdminParams['pwd'];
$sAdminLanguage = $aAdminParams['language'];
$aSelectedModules = $this->oParams->Get('selected_modules', []);
$bOldAddon = $this->oParams->Get('old_addon', false);
self::AfterDBCreate(
$sTargetDir,
$aParamValues,
$this->AfterDBCreate(
$sAdminUser,
$sAdminPwd,
$sAdminLanguage,
$aSelectedModules,
$sTargetEnvironment,
$bOldAddon
$aSelectedModules
);
$aResult = [
@@ -350,18 +354,10 @@ class ApplicationInstaller
case 'load-data':
$aSelectedModules = $this->oParams->Get('selected_modules');
$sTargetEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
$aParamValues = $this->oParams->GetParamForConfigArray();
$bOldAddon = $this->oParams->Get('old_addon', false);
$bSampleData = ($this->oParams->Get('sample_data', 0) == 1);
self::DoLoadFiles(
$this->DoLoadFiles(
$aSelectedModules,
$sTargetDir,
$aParamValues,
$sTargetEnvironment,
$bOldAddon,
$bSampleData
);
@@ -375,24 +371,16 @@ class ApplicationInstaller
break;
case 'create-config':
$sTargetEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
$sPreviousConfigFile = $this->oParams->Get('previous_configuration_file', '');
$sDataModelVersion = $this->oParams->Get('datamodel_version', '0.0.0');
$bOldAddon = $this->oParams->Get('old_addon', false);
$aSelectedModuleCodes = $this->oParams->Get('selected_modules', []);
$aSelectedExtensionCodes = $this->oParams->Get('selected_extensions', []);
$aParamValues = $this->oParams->GetParamForConfigArray();
self::DoCreateConfig(
$sTargetDir,
$this->DoCreateConfig(
$sPreviousConfigFile,
$sTargetEnvironment,
$sDataModelVersion,
$bOldAddon,
$aSelectedModuleCodes,
$aSelectedExtensionCodes,
$aParamValues,
$sInstallComment
);
@@ -473,7 +461,7 @@ class ApplicationInstaller
SetupUtils::ExitReadOnlyMode();
}
protected static function DoCopy($aCopies)
protected function DoCopy($aCopies)
{
$aReports = [];
foreach ($aCopies as $aCopy) {
@@ -494,7 +482,6 @@ class ApplicationInstaller
}
/**
* @param Config $oConfig
* @param string $sBackupFileFormat
* @param string $sSourceConfigFile
* @param string $sMySQLBinDir
@@ -504,26 +491,25 @@ class ApplicationInstaller
* @throws \MySQLException
* @since 2.5.0 uses a {@link Config} object to store DB parameters
*/
protected static function DoBackup($oConfig, $sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null)
protected function DoBackup($sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null)
{
$oBackup = new SetupDBBackup($oConfig);
$oBackup = new SetupDBBackup($this->oConfig);
$sTargetFile = $oBackup->MakeName($sBackupFileFormat);
if (!empty($sMySQLBinDir)) {
$oBackup->SetMySQLBinDir($sMySQLBinDir);
}
CMDBSource::InitFromConfig($oConfig);
CMDBSource::InitFromConfig($this->oConfig);
$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
}
/**
* @param array $aRemovedExtensionCodes
* @param array $aSelectedModules
* @param string $sSourceDir
* @param string $sExtensionDir
* @param string $sTargetDir
* @param string $sEnvironment
* @param bool $bIsSetupDataAuditEnabled
* @param boolean $bUseSymbolicLinks
* @param array $aParamValues
*
* @return void
* @throws \ConfigException
@@ -531,14 +517,24 @@ class ApplicationInstaller
*
* @since 3.1.0 N°2013 added the aParamValues param
*/
protected static function DoCompile($aSelectedModules, $sSourceDir, $sExtensionDir, $sTargetDir, $sEnvironment, $bUseSymbolicLinks = null, $aParamValues = [])
protected function DoCompile($aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, bool &$bIsSetupDataAuditEnabled, $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");
}
@@ -560,20 +556,25 @@ class ApplicationInstaller
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 ($bIsSetupDataAuditEnabled) {
if ($bIsAlreadyInMaintenanceMode) {
//required to read DM before calling SaveModelInfo
SetupUtils::ExitMaintenanceMode();
$bIsAlreadyInMaintenanceMode = false;
}
$bIsSetupDataAuditEnabled = $this->SaveModelInfo($sEnvironment);
}
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) {
$sConfigFilePath = utils::GetConfigFilePath($sEnvironment);
if (is_file($sConfigFilePath)) {
$oConfig = new Config($sConfigFilePath);
} else {
$oConfig = null;
}
if (false === is_null($oConfig)) {
$oConfig->UpdateFromParams($aParamValues);
SetupUtils::EnterMaintenanceMode($oConfig);
}
SetupUtils::EnterMaintenanceMode($oConfig);
}
if (!is_dir($sTargetPath)) {
@@ -589,6 +590,9 @@ class ApplicationInstaller
SetupUtils::tidydir($sTargetPath);
}
$oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan);
$oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes);
$oFactory = new ModelFactory($aDirsToScan);
$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
@@ -655,20 +659,92 @@ class ApplicationInstaller
}
}
private function GetModelInfoPath(string $sEnv): string
{
return APPROOT."data/beforecompilation_".$sEnv."_modelinfo.json";
}
private 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));
}
private 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 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->GetTargetEnv();
$aPreviousCompilationModelInfo = $this->GetPreviousModelInfo($sTargetEnvironment);
$oSetupAudit = new InplaceSetupAudit($aPreviousCompilationModelInfo, $sTargetEnvironment);
$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");
}
}
private 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
* @param $sModulesDir
* @param $aParamValues
* @param string $sTargetEnvironment
* @param bool $bOldAddon
* @param string $sAppRootUrl
*
* @throws \ConfigException
* @throws \CoreException
* @throws \MySQLException
*/
protected static function DoUpdateDBSchema($aSelectedModules, $sModulesDir, $aParamValues, $sTargetEnvironment = '', $bOldAddon = false, $sAppRootUrl = '')
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
@@ -682,11 +758,6 @@ class ApplicationInstaller
$oConfig = new Config();
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
if ($bOldAddon) {
// Old version of the add-on for backward compatibility with pre-2.0 data models
$oConfig->SetAddons([]);
}
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
$oProductionEnv->InitDataModel($oConfig, true); // load data model only
@@ -741,9 +812,8 @@ class ApplicationInstaller
}
// Module specific actions (migrate the data)
//
$aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir);
$oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation');
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation', $aSelectedModules);
if (!$oProductionEnv->CreateDatabaseStructure(MetaModel::GetConfig(), $sMode)) {
throw new Exception("Failed to create/upgrade the database structure for environment '$sTargetEnvironment'");
@@ -839,16 +909,16 @@ class ApplicationInstaller
ModuleInstallerAPI::MoveColumnInDB($sDBPrefix.'priv_query', 'fields', $sDBPrefix.'priv_query_oql', 'fields');
}
protected static function AfterDBCreate(
$sModulesDir,
$aParamValues,
protected function AfterDBCreate(
$sAdminUser,
$sAdminPwd,
$sAdminLanguage,
$aSelectedModules,
$sTargetEnvironment,
$bOldAddon
$aSelectedModules
) {
$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
@@ -860,11 +930,6 @@ class ApplicationInstaller
$oConfig = new Config();
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
if ($bOldAddon) {
// Old version of the add-on for backward compatibility with pre-2.0 data models
$oConfig->SetAddons([]);
}
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
$oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
@@ -873,7 +938,7 @@ class ApplicationInstaller
// Perform here additional DB setup... profiles, etc...
//
$aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir);
$oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation');
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation', $aSelectedModules);
$oProductionEnv->UpdatePredefinedObjects();
@@ -887,7 +952,7 @@ class ApplicationInstaller
// Perform final setup tasks here
//
$oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup');
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup', $aSelectedModules);
}
/**
@@ -905,14 +970,14 @@ class ApplicationInstaller
}
}
protected static function DoLoadFiles(
protected function DoLoadFiles(
$aSelectedModules,
$sModulesDir,
$aParamValues,
$sTargetEnvironment = 'production',
$bOldAddon = false,
$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
@@ -922,11 +987,6 @@ class ApplicationInstaller
$oConfig = new Config();
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
if ($bOldAddon) {
// Old version of the add-on for backward compatibility with pre-2.0 data models
$oConfig->SetAddons([]);
}
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
//Load the MetaModel if needed (asynchronous mode)
@@ -937,22 +997,19 @@ class ApplicationInstaller
}
$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, APPROOT.$sModulesDir);
$oProductionEnv->LoadData($aAvailableModules, $aSelectedModules, $bSampleData);
$oProductionEnv->LoadData($aAvailableModules, $bSampleData, $aSelectedModules);
// Perform after dbload setup tasks here
//
$oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad');
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad', $aSelectedModules);
}
/**
* @param string $sModulesDir
* @param string $sPreviousConfigFile
* @param string $sTargetEnvironment
* @param string $sDataModelVersion
* @param boolean $bOldAddon
* @param array $aSelectedModuleCodes
* @param array $aSelectedExtensionCodes
* @param array $aParamValues parameters array used to create config file using {@see Config::UpdateFromParams}
* @param string|null $sInstallComment
*
* @param null $sInstallComment
*
@@ -960,17 +1017,17 @@ class ApplicationInstaller
* @throws \CoreException
* @throws \Exception
*/
protected static function DoCreateConfig(
$sModulesDir,
protected function DoCreateConfig(
$sPreviousConfigFile,
$sTargetEnvironment,
$sDataModelVersion,
$bOldAddon,
$aSelectedModuleCodes,
$aSelectedExtensionCodes,
$aParamValues,
$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
@@ -980,12 +1037,10 @@ class ApplicationInstaller
$aParamValues['selected_modules'] = implode(',', $aSelectedModuleCodes);
$sMode = $aParamValues['mode'];
$bPreserveModuleSettings = false;
if ($sMode == 'upgrade') {
try {
$oOldConfig = new Config($sPreviousConfigFile);
$oConfig = clone($oOldConfig);
$bPreserveModuleSettings = true;
} catch (Exception $e) {
// In case the previous configuration is corrupted... start with a blank new one
$oConfig = new Config();
@@ -999,11 +1054,7 @@ class ApplicationInstaller
$oConfig->Set('access_mode', ACCESS_FULL);
// Final config update: add the modules
$oConfig->UpdateFromParams($aParamValues, $sModulesDir, $bPreserveModuleSettings);
if ($bOldAddon) {
// Old version of the add-on for backward compatibility with pre-2.0 data models
$oConfig->SetAddons([]);
}
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
// Record which modules are installed...
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);

View File

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

View File

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

View File

@@ -3,143 +3,11 @@
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
require_once(APPROOT.'/setup/itopextension.class.inc.php');
require_once(APPROOT.'/setup/parameters.class.inc.php');
require_once(APPROOT.'/core/cmdbsource.class.inc.php');
require_once(APPROOT.'/setup/modulediscovery.class.inc.php');
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
/**
* Basic helper class to describe an extension, with some characteristics and a list of modules
*/
class iTopExtension
{
public const SOURCE_WIZARD = 'datamodels';
public const SOURCE_MANUAL = 'extensions';
public const SOURCE_REMOTE = 'data';
/**
* @var string
*/
public $sCode;
/**
* @var string
*/
public $sVersion;
/**
* @var string
*/
public $sInstalledVersion;
/**
* @var string
*/
public $sLabel;
/**
* @var string
*/
public $sDescription;
/**
* @var string
*/
public $sSource;
/**
* @var bool
*/
public $bMandatory;
/**
* @var string
*/
public $sMoreInfoUrl;
/**
* @var bool
*/
public $bMarkedAsChosen;
/**
* If null, check if at least one module cannot be uninstalled
* @var bool|null
*/
public ?bool $bCanBeUninstalled = null;
/**
* @var bool
*/
public $bVisible;
/**
* @var string[]
*/
public $aModules;
/**
* @var string[]
*/
public $aModuleVersion;
/**
* @var string[]
*/
public $aModuleInfo;
/**
* @var string
*/
public $sSourceDir;
/**
*
* @var string[]
*/
public $aMissingDependencies;
/**
* @var bool
*/
public bool $bInstalled = false;
/**
* @var bool
*/
public bool $bRemovedFromDisk = false;
public function __construct()
{
$this->sCode = '';
$this->sLabel = '';
$this->sDescription = '';
$this->sSource = self::SOURCE_WIZARD;
$this->bMandatory = false;
$this->sMoreInfoUrl = '';
$this->bMarkedAsChosen = false;
$this->sVersion = ITOP_VERSION;
$this->sInstalledVersion = '';
$this->aModules = [];
$this->aModuleVersion = [];
$this->aModuleInfo = [];
$this->sSourceDir = '';
$this->bVisible = true;
$this->aMissingDependencies = [];
}
/**
* @since 3.3.0
* @return bool
*/
public function CanBeUninstalled(): bool
{
if (!is_null($this->bCanBeUninstalled)) {
return $this->bCanBeUninstalled;
}
foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) {
$this->bCanBeUninstalled = $aModuleInfo['uninstallable'] === 'yes';
return $this->bCanBeUninstalled;
}
return true;
}
}
/**
* Helper class to discover all available extensions on a given iTop system
@@ -148,32 +16,38 @@ class iTopExtensionsMap
{
/**
* The list of all discovered extensions
* @param string $sFromEnvironment The environment to scan
* @param bool $bNormailizeOldExtension true to "magically" convert some well-known old extensions (i.e. a set of modules) to the new iTopExtension format
* @return void
* @var array $aExtensions
*/
protected $aExtensions;
/**
* The list of all currently installed extensions
* @var array|null
* @var array $aInstalledExtensions
*/
protected ?array $aInstalledExtensions = null;
protected array $aInstalledExtensions;
protected array $aExtensionsByCode;
/**
* The list of directories browsed using the ReadDir method when building the map
* @var string[]
*/
protected $aScannedDirs;
/**
* The list of all discovered extensions
* @param string $sFromEnvironment The environment to scan
* @param bool $bNormailizeOldExtension true to "magically" convert some well-known old extensions (i.e. a set of modules) to the new iTopExtension format
* @return void
*/
public function __construct($sFromEnvironment = 'production', $aExtraDirs = [])
{
$this->aExtensions = [];
$this->aExtensionsByCode = [];
$this->aScannedDirs = [];
$this->ScanDisk($sFromEnvironment);
foreach ($aExtraDirs as $sDir) {
$this->ReadDir($sDir, iTopExtension::SOURCE_REMOTE);
}
$this->CheckDependencies($sFromEnvironment);
$this->CheckDependencies();
}
/**
@@ -184,8 +58,10 @@ class iTopExtensionsMap
*/
protected function ScanDisk($sEnvironment)
{
if (!$this->ReadInstallationWizard(APPROOT.'/datamodels/2.x') && !$this->ReadInstallationWizard(APPROOT.'/datamodels/2.x')) {
if (!$this->ReadInstallationWizard(APPROOT.'/datamodels/2.x')) {
//no installation xml found in 2.x: let's read all extensions in 2.x first
if (!$this->ReadDir(APPROOT.'/datamodels/2.x', iTopExtension::SOURCE_WIZARD)) {
//nothing found in 2.x : fallback read in 1.x (flat structure)
$this->ReadDir(APPROOT.'/datamodels/1.x', iTopExtension::SOURCE_WIZARD);
}
}
@@ -261,6 +137,7 @@ class iTopExtensionsMap
// This "new" extension is "newer" than the previous one, let's replace the previous one
unset($this->aExtensions[$key]);
$this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension;
$this->aExtensionsByCode[$oNewExtension->sCode] = $oNewExtension;
return;
} else {
// This "new" extension is not "newer" than the previous one, let's ignore it
@@ -270,6 +147,22 @@ class iTopExtensionsMap
}
// Finally it's not a duplicate, let's add it to the list
$this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension;
$this->aExtensionsByCode[$oNewExtension->sCode] = $oNewExtension;
}
public function RemoveExtension(string $sCode): void
{
$oExtension = $this->GetFromExtensionCode($sCode);
if (is_null($oExtension)) {
\IssueLog::Error(__METHOD__.": cannot find extension to remove", null, [$sCode]);
return;
}
\IssueLog::Debug(__METHOD__.": remove extension from map", null, [$oExtension->sCode => $oExtension->sSourceDir]);
unset($this->aExtensions[$oExtension->sCode.'/'.$oExtension->sVersion]);
unset($this->aExtensionsByCode[$sCode]);
}
/**
@@ -280,12 +173,28 @@ class iTopExtensionsMap
*/
public function GetFromExtensionCode(string $sExtensionCode): ?iTopExtension
{
foreach ($this->aExtensions as $oExtension) {
if ($oExtension->sCode === $sExtensionCode) {
return $oExtension;
return $this->aExtensionsByCode[$sExtensionCode] ?? null;
}
/**
* @param array<string> $aExtensionCodes
* @return void
*/
public function DeclareExtensionAsRemoved(array $aExtensionCodes): void
{
$aRemovedExtension = [];
foreach ($aExtensionCodes as $sCode) {
/** @var \iTopExtension $oExtension */
$oExtension = $this->GetFromExtensionCode($sCode);
if (!is_null($oExtension)) {
$aRemovedExtension [] = $oExtension;
\IssueLog::Info(__METHOD__.": remove extension locally", null, ['extension_code' => $oExtension->sCode]);
} else {
\IssueLog::Warning(__METHOD__." cannot find extensions", null, ['code' => $sCode]);
}
}
return null;
\ModuleDiscovery::DeclareRemovedExtensions($aRemovedExtension);
}
/**
@@ -346,19 +255,15 @@ class iTopExtensionsMap
// to this extension
$sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID];
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
if ($sModuleVersion == '') {
// Provide a default module version since version is mandatory when recording ExtensionInstallation
$sModuleVersion = '0.0.1';
}
$aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]['uninstallable'] ??= 'yes';
if (($sParentExtensionId !== null) && (array_key_exists($sParentExtensionId, $this->aExtensions)) && ($this->aExtensions[$sParentExtensionId] instanceof iTopExtension)) {
// Already inside an extension, let's add this module the list of modules belonging to this extension
$this->aExtensions[$sParentExtensionId]->aModules[] = $sModuleName;
$this->aExtensions[$sParentExtensionId]->aModuleVersion[$sModuleName] = $sModuleVersion;
$this->aExtensions[$sParentExtensionId]->aModuleInfo[$sModuleName] = $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG];
} else {
// Not already inside a folder containing an 'extension.xml' file
$oExtension = null;
if ($sParentExtensionId !== null) {
$oExtension = $this->aExtensions[$sParentExtensionId] ?? null;
}
if (is_null($oExtension)) {
// Not already inside an folder containing an 'extension.xml' file
// Ignore non-visible modules and auto-select ones, since these are never prompted
// as a choice to the end-user
@@ -382,6 +287,13 @@ class iTopExtensionsMap
$oExtension->sSourceDir = $sSearchDir;
$oExtension->bVisible = $bVisible;
$this->AddExtension($oExtension);
} else {
$oExtension->aModules[] = $sModuleName;
$oExtension->aModuleVersion[$sModuleName] = $sModuleVersion;
$oExtension->aModuleInfo[$sModuleName] = $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG];
$this->aExtensions[$sParentExtensionId] = $oExtension;
$this->aExtensionsByCode[$oExtension->sCode] = $oExtension;
}
closedir($hDir);
@@ -403,10 +315,9 @@ class iTopExtensionsMap
/**
* Check if some extension contains a module with missing dependencies...
* If so, populate the aMissingDepenencies array
* @param string $sFromEnvironment
* @return void
*/
protected function CheckDependencies($sFromEnvironment)
protected function CheckDependencies()
{
$aSearchDirs = [];
@@ -418,7 +329,7 @@ class iTopExtensionsMap
$aSearchDirs = array_merge($aSearchDirs, $this->aScannedDirs);
try {
$aAllModules = ModuleDiscovery::GetAvailableModules($aSearchDirs, true);
ModuleDiscovery::GetAvailableModules($aSearchDirs, true);
} catch (MissingDependencyException $e) {
// Some modules have missing dependencies
// Let's check what is the impact at the "extensions" level
@@ -458,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)
@@ -466,11 +446,17 @@ class iTopExtensionsMap
*/
public function MarkAsChosen($sExtensionCode, $bMark = true)
{
foreach ($this->aExtensions as $oExtension) {
if ($oExtension->sCode == $sExtensionCode) {
$oExtension->bMarkedAsChosen = $bMark;
break;
}
$oExtension = $this->GetFromExtensionCode($sExtensionCode);
if (!is_null($oExtension)) {
$oExtension->bMarkedAsChosen = $bMark;
}
}
public function MarkAsUninstallable($sExtensionCode, $bMark = true)
{
$oExtension = $this->GetFromExtensionCode($sExtensionCode);
if (!is_null($oExtension)) {
$oExtension->bUninstallable = $bMark;
}
}
@@ -481,11 +467,11 @@ class iTopExtensionsMap
*/
public function IsMarkedAsChosen($sExtensionCode)
{
foreach ($this->aExtensions as $oExtension) {
if ($oExtension->sCode == $sExtensionCode) {
return $oExtension->bMarkedAsChosen;
}
$oExtension = $this->GetFromExtensionCode($sExtensionCode);
if (!is_null($oExtension)) {
return $oExtension->bMarkedAsChosen;
}
return false;
}
@@ -497,11 +483,9 @@ class iTopExtensionsMap
*/
protected function SetInstalledVersion($sExtensionCode, $sInstalledVersion)
{
foreach ($this->aExtensions as $oExtension) {
if ($oExtension->sCode == $sExtensionCode) {
$oExtension->sInstalledVersion = $sInstalledVersion;
break;
}
$oExtension = $this->GetFromExtensionCode($sExtensionCode);
if (!is_null($oExtension)) {
$oExtension->sInstalledVersion = $sInstalledVersion;
}
}
@@ -527,14 +511,19 @@ class iTopExtensionsMap
*/
public function LoadChoicesFromDatabase(Config $oConfig)
{
foreach ($this->LoadInstalledExtensionsFromDatabase($oConfig) as $oExtension) {
$aLoadInstalledExtensionsFromDatabase = $this->LoadInstalledExtensionsFromDatabase($oConfig);
if (false === $aLoadInstalledExtensionsFromDatabase) {
return false;
}
foreach ($aLoadInstalledExtensionsFromDatabase as $oExtension) {
$this->MarkAsChosen($oExtension->sCode);
$this->SetInstalledVersion($oExtension->sCode, $oExtension->sVersion);
}
return true;
}
protected function LoadInstalledExtensionsFromDatabase(Config $oConfig): array|false
public function LoadInstalledExtensionsFromDatabase(Config $oConfig): array|false
{
try {
if (CMDBSource::DBName() === null) {
@@ -585,8 +574,6 @@ class iTopExtensionsMap
*/
public function ModuleIsChosenAsPartOfAnExtension($sModuleNameToFind, $sInSourceOnly = iTopExtension::SOURCE_REMOTE)
{
$bChosen = false;
foreach ($this->GetAllExtensions() as $oExtension) {
if (($oExtension->sSource == $sInSourceOnly) &&
($oExtension->bMarkedAsChosen == true) &&

View File

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

View File

@@ -0,0 +1,65 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use iTopExtensionsMap;
use MetaModel;
use RunTimeEnvironment;
use SetupUtils;
class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
{
public const DRY_REMOVAL_AUDIT_ENV = "extension-removal";
protected array $aExtensionsByCode;
/**
* Toolset for building a run-time environment
*
* @param string $sEnvironment (e.g. 'test')
* @param bool $bAutoCommit (make the target environment directly, or build a temporary one)
*/
public function __construct($sEnvironment = self::DRY_REMOVAL_AUDIT_ENV, $bAutoCommit = true)
{
parent::__construct($sEnvironment, $bAutoCommit);
$this->aExtensionsByCode = [];
}
/**
* @param string $sSourceEnv
* @param array $aExtensionCodesToRemove
*
* @return void
* @throws \Exception
*/
public function Prepare(string $sSourceEnv, array $aExtensionCodesToRemove)
{
$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);
}
private function DeclareExtensionAsRemoved(array $aExtensionCodes): void
{
$oExtensionsMap = new iTopExtensionsMap($this->sFinalEnv);
$oExtensionsMap->DeclareExtensionAsRemoved($aExtensionCodes);
}
public function Cleanup()
{
$sEnv = $this->sFinalEnv;
SetupUtils::rrmdir(APPROOT."/data/$sEnv-modules");
SetupUtils::rrmdir(APPROOT."/data/cache-$sEnv");
SetupUtils::rrmdir(APPROOT."/env-$sEnv");
SetupUtils::rrmdir(APPROOT."/conf/$sEnv");
@unlink(APPROOT."/data/datamodel-$sEnv.xml");
}
}

View File

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

View File

@@ -0,0 +1,70 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use ContextTag;
use CoreException;
use Exception;
use IssueLog;
use SetupLog;
use utils;
class ModelReflectionSerializer
{
private static ModelReflectionSerializer $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): ModelReflectionSerializer
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModelReflectionSerializer();
}
return self::$oInstance;
}
final public static function SetInstance(?ModelReflectionSerializer $oInstance): void
{
self::$oInstance = $oInstance;
}
public function GetModelFromEnvironment(string $sEnv): array
{
IssueLog::Info(__METHOD__, null, ['env' => $sEnv]);
$sPHPExec = trim(utils::GetConfig()->Get('php_path'));
$sOutput = "";
$iRes = 0;
exec(sprintf("$sPHPExec %s/get_model_reflection.php --env='%s'", __DIR__, $sEnv), $sOutput, $iRes);
if ($iRes != 0) {
$this->LogErrorWithProperLogger("Cannot get classes", null, ['env' => $sEnv, 'code' => $iRes, "output" => $sOutput]);
throw new CoreException("Cannot get classes");
}
$aClasses = json_decode($sOutput[0] ?? null, true);
if (false === $aClasses) {
$this->LogErrorWithProperLogger("Invalid JSON", null, ['env' => $sEnv, "output" => $sOutput]);
throw new Exception("cannot get classes");
}
if (!is_array($aClasses)) {
$this->LogErrorWithProperLogger("not an array", null, ['env' => $sEnv, "classes" => $aClasses, "output" => $sOutput]);
throw new Exception("cannot get classes from $sEnv");
}
return $aClasses;
}
//could be shared with others in log APIs ?
private function LogErrorWithProperLogger($sMessage, $sChannel = null, $aContext = []): void
{
if (ContextTag::Check(ContextTag::TAG_SETUP)) {
SetupLog::Error($sMessage, $sChannel, $aContext);
} else {
IssueLog::Error($sMessage, $sChannel, $aContext);
}
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use MetaModel;
require_once __DIR__.'/AbstractSetupAudit.php';
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
class SetupAudit extends AbstractSetupAudit
{
//file used when present to trigger audit exception when testing specific setups
public const GETISSUE_ERROR_MSG_FILE_FORTESTONLY = '.setup_audit_error_msg.txt';
private string $sEnvBefore;
private string $sEnvAfter;
public function __construct(string $sEnvBefore, string $sEnvAfter)
{
parent::__construct();
$this->sEnvBefore = $sEnvBefore;
$this->sEnvAfter = $sEnvAfter;
}
public function ComputeClasses(): void
{
if ($this->bClassesInitialized) {
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->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();
if (count($this->aRemovedClasses) == 0) {
if (count($this->aClassesBefore) == 0) {
return $this->aRemovedClasses;
}
if (count($this->aClassesAfter) == 0) {
return $this->aRemovedClasses;
}
$aExtensionsNames = array_diff($this->aClassesBefore, $this->aClassesAfter);
$this->aRemovedClasses = [];
$aClasses = array_values($aExtensionsNames);
sort($aClasses);
foreach ($aClasses as $i => $sClass) {
$this->aRemovedClasses[] = $sClass;
}
}
return $this->aRemovedClasses;
}
}

View File

@@ -0,0 +1,41 @@
<?php
require_once(dirname(__DIR__, 2).'/approot.inc.php');
require_once(APPROOT.'application/application.inc.php');
$sEnv = null;
if (isset($argv)) {
foreach ($argv as $iArg => $sArg) {
if (preg_match('/^--env=(.*)$/', $sArg, $aMatches)) {
$sEnv = $aMatches[1];
}
}
}
if (is_null($sEnv)) {
echo "No environment provided (--env) to read datamodel.";
exit(1);
}
$sConfFile = utils::GetConfigFilePath($sEnv);
try {
MetaModel::Startup($sConfFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
} catch (\Throwable $e) {
echo $e->getMessage();
echo $e->getTraceAsString();
\SetupLog::Error(
"Cannot read model from provided environment",
null,
[
'env' => $sEnv,
'error' => $e->getMessage(),
'stack' => $e->getTraceAsString(),
]
);
echo "Cannot read model from provided environment";
exit(1);
}
$aClasses = MetaModel::GetClasses();
echo json_encode($aClasses);

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,13 +22,13 @@
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use Combodo\iTop\Setup\ModuleDependency\Module;
use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
require_once(APPROOT.'setup/modulediscovery/ModuleFileReader.php');
require_once(__DIR__.'/moduledependency/moduledependencysort.class.inc.php');
use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort;
require_once(__DIR__.'/itopextension.class.inc.php');
class MissingDependencyException extends CoreException
{
@@ -95,6 +95,9 @@ class ModuleDiscovery
protected static $m_aModules = [];
protected static $m_aModuleVersionByName = [];
/** @var array<\iTopExtension> $m_aRemovedExtensions */
protected static $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'];
@@ -120,10 +123,6 @@ class ModuleDiscovery
if (is_null($aArgs) || ! is_array($aArgs)) {
throw new ModuleFileReaderException("Error parsing module file args", 0, null, $sFilePath);
}
if (!array_key_exists('itop_version', $aArgs)) {
// Assume 1.0.2
$aArgs['itop_version'] = '1.0.2';
}
foreach (array_keys(self::$m_aModuleArgs) as $sArgName) {
if (!array_key_exists($sArgName, $aArgs)) {
throw new Exception("Module '$sId': missing argument '$sArgName'");
@@ -135,6 +134,10 @@ class ModuleDiscovery
list($sModuleName, $sModuleVersion) = static::GetModuleName($sId);
if (self::IsModuleInExtensionList(self::$m_aRemovedExtensions, $sModuleName, $sModuleVersion, $aArgs)) {
return;
}
if (array_key_exists($sModuleName, self::$m_aModuleVersionByName)) {
if (version_compare($sModuleVersion, self::$m_aModuleVersionByName[$sModuleName]['version'], '>')) {
// Newer version, let's upgrade
@@ -218,29 +221,97 @@ class ModuleDiscovery
*/
public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
if (is_null($aModulesToLoad)) {
if (is_null($aModulesToLoad) && count(self::$m_aRemovedExtensions) === 0) {
$aFilteredModules = $aModules;
} else {
$aFilteredModules = [];
foreach ($aModules as $sModuleId => $aModule) {
foreach ($aModules as $sModuleId => $aModuleInfo) {
$oModule = new Module($sModuleId);
$sModuleName = $oModule->GetModuleName();
if (in_array($sModuleName, $aModulesToLoad)) {
$aFilteredModules[$sModuleId] = $aModule;
if (self::IsModuleInExtensionList(self::$m_aRemovedExtensions, $sModuleName, $oModule->GetVersion(), $aModuleInfo)) {
continue;
}
if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad)) {
$aFilteredModules[$sModuleId] = $aModuleInfo;
}
}
}
return ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aFilteredModules, $bAbortOnMissingDependency);
}
/**
* @param array<\iTopExtension> $aRemovedExtension
* @return void
*/
public static function DeclareRemovedExtensions(array $aRemovedExtension)
{
if (self::$m_aRemovedExtensions != $aRemovedExtension) {
self::ResetCache();
}
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::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;
}
private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
{
if (!isset(static::$oPhpExpressionEvaluator)) {
static::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
if (!isset(self::$oPhpExpressionEvaluator)) {
self::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
}
return static::$oPhpExpressionEvaluator;
return self::$oPhpExpressionEvaluator;
}
/**
@@ -289,10 +360,12 @@ class ModuleDiscovery
/**
* Helper function to interpret the name of a module
*
* @param $sModuleId string Identifier of the module, in the form 'name/version'
* @return array(name, version)
*
* @return array of 2 elements (name, version)
*/
public static function GetModuleName($sModuleId)
public static function GetModuleName($sModuleId): array
{
$aMatches = [];
if (preg_match('!^(.*)/(.*)$!', $sModuleId, $aMatches)) {

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ require_once APPROOT."setup/modulediscovery.class.inc.php";
require_once APPROOT.'setup/modelfactory.class.inc.php';
require_once APPROOT.'setup/compiler.class.inc.php';
require_once APPROOT.'setup/extensionsmap.class.inc.php';
require_once APPROOT.'setup/AnalyzeInstallation.php';
define('MODULE_ACTION_OPTIONAL', 1);
define('MODULE_ACTION_MANDATORY', 2);
@@ -61,7 +62,23 @@ class RunTimeEnvironment
* Extensions map of the source environment
* @var iTopExtensionsMap
*/
protected $oExtensionsMap;
protected ?iTopExtensionsMap $oExtensionsMap;
protected function GetExtensionMap(): ?iTopExtensionsMap
{
return $this->oExtensionsMap;
}
public function InitExtensionMap($aExtraDirs, $oSourceConfig)
{
// Actually read the modules available for the target environment,
// but get the selection from the source environment and finally
// mark as (automatically) chosen alll the "remote" modules present in the
// target environment (data/<target-env>-modules)
// The actual choices will be recorded by RecordInstallation below
$this->oExtensionsMap = new iTopExtensionsMap($this->sTargetEnv, $aExtraDirs);
$this->oExtensionsMap->LoadChoicesFromDatabase($oSourceConfig);
}
/**
* Toolset for building a run-time environment
@@ -109,9 +126,8 @@ class RunTimeEnvironment
* from the given file
* @param $oConfig object The configuration (volatile, not necessarily already on disk)
* @param $bModelOnly boolean Whether or not to allow loading a data model with no corresponding DB
* @return none
*/
public function InitDataModel($oConfig, $bModelOnly = true, $bUseCache = false)
public function InitDataModel($oConfig, $bModelOnly = true, $bUseCache = false): void
{
require_once APPROOT.'/setup/moduleinstallation.class.inc.php';
@@ -145,12 +161,12 @@ class RunTimeEnvironment
* @return array Array with the following format:
* array =>
* 'iTop' => array(
* 'version_db' => ... (could be empty in case of a fresh install)
* 'version_code => ...
* 'installed_version' => ... (could be empty in case of a fresh install)
* 'available_version => ...
* )
* <module_name> => array(
* 'version_db' => ...
* 'version_code' => ...
* 'installed_version' => ...
* 'available_version' => ...
* 'install' => array(
* 'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
* 'message' => ...
@@ -168,137 +184,7 @@ class RunTimeEnvironment
*/
public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
$aRes = [
ROOT_MODULE => [
'version_db' => '',
'name_db' => '',
'version_code' => ITOP_VERSION_FULL,
'name_code' => ITOP_APPLICATION,
],
];
$aDirs = is_array($modulesPath) ? $modulesPath : [$modulesPath];
$aModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
foreach ($aModules as $sModuleId => $aModuleInfo) {
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
if ($sModuleName == '') {
throw new Exception("Missing name for the module: '$sModuleId'");
}
if ($sModuleVersion == '') {
// The version must not be empty (it will be used as a criteria to determine wether a module has been installed or not)
//throw new Exception("Missing version for the module: '$sModuleId'");
$sModuleVersion = '1.0.0';
}
$sModuleAppVersion = $aModuleInfo['itop_version'];
$aModuleInfo['version_db'] = '';
$aModuleInfo['version_code'] = $sModuleVersion;
if (!in_array($sModuleAppVersion, ['1.0.0', '1.0.1', '1.0.2'])) {
// This module is NOT compatible with the current version
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_IMPOSSIBLE,
'message' => 'the module is not compatible with the current version of the application',
];
} elseif ($aModuleInfo['mandatory']) {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_MANDATORY,
'message' => 'the module is part of the application',
];
} else {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
$aRes[$sModuleName] = $aModuleInfo;
}
try {
$aSelectInstall = [];
if (! is_null($oConfig)) {
CMDBSource::InitFromConfig($oConfig);
$aSelectInstall = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install");
}
} catch (MySQLException $e) {
// No database or erroneous information
}
// Build the list of installed module (get the latest installation)
//
$aInstallByModule = []; // array of <module> => array ('installed' => timestamp, 'version' => <version>)
$iRootId = 0;
foreach ($aSelectInstall as $aInstall) {
if (($aInstall['parent_id'] == 0) && ($aInstall['name'] != 'datamodel')) {
// Root module, what is its ID ?
$iId = (int) $aInstall['id'];
if ($iId > $iRootId) {
$iRootId = $iId;
}
}
}
foreach ($aSelectInstall as $aInstall) {
//$aInstall['comment']; // unsused
$iInstalled = strtotime($aInstall['installed']);
$sModuleName = $aInstall['name'];
$sModuleVersion = $aInstall['version'];
if ($sModuleVersion == '') {
// Though the version cannot be empty in iTop 2.0, it used to be possible
// therefore we have to put something here or the module will not be considered
// as being installed
$sModuleVersion = '0.0.0';
}
if ($aInstall['parent_id'] == 0) {
$sModuleName = ROOT_MODULE;
} elseif ($aInstall['parent_id'] != $iRootId) {
// Skip all modules belonging to previous installations
continue;
}
if (array_key_exists($sModuleName, $aInstallByModule)) {
if ($iInstalled < $aInstallByModule[$sModuleName]['installed']) {
continue;
}
}
if ($aInstall['parent_id'] == 0) {
$aRes[$sModuleName]['version_db'] = $sModuleVersion;
$aRes[$sModuleName]['name_db'] = $aInstall['name'];
}
$aInstallByModule[$sModuleName]['installed'] = $iInstalled;
$aInstallByModule[$sModuleName]['version'] = $sModuleVersion;
}
// Adjust the list of proposed modules
//
foreach ($aInstallByModule as $sModuleName => $aModuleDB) {
if ($sModuleName == ROOT_MODULE) {
continue;
} // Skip the main module
if (!array_key_exists($sModuleName, $aRes)) {
// A module was installed, it is not proposed in the new build... skip
continue;
}
$aRes[$sModuleName]['version_db'] = $aModuleDB['version'];
if ($aRes[$sModuleName]['install']['flag'] == MODULE_ACTION_MANDATORY) {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_IMPOSSIBLE,
'message' => 'the module is part of the application',
];
} else {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
}
return $aRes;
return AnalyzeInstallation::GetInstance()->AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDependency, $aModulesToLoad);
}
/**
@@ -344,6 +230,7 @@ class RunTimeEnvironment
*/
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)");
@@ -360,30 +247,29 @@ class RunTimeEnvironment
$aExtraDirs = $this->GetExtraDirsToScan($aDirsToCompile);
$aDirsToCompile = array_merge($aDirsToCompile, $aExtraDirs);
$aRet = [];
// Determine the installed modules and extensions
//
$oSourceConfig = new Config(APPCONF.$sSourceEnv.'/'.ITOP_CONFIG_FILE);
$oSourceEnv = new RunTimeEnvironment($sSourceEnv);
$aAvailableModules = $oSourceEnv->AnalyzeInstallation($oSourceConfig, $aDirsToCompile);
$aAvailableModules = $this->AnalyzeInstallation($oSourceConfig, $aDirsToCompile);
// Actually read the modules available for the target environment,
// but get the selection from the source environment and finally
// mark as (automatically) chosen alll the "remote" modules present in the
// mark as (automatically) chosen all the "remote" modules present in the
// target environment (data/<target-env>-modules)
// The actual choices will be recorded by RecordInstallation below
$this->oExtensionsMap = new iTopExtensionsMap($this->sTargetEnv, $aExtraDirs);
$this->oExtensionsMap->LoadChoicesFromDatabase($oSourceConfig);
foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) {
$this->InitExtensionMap($aExtraDirs, $oSourceConfig);
$this->GetExtensionMap()->LoadChoicesFromDatabase($oSourceConfig);
foreach ($this->GetExtensionMap()->GetAllExtensions() as $oExtension) {
if ($this->IsExtensionSelected($oExtension)) {
$this->oExtensionsMap->MarkAsChosen($oExtension->sCode);
$this->GetExtensionMap()->MarkAsChosen($oExtension->sCode);
}
}
// Do load the required modules
//
$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
$aRet = [];
$aRet[$oDictModule->GetName()] = $oDictModule;
$oFactory = new ModelFactory($aDirsToCompile);
@@ -401,10 +287,9 @@ class RunTimeEnvironment
$aModules = $oFactory->FindModules();
foreach ($aModules as $oModule) {
$sModule = $oModule->GetName();
$sModuleRootDir = $oModule->GetRootDir();
$bIsExtra = $this->oExtensionsMap->ModuleIsChosenAsPartOfAnExtension($sModule, iTopExtension::SOURCE_REMOTE);
$bIsExtra = $this->GetExtensionMap()->ModuleIsChosenAsPartOfAnExtension($sModule, iTopExtension::SOURCE_REMOTE);
if (array_key_exists($sModule, $aAvailableModules)) {
if (($aAvailableModules[$sModule]['version_db'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) { //Extra modules are always unless they are 'AutoSelect'
if (($aAvailableModules[$sModule]['installed_version'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) { //Extra modules are always unless they are 'AutoSelect'
$aRet[$oModule->GetName()] = $oModule;
}
}
@@ -462,6 +347,7 @@ class RunTimeEnvironment
//
$oFactory = new ModelFactory($sSourceDirFull);
$aModulesToCompile = $this->GetMFModulesToCompile($sSourceEnv, $sSourceDir);
$oModule = null;
foreach ($aModulesToCompile as $oModule) {
if ($oModule instanceof MFDeltaModule) {
// Just before loading the delta, let's save an image of the datamodel
@@ -471,7 +357,7 @@ class RunTimeEnvironment
$oFactory->LoadModule($oModule);
}
if ($oModule instanceof MFDeltaModule) {
if (!is_null($oModule) && ($oModule instanceof MFDeltaModule)) {
// A delta was loaded, let's save a second copy of the datamodel
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sTargetEnv.'-with-delta.xml');
} else {
@@ -648,7 +534,7 @@ class RunTimeEnvironment
$oInstallRec->Set('comment', json_encode($aData));
$oInstallRec->Set('parent_id', 0); // root module
$oInstallRec->Set('installed', $iInstallationTime);
$iMainItopRecord = $oInstallRec->DBInsertNoReload();
$oInstallRec->DBInsertNoReload();
// Record main installation
$oInstallRec = new ModuleInstallation();
@@ -661,15 +547,19 @@ class RunTimeEnvironment
// Record installed modules and extensions
//
$aAvailableExtensions = [];
$aAvailableModules = $this->AnalyzeInstallation($oConfig, $this->GetBuildDir());
foreach ($aSelectedModuleCodes as $sModuleId) {
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
}
if (!array_key_exists($sModuleId, $aAvailableModules)) {
continue;
}
$aModuleData = $aAvailableModules[$sModuleId];
$sName = $sModuleId;
$sVersion = $aModuleData['version_code'];
$sVersion = $aModuleData['available_version'];
$sUninstallable = $aModuleData['uninstallable'] ?? 'yes';
$aComments = [];
$aComments[] = $sShortComment;
@@ -702,16 +592,17 @@ class RunTimeEnvironment
$oInstallRec->DBInsertNoReload();
}
if ($this->oExtensionsMap) {
if ($this->GetExtensionMap()) {
// Mark as chosen the selected extensions code passed to us
// Note: some other extensions may already be marked as chosen
foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) {
foreach ($this->GetExtensionMap()->GetAllExtensions() as $oExtension) {
if (in_array($oExtension->sCode, $aSelectedExtensionCodes)) {
$this->oExtensionsMap->MarkAsChosen($oExtension->sCode);
$this->GetExtensionMap()->MarkAsChosen($oExtension->sCode);
}
}
foreach ($this->oExtensionsMap->GetChoices() as $oExtension) {
foreach ($this->GetExtensionMap()->GetChoices() as $oExtension) {
$oInstallRec = new ExtensionInstallation();
$oInstallRec->Set('code', $oExtension->sCode);
$oInstallRec->Set('label', $oExtension->sLabel);
@@ -739,9 +630,7 @@ class RunTimeEnvironment
public function GetApplicationVersion(Config $oConfig)
{
try {
CMDBSource::InitFromConfig($oConfig);
$sSQLQuery = "SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install";
$aSelectInstall = CMDBSource::QueryToArray($sSQLQuery);
$aSelectInstall = ModuleInstallationRepository::GetInstance()->ReadFromDB($oConfig);
} catch (MySQLException $e) {
// No database or erroneous information
$this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name'));
@@ -779,7 +668,8 @@ class RunTimeEnvironment
$aResult['datamodel_version'] = $aResult['product_version'];
}
$this->log_info("GetApplicationVersion returns: product_name: ".$aResult['product_name'].', product_version: '.$aResult['product_version']);
return empty($aResult) ? false : $aResult;
return count($aResult) == 0 ? false : $aResult;
}
public static function MakeDirSafe($sDir)
@@ -967,16 +857,20 @@ class RunTimeEnvironment
/**
* Call the given handler method for all selected modules having an installation handler
* @param array[] $aAvailableModules
* @param string[] $aSelectedModules
* @param string $sHandlerName
* @param string[]|null $aSelectedModules
* @throws CoreException
*/
public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sHandlerName)
public function CallInstallerHandlers($aAvailableModules, $sHandlerName, $aSelectedModules = null)
{
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules)) {
$aArgs = [MetaModel::GetConfig(), $aModule['version_db'], $aModule['version_code']];
RunTimeEnvironment::CallInstallerHandler($aAvailableModules[$sModuleId], $sHandlerName, $aArgs);
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
}
if (is_null($aSelectedModules) || in_array($sModuleId, $aSelectedModules)) {
$aArgs = [MetaModel::GetConfig(), $aModule['installed_version'], $aModule['available_version']];
RunTimeEnvironment::CallInstallerHandler($aModule, $sHandlerName, $aArgs);
}
}
}
@@ -1003,6 +897,7 @@ class RunTimeEnvironment
try {
call_user_func_array($aCallSpec, $aArgs);
} catch (Exception $e) {
$sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID] ?? "";
$sErrorMessage = "Module $sModuleId : error when calling module installer class $sModuleInstallerClass for $sHandlerName handler";
$aExceptionContextData = [
'ModulelId' => $sModuleId,
@@ -1019,10 +914,10 @@ class RunTimeEnvironment
/**
* Load data from XML files for the selected modules (structural data and/or sample data)
* @param array[] $aAvailableModules All available modules and their definition
* @param string[] $aSelectedModules List of selected modules
* @param bool $bSampleData Wether or not to load sample data
* @param null|string[] $aSelectedModules List of selected modules
*/
public function LoadData($aAvailableModules, $aSelectedModules, $bSampleData)
public function LoadData($aAvailableModules, $bSampleData, $aSelectedModules = null)
{
$oDataLoader = new XMLDataLoader();
@@ -1035,30 +930,33 @@ class RunTimeEnvironment
$aFiles = [];
$aPreviouslyLoadedFiles = [];
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId != ROOT_MODULE)) {
$sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']);
// Load data only for selected AND newly installed modules
if (in_array($sModuleId, $aSelectedModules)) {
if ($aModule['version_db'] != '') {
// Simulate the load of the previously loaded XML files to get the mapping of the keys
if ($bSampleData) {
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
} else {
// Load only structural data
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
}
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
}
$sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']);
// Load data only for selected AND newly installed modules
if (is_null($aSelectedModules) || in_array($sModuleId, $aSelectedModules)) {
if ($aModule['installed_version'] != '') {
// Simulate the load of the previously loaded XML files to get the mapping of the keys
if ($bSampleData) {
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
} else {
if ($bSampleData) {
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
} else {
// Load only structural data
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
}
// Load only structural data
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
}
} else {
if ($bSampleData) {
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
} else {
// Load only structural data
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
}
}
}
}
// Simulate the load of the previously loaded files, in order to initialize
@@ -1067,7 +965,7 @@ class RunTimeEnvironment
foreach ($aPreviouslyLoadedFiles as $sFileRelativePath) {
$sFileName = APPROOT.$sFileRelativePath;
SetupLog::Info("Loading file: $sFileName (just to get the keys mapping)");
if (empty($sFileName) || !file_exists($sFileName)) {
if (!file_exists($sFileName)) {
throw(new Exception("File $sFileName does not exist"));
}
@@ -1079,7 +977,7 @@ class RunTimeEnvironment
foreach ($aFiles as $sFileRelativePath) {
$sFileName = APPROOT.$sFileRelativePath;
SetupLog::Info("Loading file: $sFileName");
if (empty($sFileName) || !file_exists($sFileName)) {
if (!file_exists($sFileName)) {
throw(new Exception("File $sFileName does not exist"));
}

View File

@@ -254,32 +254,23 @@ class SetupUtils
if (!utils::IsModeCLI()) {
$sUploadTmpDir = self::GetUploadTmpDir();
if (empty($sUploadTmpDir)) {
$sUploadTmpDir = '/tmp';
$aResult[] = new CheckResult(
CheckResult::WARNING,
"Temporary directory for files upload is not defined (upload_tmp_dir), assuming that $sUploadTmpDir is used."
);
}
// check that the upload directory is indeed writable from PHP
if (!empty($sUploadTmpDir)) {
if (!file_exists($sUploadTmpDir)) {
if (!file_exists($sUploadTmpDir)) {
$aResult[] = new CheckResult(
CheckResult::ERROR,
"Temporary directory for files upload ($sUploadTmpDir) does not exist or cannot be read by PHP."
);
} else {
if (!is_writable($sUploadTmpDir)) {
$aResult[] = new CheckResult(
CheckResult::ERROR,
"Temporary directory for files upload ($sUploadTmpDir) does not exist or cannot be read by PHP."
"Temporary directory for files upload ($sUploadTmpDir) is not writable."
);
} else {
if (!is_writable($sUploadTmpDir)) {
$aResult[] = new CheckResult(
CheckResult::ERROR,
"Temporary directory for files upload ($sUploadTmpDir) is not writable."
);
} else {
$aResult[] = new CheckResult(
CheckResult::TRACE,
"Info - Temporary directory for files upload ($sUploadTmpDir) is writable."
);
}
$aResult[] = new CheckResult(
CheckResult::TRACE,
"Info - Temporary directory for files upload ($sUploadTmpDir) is writable."
);
}
}
}
@@ -599,7 +590,7 @@ class SetupUtils
// create and test destination location
//
$sDestDir = dirname($sDBBackupPath);
setuputils::builddir($sDestDir);
SetupUtils::builddir($sDestDir);
if (!is_dir($sDestDir)) {
$aResult[] = new CheckResult(CheckResult::ERROR, "$sDestDir does not exist and could not be created.");
}
@@ -1555,17 +1546,8 @@ JS
return $sHtml;
}
/**
* @param \WizardController $oWizard
* @param bool $bAbortOnMissingDependency ...
* @param array $aModulesToLoad List of modules to search for, defaults to all if ommitted
*
* @return array
* @throws Exception
*/
public static function AnalyzeInstallation($oWizard, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
public static function GetConfig(WizardController $oWizard)
{
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
$oConfig = new Config();
$sSourceDir = $oWizard->GetParameter('source_dir', '');
@@ -1579,8 +1561,26 @@ JS
$aParamValues = $oWizard->GetParamForConfigArray();
$aParamValues['source_dir'] = $sRelativeSourceDir;
$oConfig->UpdateFromParams($aParamValues, null);
$aDirsToScan = [$sSourceDir];
$oConfig->UpdateFromParams($aParamValues);
return $oConfig;
}
/**
* @param \WizardController $oWizard
* @param bool $bAbortOnMissingDependency ...
* @param array $aModulesToLoad List of modules to search for, defaults to all if ommitted
*
* @return array
* @throws Exception
*/
public static function AnalyzeInstallation($oWizard, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
$oConfig = self::GetConfig($oWizard);
$aDirsToScan = [$oWizard->GetParameter('source_dir', '')];
if (is_dir(APPROOT.'extensions')) {
$aDirsToScan[] = APPROOT.'extensions';
@@ -1593,6 +1593,10 @@ JS
$aDirsToScan[] = $sExtraDir;
}
$oProductionEnv = new RunTimeEnvironment();
$aRemovedExtensionCodes = json_decode($oWizard->GetParameter('removed_extensions'), true) ?? [];
$oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan);
$oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes);
$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, $aDirsToScan, $bAbortOnMissingDependency, $aModulesToLoad);
foreach ($aAvailableModules as $key => $aModule) {
@@ -1618,7 +1622,7 @@ JS
$aParamValues = $oWizard->GetParamForConfigArray();
$aParamValues['source_dir'] = '';
$oConfig->UpdateFromParams($aParamValues, null);
$oConfig->UpdateFromParams($aParamValues);
$oProductionEnv = new RunTimeEnvironment();
return $oProductionEnv->GetApplicationVersion($oConfig);

View File

@@ -71,12 +71,12 @@ class InstallationFileService
return $this->aAfterComputationSelectedExtensions;
}
public function SetItopExtensionsMap(ItopExtensionsMap $oItopExtensionsMap): void
public function SetItopExtensionsMap(iTopExtensionsMap $oItopExtensionsMap): void
{
$this->oItopExtensionsMap = $oItopExtensionsMap;
}
public function GetItopExtensionsMap(): ItopExtensionsMap
public function GetItopExtensionsMap(): iTopExtensionsMap
{
if (is_null($this->oItopExtensionsMap)) {
$this->oItopExtensionsMap = new iTopExtensionsMap($this->sTargetEnvironment);

View File

@@ -16,7 +16,6 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
use Combodo\iTop\Application\WebPage\WebPage;
/**
@@ -402,18 +401,6 @@ abstract class WizardStep
* @return void
*/
abstract public function Display(WebPage $oPage);
/**
* Displays the wizard page for the current class/state
* return UIBlock
* 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 \Combodo\iTop\Application\UI\Base\UIBlock
* @since 3.0.0
*/
public function DisplayBlock(WebPage $oPage)
{
return new Html($this->Display($oPage));
}
/**
* Processes the page's parameters and (if moving forward) returns the next step/state to be displayed

View File

@@ -671,9 +671,13 @@ class WizStepLicense extends WizardStep
private function NeedsGdprConsent()
{
$sMode = $this->oWizard->GetParameter('install_mode');
$aModules = SetupUtils::AnalyzeInstallation($this->oWizard);
return (($sMode === 'install') && SetupUtils::IsConnectableToITopHub($aModules));
if ($sMode !== 'install') {
return false;
}
$aModules = SetupUtils::AnalyzeInstallation($this->oWizard);
return SetupUtils::IsConnectableToITopHub($aModules);
}
/**
@@ -1321,6 +1325,8 @@ class WizStepModulesChoice extends WizardStep
*/
protected iTopExtensionsMap $oExtensionsMap;
private ?array $aSteps = null;
protected PhpExpressionEvaluator $oPhpExpressionEvaluator;
/**
@@ -1329,6 +1335,9 @@ class WizStepModulesChoice extends WizardStep
*/
protected bool $bChoicesFromDatabase;
private array $aAnalyzeInstallationModules;
private ?MissingDependencyException $oMissingDependencyException = null;
public function __construct(WizardController $oWizard, $sCurrentState)
{
parent::__construct($oWizard, $sCurrentState);
@@ -1353,13 +1362,21 @@ class WizStepModulesChoice extends WizardStep
$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()
public function GetTitle(): string
{
$aStepInfo = $this->GetStepInfo();
$sTitle = isset($aStepInfo['title']) ? $aStepInfo['title'] : 'Modules selection';
return $sTitle;
return $aStepInfo['title'] ?? 'Modules selection';
}
public function GetPossibleSteps()
@@ -1367,6 +1384,27 @@ class WizStepModulesChoice extends WizardStep
return ['WizStepModulesChoice', 'WizStepSummary'];
}
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 ProcessParams($bMoveForward = true)
{
// Accumulates the selected modules:
@@ -1396,9 +1434,14 @@ class WizStepModulesChoice extends WizardStep
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)));
return ['class' => 'WizStepSummary', 'state' => ''];
}
@@ -1418,10 +1461,8 @@ class WizStepModulesChoice extends WizardStep
protected function DisplayStep($oPage)
{
// Sanity check (not stopper, to let developers go further...)
try {
SetupUtils::AnalyzeInstallation($this->oWizard, true);
} catch (MissingDependencyException $e) {
$oPage->warning($e->getHtmlDesc(), $e->getMessage());
if (! is_null($this->oMissingDependencyException)) {
$oPage->warning($this->oMissingDependencyException->getHtmlDesc(), $this->oMissingDependencyException->getMessage());
}
$this->bUpgrade = ($this->oWizard->GetParameter('install_mode') != 'install');
@@ -1432,9 +1473,8 @@ class WizStepModulesChoice extends WizardStep
$oPage->add_style(".choice-disabled { color: #999; }");
$oPage->add_style("input.unremovable { accent-color: orangered;}");
$aModules = SetupUtils::AnalyzeInstallation($this->oWizard);
$sManualInstallError = SetupUtils::CheckManualInstallDirEmpty(
$aModules,
$this->aAnalyzeInstallationModules,
$this->oWizard->GetParameter('extensions_dir', 'extensions')
);
if ($sManualInstallError !== '') {
@@ -1460,7 +1500,7 @@ class WizStepModulesChoice extends WizardStep
$oPage->add('</div>');
// Build the default choices
$aDefaults = $this->GetDefaults($aStepInfo, $aModules);
$aDefaults = $this->GetDefaults($aStepInfo, $this->aAnalyzeInstallationModules);
$index = $this->GetStepIndex();
// retrieve the saved selection
@@ -1561,7 +1601,7 @@ EOF
}
}
$aAlternatives = isset($aInfo['alternatives']) ? $aInfo['alternatives'] : [];
$aAlternatives = $aInfo['alternatives'] ?? [];
$sChoiceName = null;
foreach ($aAlternatives as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
@@ -1605,7 +1645,7 @@ EOF
if ($this->bUpgrade) {
// In upgrade mode, the defaults are the installed modules
foreach ($aChoice['modules'] as $sModuleId) {
if ($aModules[$sModuleId]['version_db'] != '') {
if ($aModules[$sModuleId]['installed_version'] != '') {
// A module corresponding to this choice is installed
$aScores[$sChoiceId][$sModuleId] = true;
}
@@ -1663,7 +1703,7 @@ EOF
}
if (array_key_exists('modules', $aChoice)) {
foreach ($aChoice['modules'] as $sModuleId) {
if ($aModules[$sModuleId]['version_db'] != '') {
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] = [];
@@ -1720,7 +1760,7 @@ EOF
{
if ($sParentId == '') {
// Check once (before recursing) that the hidden modules are selected
foreach (SetupUtils::AnalyzeInstallation($this->oWizard) as $sModuleId => $aModule) {
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;
@@ -1810,11 +1850,10 @@ EOF
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
$aAvailableModules = SetupUtils::AnalyzeInstallation($this->oWizard);
do {
// Loop while new modules are added...
$bModuleAdded = false;
foreach ($aAvailableModules as $sModuleId => $aModule) {
foreach ($this->aAnalyzeInstallationModules as $sModuleId => $aModule) {
if (($sModuleId != ROOT_MODULE) && !array_key_exists($sModuleId, $aModules) && isset($aModule['auto_select'])) {
try {
SetupInfo::SetSelectedModules($aModules);
@@ -1852,110 +1891,102 @@ EOF
protected function GetStepInfo($idx = null)
{
$aStepInfo = null;
if ($idx === null) {
$index = $this->GetStepIndex();
} else {
$index = $idx;
}
$index = $idx ?? $this->GetStepIndex();
$aSteps = [];
$this->oWizard->SetParameter('additional_extensions_modules', json_encode([])); // Default value, no additional extensions
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());
$aSteps = $aParams->Get('steps', []);
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', []);
// Additional step for the "extensions"
$aStepDefinition = [
'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' => [],
];
if ($index + 1 >= count($this->aSteps)) {
//make sure we also cache next step as well
$aOptions = $this->oExtensionsMap->GetAllExtensionsOptionInfo();
foreach ($this->oExtensionsMap->GetAllExtensionsWithPreviouslyInstalled() as $oExtension) {
if (($oExtension->sSource !== iTopExtension::SOURCE_WIZARD) && ($oExtension->bVisible) && (count($oExtension->aMissingDependencies) == 0)) {
$aStepDefinition['options'][] = [
'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 || ($oExtension->sSource === iTopExtension::SOURCE_REMOTE),
'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource),
'uninstallable' => $oExtension->CanBeUninstalled(),
'missing' => $oExtension->bRemovedFromDisk,
];
// 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();
// Display this step of the wizard only if there is something to display
if (count($aStepDefinition['options']) !== 0) {
$aSteps[] = $aStepDefinition;
$this->oWizard->SetParameter('additional_extensions_modules', json_encode($aStepDefinition['options']));
// 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,
],
];
}
} else {
// 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.
$aStepDefinition = [
'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' => [],
];
foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) {
if (($oExtension->bVisible) && (count($oExtension->aMissingDependencies) == 0)) {
$aStepDefinition['options'][] = [
'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 || ($oExtension->sSource !== iTopExtension::SOURCE_REMOTE),
'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource),
];
}
}
$aSteps[] = $aStepDefinition;
}
if (array_key_exists($index, $aSteps)) {
$aStepInfo = $aSteps[$index];
}
return $aStepInfo;
return $this->aSteps[$index] ?? null;
}
protected function GetExtensionSourceLabel($sSource)
public function ComputeChoiceFlags(array $aChoice, string $sChoiceId, array $aSelectedComponents, bool $bAllDisabled, bool $bDisableUninstallCheck, bool $bUpgradeMode)
{
$sDecorationClass = '';
switch ($sSource) {
case iTopExtension::SOURCE_MANUAL:
$sResult = 'Local extensions folder';
$sDecorationClass = 'fas fa-folder';
break;
$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;
case iTopExtension::SOURCE_REMOTE:
$sResult = (ITOP_APPLICATION == 'iTop') ? 'iTop Hub' : 'ITSM Designer';
$sDecorationClass = (ITOP_APPLICATION == 'iTop') ? 'fc fc-chameleon-icon' : 'fa pencil-ruler';
break;
$bChecked = $bSelected;
$bDisabled = false;
if ($bMissingFromDisk) {
$bDisabled = true;
$bChecked = false;
}
elseif($bMandatory || $bInstalled && !$bCanBeUninstalled){
$bDisabled = true;
$bChecked = true;
}
if($bAllDisabled){
$bDisabled = true;
}
default:
$sResult = '';
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;
}
}
}
}
if ($sResult == '') {
return '';
}
return '<i class="setup-extension--icon '.$sDecorationClass.'" data-tooltip-content="'.$sResult.'"></i>';
return [
'uninstallable' => $bCanBeUninstalled,
'missing' => $bMissingFromDisk,
'installed' => $bInstalled,
'disabled' => $bDisabled,
'checked' => $bChecked,
];
}
protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults, $sParentId = '', $bAllDisabled = false)
{
$aOptions = isset($aStepInfo['options']) ? $aStepInfo['options'] : [];
$aAlternatives = isset($aStepInfo['alternatives']) ? $aStepInfo['alternatives'] : [];
$aOptions = $aStepInfo['options'] ?? [];
$aAlternatives = $aStepInfo['alternatives'] ?? [];
$bDisableUninstallCheck = (bool)$this->oWizard->GetParameter('force-uninstall', false);
@@ -1963,43 +1994,33 @@ EOF
$sChoiceId = $sParentId.self::$SEP.$index;
$sDataId = 'data-id="'.utils::EscapeHtml($aChoice['extension_code']).'"';
$sId = utils::EscapeHtml($aChoice['extension_code']);
$bIsDefault = array_key_exists($sChoiceId, $aDefaults);
$oITopExtension = $this->oExtensionsMap->GetFromExtensionCode($aChoice['extension_code']);
$bCanBeUninstalled = isset($aChoice['uninstallable']) ? $aChoice['uninstallable'] : $oITopExtension->CanBeUninstalled();
$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId);
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $bIsDefault && !$bCanBeUninstalled && !$bDisableUninstallCheck;
;
$bMissingFromDisk = isset($aChoice['missing']) && $aChoice['missing'] === true;
$bInstalled = $bMissingFromDisk || $oITopExtension->bInstalled;
$bDisabled = $bMandatory || $bAllDisabled || $bMissingFromDisk;
$bChecked = $bMandatory || $bSelected;
$aFlags = static::ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $this->bUpgrade);
$sTooltip = '';
$sUnremovable = '';
if ($bMissingFromDisk) {
$sTooltip .= '<span class="setup-extension-tag removed">source removed</span>';
if ($aFlags['missing']) {
$sTooltip .= '<div class="setup-extension-tag removed">source removed</div>';
}
if ($bInstalled) {
$sTooltip .= '<span class="setup-extension-tag checked installed">installed</span>';
$sTooltip .= '<span class="setup-extension-tag unchecked tobeuninstalled">to be uninstalled</span>';
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 .= '<span class="setup-extension-tag checked tobeinstalled">to be installed</span>';
$sTooltip .= '<span class="setup-extension-tag unchecked notinstalled">not installed</span>';
$sTooltip .= '<div class="setup-extension-tag checked tobeinstalled">to be installed</div>';
$sTooltip .= '<div class="setup-extension-tag unchecked notinstalled">not installed</div>';
}
if (!$bCanBeUninstalled) {
$sTooltip .= '<span class="setup-extension-tag notuninstallable">cannot be uninstalled</span>';
if (!$aFlags['uninstallable']) {
$sTooltip .= '<div class="setup-extension-tag notuninstallable">cannot be uninstalled</div>';
}
if ($bDisabled && !$bChecked && !$bCanBeUninstalled && !$bDisableUninstallCheck) {
if ($aFlags['disabled'] && !$aFlags['checked'] && !$aFlags['uninstallable'] && !$bDisableUninstallCheck) {
$this->bCanMoveForward = false;//Disable "Next"
}
$sChecked = $bChecked ? ' checked ' : '';
$sDisabled = $bDisabled ? ' disabled data-disabled="disabled" ' : '';
$sMissingModule = $bMissingFromDisk ? 'setup-extension--missing' : '';
$sChecked = $aFlags['checked'] ? ' checked ' : '';
$sDisabled = $aFlags['disabled'] ? ' disabled data-disabled="disabled" ' : '';
$sMissingModule = $aFlags['missing'] ? 'setup-extension--missing' : '';
$sHiddenInput = $bDisabled && $bChecked ? '<input type="hidden" name="choice['.$sChoiceId.']" value="'.$sChoiceId.'"/>' : '';
$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, $bDisabled, $sTooltip);
$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $aFlags['disabled'], $sTooltip);
$oPage->add('</div>');
}
$sChoiceName = null;
@@ -2034,7 +2055,6 @@ EOF
$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
}
@@ -2178,6 +2198,41 @@ class WizStepSummary extends WizardStep
$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>');
@@ -2231,8 +2286,6 @@ class WizStepSummary extends WizardStep
}
$aSelectedModules = $aInstallParams['selected_modules'];
if (isset($aMiscOptions['generate_config'])) {
$oDoc = new DOMDocument('1.0', 'UTF-8');
$oDoc->preserveWhiteSpace = false;

View File

@@ -230,6 +230,7 @@ class XMLDataLoader
} else {
$iDstObj = (int)($oSubNode);
// Attempt to find the object in the list of loaded objects
/** @var \Combodo\iTop\Core\AttributeDefinition\AttributeExternalKey $oAttDef */
$iExtKey = $this->GetObjectKey($oAttDef->GetTargetClass(), $iDstObj);
if ($iExtKey == 0) {
$iExtKey = -$iDstObj; // Convention: Unresolved keys are stored as negative !
@@ -353,8 +354,10 @@ class XMLDataLoader
foreach ($oObjList as $oTargetObj) {
$bChanged = false;
$sClass = get_class($oTargetObj);
$iExtKey = -1;
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) {
if (($oAttDef->IsExternalKey()) && ($oTargetObj->Get($sAttCode) < 0)) { // Convention unresolved key = negative
/** @var \Combodo\iTop\Core\AttributeDefinition\AttributeExternalKey $oAttDef */
$sTargetClass = $oAttDef->GetTargetClass();
$iTempKey = $oTargetObj->Get($sAttCode);

View File

@@ -25,6 +25,7 @@ class JsonPage extends WebPage
* This can be useful when feeding response to a third party lib that doesn't understand the structured format.
*/
protected $bOutputDataOnly = false;
protected $bOutputHeaders = true;
/**
* JsonPage constructor.
@@ -82,6 +83,19 @@ class JsonPage extends WebPage
return $this;
}
/**
* @see static::$bOutputHeaders
* @param bool $bFlag
*
* @return $this
*/
public function SetOutputHeaders(bool $bFlag)
{
$this->bOutputHeaders = $bFlag;
return $this;
}
/**
* Output the headers
*
@@ -119,7 +133,10 @@ class JsonPage extends WebPage
public function output()
{
$oKpi = new ExecutionKPI();
$this->OutputHeaders();
if ($this->bOutputHeaders) {
$this->OutputHeaders();
}
$sContent = $this->ComputeContent();
$oKpi->ComputeAndReport(get_class($this).' output');

View File

@@ -1019,10 +1019,10 @@ EOF
if ($sModuleId == '_Root_') {
continue;
}
if ($aModuleData['version_db'] == '') {
if ($aModuleData['installed_version'] == '') {
continue;
}
$oPage->add('InstalledModule/'.$sModuleId.': '.$aModuleData['version_db']."\n");
$oPage->add('InstalledModule/'.$sModuleId.': '.$aModuleData['installed_version']."\n");
}
$oPage->add('===== end =====');

View File

@@ -178,12 +178,19 @@ class InterfaceDiscovery
continue;
}
$aTmpClassMap = include $sAutoloadFile;
if (! is_array($aTmpClassMap)) {
//can happen when setup compilation broken in the middle
//ex: $sAutoloadFile could be empty and $aTmpClassMap is a int
$aAutoloaderErrors[] = $sAutoloadFile;
continue;
}
/** @noinspection SlowArrayOperationsInLoopInspection we are getting an associative array so the documented workarounds cannot be used */
$aClassMap = array_merge($aClassMap, $aTmpClassMap);
}
if (count($aAutoloaderErrors) > 0) {
IssueLog::Debug(
__METHOD__." cannot load some of the autoloader files",
__METHOD__." cannot load some of the autoloader files: missing or corrupted",
LogChannels::CORE,
['autoloader_errors' => $aAutoloaderErrors]
);

View File

@@ -0,0 +1,129 @@
# PHP static analysis
- [Installation](#installation)
- [Usages](#usages)
- [Analysing a package](#analysing-a-package)
- [Analysing a module](#analysing-a-module)
- [Configuration](#configuration)
- [Adjust local configuration to your needs](#adjust-local-configuration-to-your-needs)
- [Adjust configuration for a particular CI repository / job](#adjust-configuration-for-a-particular-ci-repository--job)
## Installation
- Install dependencies by running `composer install` in this folder
- You should be all set! 🚀
## Usages
### Analysing a package
_Do this if you want to analyse the whole iTop package (iTop core, extensions, third-party libs, ...)_
- Make sure you ran a setup on your iTop as it will analyse the `env-production` folder
- Open a prompt in your iTop folder
- Run the following command
```bash
tests/php-static-analysis/vendor/bin/phpstan analyse \
--configuration ./tests/php-static-analysis/config/for-package.dist.neon \
--error-format raw
```
You will then have an output like this listing all errors:
```bash
tests/php-static-analysis/vendor/bin/phpstan analyse \
--configuration ./tests/php-static-analysis/config/for-package.dist.neon \
--error-format raw
1049/1049 [============================] 100%
<ITOP>\addons\userrights\userrightsprofile.class.inc.php:552:Call to static method InitSharedClassProperties() on an unknown class SharedObject.
<ITOP>\addons\userrights\userrightsprofile.db.class.inc.php:927:Call to static method GetSharedClassProperties() on an unknown class SharedObject.
<ITOP>\addons\userrights\userrightsprojection.class.inc.php:722:Access to an undefined property UserRightsProjection::$m_aClassProjs.
<ITOP>\application\applicationextension.inc.php:295:Method AbstractPreferencesExtension::ApplyPreferences() should return bool but return statement is missing.
<ITOP>\application\cmdbabstract.class.inc.php:1010:Class utils referenced with incorrect case: Utils.
[...]
```
### Analysing a module
_Do this if you only want to analyse one or more modules within this iTop but not the whole package_
- Make sure you ran a setup on your iTop as it will analyse the `env-production` folder
- Open a prompt in your iTop folder
- Run the following command
```
tests/php-static-analysis/vendor/bin/phpstan analyse \
--configuration ./tests/php-static-analysis/config/for-module.dist.neon \
--error-format raw \
env-production/<MODULE_CODE_1> [env-production/<MODULE_CODE_2> ...]
```
You will then have an output like this listing all errors:
```
tests/php-static-analysis/vendor/bin/phpstan analyse \
--configuration ./tests/php-static-analysis/config/for-module.dist.neon \
--error-format raw \
env-production/authent-ldap env-production/itop-oauth-client
49/49 [============================] 100%
<ITOP>\env-production\authent-ldap\model.authent-ldap.php:79:Undefined variable: $hDS
<ITOP>\env-production\authent-ldap\model.authent-ldap.php:80:Undefined variable: $name
<ITOP>\env-production\authent-ldap\model.authent-ldap.php:80:Undefined variable: $value
<ITOP>\env-production\itop-oauth-client\vendor\composer\InstalledVersions.php:105:Parameter $parser of method Composer\InstalledVersions::satisfies() has invalid type Composer\Semver\VersionParser.
[...]
```
## Configuration
### Adjust local configuration to your needs
#### Define which PHP version to run the analysis for
The way we configured PHPStan in this project changes how it will find the PHP version to run the analysis for. \
By default PHPStan check the information from the composer.json file, but we changed that (via the `config/php-includes/set-php-version-from-process.php` include) so it used the PHP
version currently ran by the CLI.
So all you have to do is either:
- Prepend your command line with the path of the executable of the desired PHP version
- Change the default PHP interpreter in your IDE settings
#### Change some parameters for a local run
If you want to change some particular settings (eg. the memory limit, the rules level, ...) for a local run of the analysis you have 2 choices.
##### Method 1: CLI parameter
For most parameters there is a good chance you can just add the parameter and its value in your command, which will override the one defined in the configuration file. \
Below are some example, but your can find the complete reference [here](https://phpstan.org/user-guide/command-line-usage).
```bash
--memory-limit 1G
--level 5
--error-format raw
[...]
```
**Pros** Quick and easy to try different parameters \
**Cons** Parameters aren't saved, so you'll have to remember them and put them again next time
##### Method 2: Configuration file
Crafting your own configuration file gives you the ability to fine tune any parameters, it's way more powerful but can also quickly lead to crashes if you mess with the symbols discovery (classes, ...). \
But mostly it can be saved, shared, re-used; which is it's main purpose.
It is recommended that you create your configuration file from scratch and that you include the `base.dist.neon` so you are bootstrapped for the symbols discovery. Then you can override any parameter. \
Check [the documentation](https://phpstan.org/config-reference#multiple-files) for more information.
```neon
includes:
- base.dist.neon
parameters:
# Override parameters here
```
#### Analyse only one (or some) folder(s) quicker
It's pretty easy and good news you don't need to create a new configuration file or change an existing one. \
Just adapt and use command lines from the [usages section](#usages) and add the folders you want to analyse at the end of the command, exactly like when analysing modules.
For example if you want to analyse just `<ITOP>/setup` and `<ITOP>/sources`, use something like:
```
tests/php-static-analysis/vendor/bin/phpstan analyse \
--configuration ./tests/php-static-analysis/config/for-package.dist.neon \
--error-format raw \
setup sources
```
### Adjust configuration for a particular CI repository / job
TODO

View File

@@ -0,0 +1,5 @@
{
"require": {
"phpstan/phpstan": "^2.1"
}
}

72
tests/php-static-analysis/composer.lock generated Normal file
View File

@@ -0,0 +1,72 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "cc6d7580a5e98236d68d8b91de9ddebb",
"packages": [
{
"name": "phpstan/phpstan",
"version": "2.1.33",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f",
"reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"docs": "https://phpstan.org/user-guide/getting-started",
"forum": "https://github.com/phpstan/phpstan/discussions",
"issues": "https://github.com/phpstan/phpstan/issues",
"security": "https://github.com/phpstan/phpstan/security/policy",
"source": "https://github.com/phpstan/phpstan-src"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
}
],
"time": "2025-12-05T10:24:31+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

View File

@@ -0,0 +1,29 @@
## Disclaimer
DON'T modify the following files without knowledge and discussing with the team:
- base.dist.neon
- for-package.dist.neon
- for-module.dist.neon
## Purpose of these files
### base.dist.neon
This configuration file contains the common parameters for all analysis, whereas it is a package, a module or something specific. Among others:
- Rules level for analysis
- PHP version to compare
- Necessary files for autoloaders discovery and such
- ...
This file should not be modified for your specific needs, you should always include it and override the desired parameters. \
See how it is done in `for-package.dist.neon` and `for-module.dist.neon` or on the documentation [here](https://phpstan.org/config-reference#multiple-files).
### for-package.dist.neon
This configuration file contains the parameters to analyse a package (iTop core, modules, third-party libs).
### for-module.dist.neon
This configuration file contains the parameters to analyse one or more modules only.
## How / when can I modify these files?
**You CAN'T!** \
Well, unless there is a good reason and you talked about it with the team. But you should never modify them for a specific need on your local environment.
- If you have a particular need for your local environment (eg. increase memory limit, change rules levels, analyse only a specific folder), check the [Configuration section](../#configuration) of the main README.md.
- If you feel like there is need for an adjustment in the default configurations, discuss it with th team and make a PR.

View File

@@ -0,0 +1,40 @@
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
parameters:
level: 0
#phpVersion: null # Explicitly commented as we rather use the detected version from the above include (`php-includes/target-php-version.php`)
editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%' # Open in PHPStorm as it's Combodo's default IDE
bootstrapFiles:
- ../../../approot.inc.php
- ../../../bootstrap.inc.php
scanFiles:
# Files necessary as they contain some declarations (constants, classes, functions, ...)
- ../../../approot.inc.php
- ../../../bootstrap.inc.php
excludePaths:
analyse:
# For third-party libs we should analyse them in a dedicated configuration as we can't improve / clean them which would
# prevent us from raising the rules level as we improve / clean our codebase
- ../../../lib # Irrelevant as we only want to analyze our codebase
- ../../../node_modules # Irrelevant as we only want to analyze our codebase
analyseAndScan:
# This file generates "unignorable errors" for the baseline due to its format, so we don't have any other choice than to exclude it.
# But mind that it will prevent PHPStan from warning us about PHP syntax errors in this file.
- ../../../core/oql/build/PHP/Lempar.php
#- ../../../data # Left and commented on purpose to show that we want to analyse the generated cache files
# Note 1: We can't analyse these folders as if a PHP file requires another PHP element declared in an XML file, it won't find it. So we rely only on `env-production`
# Note 2: Only the options selected during the setup will be analysed correctly in `env-production`. For unselected options, we still want to ignore them during the analysis as they would only give a false sentiment of security as their XML PHP classes / snippets / etc would not be tested.
- ../../../data/production-modules (?) # Irrelevent as it will already be in `env-production` (for local run only, not useful in the CI)
- ../../../datamodels # Irrelevent as it will already be in `env-production`
- ../../../extensions # Irrelevent as it will already be in `env-production` (for local run only, not useful in the CI)
- ../../../env-php-unit-tests (?) # Irrelevant as it will either already be in `env-production` or might be desynchronized from `env-production`
- ../../../env-toolkit (?) # Irrelevent as it will either already be in `env-production` or might be desynchronized from `env-production` (for local run only, not useful in the CI)
- ../../../tests (?) # Exclude tests for now
- ../../../toolkit (?) # Exlclude toolkit for now

View File

@@ -0,0 +1,15 @@
includes:
- base.dist.neon
parameters:
paths:
# We just want to analyse the module folder(s), either:
# - Create your own `for-module.neon` file, include this one and override this parameter (see https://phpstan.org/config-reference#multiple-files)
# - Pass the module folder(s) in the commande line (see https://phpstan.org/config-reference#analysed-files)
scanDirectories:
# Unlike for `for-package.dist.neon`, here we need to scan all the folders to discover symbols, but we only want to analyse the module folder.
# We initially thought of doing it through the `excludePaths` param. by excluding everything but the module folder, but it doesn't seem to be possible, because it uses the `fnmatch()` function.
# As a workaround, we list here all the folders to scan.
#
# Scan the whole project and rely on the `excludePaths` param. to filter the unnecessary
- ../../..

View File

@@ -0,0 +1,7 @@
includes:
- base.dist.neon
parameters:
paths:
# We want to analyse almost the whole project, so we do a negative selection between the `paths` and `excludePaths` (see base.dist.neon) parameters
- ../../../

View File

@@ -0,0 +1,25 @@
<?php
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
declare(strict_types=1);
/**
* This file is only here to allow setting a specific PHP version to run the analysis for without
* having to explicitly set it in the .neon file. This is the best way we found so far.
*
* @link https://phpstan.org/config-reference#phpversion
*
* Usage: Uses the CLI PHP version by default, which would work fine for
* - The CI as the docker image has the target PHP version in both CLI and web
* - The developer's IDE as PHPStorm also has a default PHP version configured which can be changed on the fly
*/
// Default PHP version to analyse is the one running in CLI
$config = [];
$config['parameters']['phpVersion'] = PHP_VERSION_ID;
return $config;

View File

@@ -2,9 +2,6 @@
Documentation on creating and maintaining tests in iTop.
## Prerequisites
### PHPUnit configuration file
@@ -78,7 +75,8 @@ Example :
$oTagData->DBDelete();
```
Warning : when the condition is met the test is finished and following code will be ignored !
> [!WARNING]
> When the condition is met the test is finished and following code will be ignored !
Another way to do is using try/catch blocks, for example :
```php

View File

@@ -28,14 +28,14 @@ class AjaxPageTest extends ItopDataTestCase
$iLastCompilation = filemtime(APPROOT.'env-production');
// When
$sOutput = $this->CallItopUrl(
"/pages/exec.php?exec_module=itop-hub-connector&exec_page=ajax.php",
$sOutput = $this->CallItopUri(
"pages/exec.php?exec_module=itop-hub-connector&exec_page=ajax.php",
[
'auth_user' => $sLogin,
'auth_pwd' => self::AUTHENTICATION_PASSWORD,
'operation' => "compile",
'authent' => self::AUTHENTICATION_TOKEN,
]
],
);
// Then
@@ -53,26 +53,4 @@ class AjaxPageTest extends ItopDataTestCase
clearstatcache();
$this->assertGreaterThan($iLastCompilation, filemtime(APPROOT.'env-production'), 'The env-production directory should have been rebuilt');
}
protected function CallItopUrl($sUri, ?array $aPostFields = null, bool $bXDebugEnabled = false)
{
$ch = curl_init();
if ($bXDebugEnabled) {
curl_setopt($ch, CURLOPT_COOKIE, 'XDEBUG_SESSION=phpstorm');
}
$sUrl = \MetaModel::GetConfig()->Get('app_root_url')."/$sUri";
var_dump($sUrl);
curl_setopt($ch, CURLOPT_URL, $sUrl);
curl_setopt($ch, CURLOPT_POST, 1);// set post data to true
curl_setopt($ch, CURLOPT_POSTFIELDS, $aPostFields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$sOutput = curl_exec($ch);
//echo "$sUrl error code:".curl_error($ch);
curl_close($ch);
return $sOutput;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Combodo\iTop\Test\UnitTest\HubConnector;
use Combodo\iTop\DBTools\Service\DBToolsUtils;
use Combodo\iTop\HubConnector\Controller\HubController;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use DOMFormatException;
use JsonPage;
use MFCompiler;
use SetupLog;
use SetupUtils;
use utils;
/**
* @runClassInSeparateProcess
*/
class HubControllerTest extends ItopDataTestCase
{
public const USE_TRANSACTION = false;
public const AUTHENTICATION_TOKEN = '14b5da9d092f84044187421419a0347e7317bc8cd2b486fdda631be06b959269';
public const AUTHENTICATION_PASSWORD = "tagada-Secret,007";
protected function setUp(): void
{
$this->SkipIfModuleNotPresent('itop-hub-connector');
parent::setUp();
$this->RequireOnceItopFile('env-production/itop-hub-connector/src/Controller/HubController.php');
}
public function testLaunchCompile(): void
{
$this->PrepareCompileAuthent();
$this->CopyProductionModulesIntoHubExtensionDir();
HubController::GetInstance()->SetOutputHeaders(false);
HubController::GetInstance()->LaunchCompile();
$this->CheckReport('{"code":0,"message":"Ok","fields":[]}');
}
public function testLaunchDeploy(): void
{
$this->testLaunchCompile();
HubController::GetInstance()->LaunchDeploy();
$this->CheckReport('{"code":0,"message":"Compilation successful.","fields":[]}');
$this->AssertPreviousAndCurrentInstallationAreEquivalent();
}
private function CheckReport($sExpected)
{
$oJsonPage = HubController::GetInstance()->GetLastJsonPage();
$this->assertEquals($sExpected, $this->InvokeNonPublicMethod(JsonPage::class, 'ComputeContent', $oJsonPage, []));
//keep line below to avoid: Test code or tested code did not (only) close its own output buffers
$this->InvokeNonPublicMethod(JsonPage::class, 'RenderContent', $oJsonPage, []);
}
private function PrepareCompileAuthent()
{
$sUUID = 'hub_'.uniqid();
$_REQUEST['authent'] = $sUUID;
$sPath = utils::GetDataPath().'hub/compile_authent';
file_put_contents($sPath, $sUUID);
$this->aFileToClean[] = $sPath;
}
private function CopyProductionModulesIntoHubExtensionDir()
{
$sProdModules = APPROOT.'/data/production-modules';
$sExtensionDir = APPROOT.'/data/downloaded-extensions/';
$this->aFileToClean[] = $sExtensionDir;
SetupUtils::rrmdir($sExtensionDir);
@mkdir($sExtensionDir);
SetupUtils::copydir($sExtensionDir, $sProdModules);
}
}

View File

@@ -1,4 +1,5 @@
<?php
/**
* Copyright (C) 2013-2024 Combodo SAS
*
@@ -24,19 +25,15 @@
require_once('../../../approot.inc.php');
require_once(APPROOT.'application/startup.inc.php');
$sEnvironment = MetaModel::GetEnvironmentId();
$aEntries = array();
$aEntries = [];
$aCacheUserData = apc_cache_info_compat();
if (is_array($aCacheUserData) && isset($aCacheUserData['cache_list']))
{
if (is_array($aCacheUserData) && isset($aCacheUserData['cache_list'])) {
$sPrefix = 'itop-'.$sEnvironment.'-query-cache-';
foreach($aCacheUserData['cache_list'] as $i => $aEntry)
{
foreach ($aCacheUserData['cache_list'] as $i => $aEntry) {
$sEntryKey = array_key_exists('info', $aEntry) ? $aEntry['info'] : $aEntry['key'];
if (strpos($sEntryKey, $sPrefix) === 0)
{
if (strpos($sEntryKey, $sPrefix) === 0) {
$aEntries[] = $sEntryKey;
}
}
@@ -44,52 +41,39 @@ if (is_array($aCacheUserData) && isset($aCacheUserData['cache_list']))
echo "<pre>";
if (empty($aEntries))
{
if (empty($aEntries)) {
echo "No Data";
return;
}
$sKey = $aEntries[0];
$result = apc_fetch($sKey);
if (!is_object($result))
{
if (!is_object($result)) {
return;
}
$oSQLQuery = $result;
echo "NB Tables before;NB Tables after;";
foreach($oSQLQuery->m_aContextData as $sField => $oValue)
{
foreach ($oSQLQuery->m_aContextData as $sField => $oValue) {
echo $sField.';';
}
echo "\n";
sort($aEntries);
foreach($aEntries as $sKey)
{
foreach ($aEntries as $sKey) {
$result = apc_fetch($sKey);
if (is_object($result))
{
if (is_object($result)) {
$oSQLQuery = $result;
if (isset($oSQLQuery->m_aContextData))
{
if (isset($oSQLQuery->m_aContextData)) {
echo $oSQLQuery->m_iOriginalTableCount.";".$oSQLQuery->CountTables().';';
foreach($oSQLQuery->m_aContextData as $oValue)
{
if (is_array($oValue))
{
foreach ($oSQLQuery->m_aContextData as $oValue) {
if (is_array($oValue)) {
$sVal = json_encode($oValue);
}
else
{
if (empty($oValue))
{
} else {
if (empty($oValue)) {
$sVal = '';
}
else
{
} else {
$sVal = $oValue;
}
}
@@ -101,4 +85,3 @@ foreach($aEntries as $sKey)
}
echo "</pre>";

View File

@@ -54,10 +54,20 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
*/
abstract public function GetDatamodelDeltaAbsPath(): string;
/**
* @return array<string, string> : dict extensions folders by their code
*/
public function GetAdditionalFeaturePaths(): array
{
return [];
}
protected function setUp(): void
{
static::LoadRequiredItopFiles();
$this->oEnvironment = new UnitTestRunTimeEnvironment('production', $this->GetTestEnvironment());
if (is_null($this->oEnvironment)) {
$this->oEnvironment = new UnitTestRunTimeEnvironment($this->GetTestEnvironment());
}
parent::setUp();
}
@@ -123,7 +133,6 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
// Note: To improve performances, we compile all XML deltas from test cases derived from this class and make a single environment where everything will be ran at once.
// This requires XML deltas to be compatible, but it is a known and accepted trade-off. See PR #457
if (false === $this->IsEnvironmentReady()) {
$this->debug("Preparing custom environment '$sTestEnv' with the following datamodel files:");
foreach ($this->oEnvironment->GetCustomDatamodelFiles() as $sCustomDatamodelFile) {
$this->debug(" - $sCustomDatamodelFile");
@@ -155,24 +164,33 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
$oTestConfig->ChangeModulesPath($sSourceEnv, $sTestEnv);
// - Switch DB name to a dedicated one so we don't mess with the original one
$sTestEnvSanitizedForDBName = preg_replace('/[^\d\w]/', '', $sTestEnv);
$oTestConfig->Set('db_name', $oTestConfig->Get('db_name').'_'.$sTestEnvSanitizedForDBName);
$sPreviousDB = $oTestConfig->Get('db_name');
$sNewDB = $sPreviousDB.'_'.$sTestEnvSanitizedForDBName;
$oTestConfig->Set('db_name', $sNewDB);
// - Compile env. based on the existing 'production' env.
$oEnvironment = new UnitTestRunTimeEnvironment($sSourceEnv, $sTestEnv);
$oEnvironment->WriteConfigFileSafe($oTestConfig);
$oEnvironment->CompileFrom($sSourceEnv);
//$oEnvironment = new UnitTestRunTimeEnvironment($sSourceEnv, $sTestEnv);
$this->oEnvironment->WriteConfigFileSafe($oTestConfig);
$this->oEnvironment->CompileFrom($sSourceEnv);
// - Force re-creating a fresh DB
CMDBSource::InitFromConfig($oTestConfig);
if (CMDBSource::IsDB($oTestConfig->Get('db_name'))) {
if (CMDBSource::IsDB($sNewDB)) {
CMDBSource::DropDB();
}
CMDBSource::CreateDB($oTestConfig->Get('db_name'));
CMDBSource::CreateDB($sNewDB);
MetaModel::Startup($sConfFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sTestEnv);
// N°7446 For some reason we need to create the DB schema before starting the MM, then only we can create the tables.
MetaModel::DBCreate();
// Make sure that runtime environment is complete
// RunTimeEnvironment::AnalyzeInstallation would not return core modules otherwise...
CMDBSource::DropTable("priv_module_install");
CMDBSource::Query("CREATE TABLE $sNewDB.priv_module_install SELECT * FROM $sPreviousDB.priv_module_install");
$this->debug("Custom environment '$sTestEnv' is ready!");
} else {
$this->debug("Custom environment '$sTestEnv' READY BUILT:");
}
parent::PrepareEnvironment();

View File

@@ -17,7 +17,9 @@ namespace Combodo\iTop\Test\UnitTest;
use ArchivedObjectException;
use CMDBObject;
use CMDBSource;
use Combodo\iTop\DBTools\Service\DBToolsUtils;
use Combodo\iTop\Service\Events\EventService;
use Config;
use Contact;
use CoreException;
use CoreUnexpectedValue;
@@ -33,6 +35,7 @@ use lnkContactToTicket;
use lnkFunctionalCIToTicket;
use MetaModel;
use MissingQueryArgument;
use ModuleInstallationRepository;
use MySQLException;
use MySQLHasGoneAwayException;
use Person;
@@ -70,6 +73,9 @@ abstract class ItopDataTestCase extends ItopTestCase
private $aCreatedObjects = [];
private $aEventListeners = [];
protected ?string $sConfigTmpBackupFile = null;
protected ?Config $oiTopConfig = null;
/**
* @var bool When testing with silo, there are some cache we need to update on tearDown. Doing it all the time will cost too much, so it's opt-in !
* @see tearDown
@@ -124,6 +130,8 @@ abstract class ItopDataTestCase extends ItopTestCase
{
parent::setUp();
\IssueLog::Error($this->getName());
$this->PrepareEnvironment();
if (static::USE_TRANSACTION) {
@@ -190,6 +198,8 @@ abstract class ItopDataTestCase extends ItopTestCase
CMDBObject::SetCurrentChange(null);
$this->RestoreConfiguration();
parent::tearDown();
}
@@ -1517,4 +1527,62 @@ abstract class ItopDataTestCase extends ItopTestCase
$oObject->Set($sStopwatchAttCode, $oStopwatch);
}
protected function BackupConfiguration(): void
{
$sConfigPath = MetaModel::GetConfig()->GetLoadedFile();
clearstatcache();
echo sprintf("rights via ls on %s:\n %s \n", $sConfigPath, exec("ls -al $sConfigPath"));
$sFilePermOutput = substr(sprintf('%o', fileperms('/etc/passwd')), -4);
echo sprintf("rights via fileperms on %s:\n %s \n", $sConfigPath, $sFilePermOutput);
$this->sConfigTmpBackupFile = tempnam(sys_get_temp_dir(), "config_");
MetaModel::GetConfig()->WriteToFile($this->sConfigTmpBackupFile);
$this->oiTopConfig = new Config($sConfigPath);
}
protected function RestoreConfiguration(): void
{
if (is_null($this->sConfigTmpBackupFile) || ! is_file($this->sConfigTmpBackupFile)) {
return;
}
if (is_null($this->oiTopConfig)) {
return;
}
//put config back
$sConfigPath = $this->oiTopConfig->GetLoadedFile();
@chmod($sConfigPath, 0770);
$oConfig = new Config($this->sConfigTmpBackupFile);
$oConfig->WriteToFile($sConfigPath);
@chmod($sConfigPath, 0440);
@unlink($this->sConfigTmpBackupFile);
}
public function AssertPreviousAndCurrentInstallationAreEquivalent()
{
$aPreviousInstallations = ModuleInstallationRepository::GetInstance()->GetPreviousModuleInstallationsByOffset(1);
$aInstallations = ModuleInstallationRepository::GetInstance()->GetPreviousModuleInstallationsByOffset();
$this->assertEquals($this->GetCanonicalComparableModuleInstallationArray($aPreviousInstallations), $this->GetCanonicalComparableModuleInstallationArray($aInstallations));
}
protected function GetCanonicalComparableModuleInstallationArray($aInstallations): array
{
$aRes = [];
$aIgnoredFields = ['id', 'parent_id', 'installed', 'comment'];
foreach ($aInstallations as $aData) {
$aNewData = [];
foreach ($aData as $sKey => $val) {
if (in_array($sKey, $aIgnoredFields)) {
continue;
}
$aNewData[$sKey] = $val;
}
$sName = $aNewData['name'];
$aRes[$sName] = $aNewData;
}
asort($aRes);
return $aRes;
}
}

View File

@@ -8,12 +8,11 @@
namespace Combodo\iTop\Test\UnitTest;
use CMDBSource;
use DateTime;
use DeprecatedCallsLog;
use MySQLTransactionNotClosedException;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use ReflectionMethod;
use SetupUtils;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpKernel\KernelInterface;
use const DEBUG_BACKTRACE_IGNORE_ARGS;
@@ -29,6 +28,7 @@ use const DEBUG_BACKTRACE_IGNORE_ARGS;
abstract class ItopTestCase extends KernelTestCase
{
public const TEST_LOG_DIR = 'test';
protected array $aFileToClean = [];
/**
* @var bool
@@ -37,7 +37,7 @@ abstract class ItopTestCase extends KernelTestCase
public const DISABLE_DEPRECATEDCALLSLOG_ERRORHANDLER = true;
public static $DEBUG_UNIT_TEST = false;
protected static $aBackupStaticProperties = [];
public ?array $aLastCurlGetInfo = null;
/**
* @link https://docs.phpunit.de/en/9.6/annotations.html#preserveglobalstate PHPUnit `preserveGlobalState` annotation documentation
*
@@ -175,6 +175,15 @@ abstract class ItopTestCase extends KernelTestCase
}
throw new MySQLTransactionNotClosedException('Some DB transactions were opened but not closed ! Fix the code by adding ROLLBACK or COMMIT statements !', []);
}
foreach ($this->aFileToClean as $sPath) {
if (is_file($sPath)) {
@unlink($sPath);
continue;
}
SetupUtils::tidydir($sPath);
}
}
/**
@@ -631,4 +640,75 @@ abstract class ItopTestCase extends KernelTestCase
fclose($handle);
return array_reverse($aLines);
}
/**
* @param $sUrl
* @param array|null $aPostFields
* @param array|null $aCurlOptions
* @param $bXDebugEnabled
* @return string
*/
protected function CallUrl($sUrl, ?array $aPostFields = [], ?array $aCurlOptions = [], $bXDebugEnabled = false): string
{
$ch = curl_init();
if ($bXDebugEnabled) {
curl_setopt($ch, CURLOPT_COOKIE, "XDEBUG_SESSION=phpstorm");
}
curl_setopt($ch, CURLOPT_URL, $sUrl);
curl_setopt($ch, CURLOPT_POST, 1);// set post data to true
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Force disable of certificate check as most of dev / test env have a self-signed certificate
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt_array($ch, $aCurlOptions);
if ($this->IsArrayOfArray($aPostFields)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($aPostFields));
} else {
curl_setopt($ch, CURLOPT_POSTFIELDS, $aPostFields);
}
$sOutput = curl_exec($ch);
$info = curl_getinfo($ch);
$this->aLastCurlGetInfo = $info;
$sErrorMsg = curl_error($ch);
$iErrorCode = curl_errno($ch);
curl_close($ch);
\IssueLog::Info(__METHOD__, null, ['url' => $sUrl, 'error' => $sErrorMsg, 'error_code' => $iErrorCode, 'post_fields' => $aPostFields, 'info' => $info]);
return $sOutput;
}
private function IsArrayOfArray(array $aStruct): bool
{
foreach ($aStruct as $k => $v) {
if (is_array($v)) {
return true;
}
}
return false;
}
protected function CallItopUri(string $sUri, ?array $aPostFields = [], ?array $aCurlOptions = [], $bXDebugEnabled = false): string
{
$sUrl = \MetaModel::GetConfig()->Get('app_root_url')."/$sUri";
return $this->CallUrl($sUrl, $aPostFields, $aCurlOptions, $bXDebugEnabled);
}
/**
* Return a temporary file path. that will be cleaned up by tearDown()
*
* @return string: temporary file path: file prefix include phpunit test method name
*/
public function GetTemporaryFilePath(): string
{
$sPrefix = $this->getName(false);
$sPath = tempnam(sys_get_temp_dir(), $sPrefix);
$this->aFileToClean[] = $sPath;
return $sPath;
}
}

View File

@@ -8,11 +8,13 @@
namespace Combodo\iTop\Test\UnitTest\Service;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use DOMFormatException;
use MFCoreModule;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use RunTimeEnvironment;
use SetupLog;
use SetupUtils;
use utils;
@@ -26,26 +28,25 @@ use utils;
*/
class UnitTestRunTimeEnvironment extends RunTimeEnvironment
{
/**
* @var false
*/
public bool $bUseDelta = true;
/**
* @var true
*/
public bool $bUseAdditionalFeatures = false;
/**
* @var string[]
*/
protected $aCustomDatamodelFiles = null;
/**
* @var string
* @var string[]
*/
protected $sSourceEnv;
public function __construct($sSourceEnv, $sTargetEnv)
{
parent::__construct($sTargetEnv);
$this->sSourceEnv = $sSourceEnv;
}
public function GetEnvironment(): string
{
return $this->sFinalEnv;
}
protected $aAdditionExtensionFoldersByCode = null;
public function CompileFrom($sSourceEnv, $bUseSymLinks = null)
{
@@ -56,7 +57,22 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment
SetupUtils::copydir(APPROOT.'/data/'.$sSourceEnv.'-modules', $sDestModulesDir, $bUseSymLinks);
parent::CompileFrom($sSourceEnv, $bUseSymLinks);
if ($this->bUseAdditionalFeatures) {
foreach ($this->GetExtensionFoldersToAdd() as $sExtensionCode => $sFolderPath) {
\SetupLog::Info("ExtensionFoldersToAdd: $sExtensionCode => $sFolderPath");
$sFolderName = basename($sFolderPath);
@mkdir($sDestModulesDir.DIRECTORY_SEPARATOR.$sFolderName);
SetupUtils::copydir($sFolderPath, $sDestModulesDir.DIRECTORY_SEPARATOR.$sFolderName, $bUseSymLinks);
}
}
try {
parent::CompileFrom($sSourceEnv, $bUseSymLinks);
} catch (DOMFormatException $e) {
$sFileName = $sSourceEnv.'.delta.xml';
SetupLog::Error(__METHOD__, null, [$sFileName => @file_get_contents(APPROOT.'data/'.$sFileName)]);
throw $e;
}
}
public function IsUpToDate()
@@ -94,23 +110,43 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment
*/
protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir)
{
\SetupLog::Info(__METHOD__);
$aRet = parent::GetMFModulesToCompile($sSourceEnv, $sSourceDir);
foreach ($this->GetCustomDatamodelFiles() as $sDeltaFile) {
$sDeltaId = preg_replace('/[^\d\w]/', '', $sDeltaFile);
$sDeltaName = basename($sDeltaFile);
$sDeltaDir = dirname($sDeltaFile);
$oDelta = new MFCoreModule($sDeltaName, "$sDeltaDir/$sDeltaName", $sDeltaFile);
$aRet[$sDeltaId] = $oDelta;
if ($this->bUseDelta) {
foreach ($this->GetCustomDatamodelFiles() as $sDeltaFile) {
$sDeltaId = preg_replace('/[^\d\w]/', '', $sDeltaFile);
$sDeltaName = basename($sDeltaFile);
$sDeltaDir = dirname($sDeltaFile);
$oDelta = new MFCoreModule($sDeltaName, "$sDeltaDir/$sDeltaName", $sDeltaFile);
$aRet[$sDeltaId] = $oDelta;
}
}
return $aRet;
}
public function GetCustomDatamodelFiles()
public function GetExtensionFoldersToAdd(): array
{
if (!is_null($this->aCustomDatamodelFiles)) {
return $this->aCustomDatamodelFiles;
if (is_null($this->aAdditionExtensionFoldersByCode)) {
$this->InitViaItopCustomDatamodelTestCaseClasses();
}
return $this->aAdditionExtensionFoldersByCode;
}
public function GetCustomDatamodelFiles(): array
{
if (is_null($this->aCustomDatamodelFiles)) {
$this->InitViaItopCustomDatamodelTestCaseClasses();
}
return $this->aCustomDatamodelFiles;
}
public function InitViaItopCustomDatamodelTestCaseClasses()
{
$this->aAdditionExtensionFoldersByCode = [];
$this->aCustomDatamodelFiles = [];
// Search for the PHP files implementing the method GetDatamodelDeltaAbsPath
@@ -169,16 +205,19 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment
continue;
}
$sDeltaFile = $oTestClassInstance->GetDatamodelDeltaAbsPath();
if (!is_file($sDeltaFile)) {
throw new \Exception("Unknown delta file: $sDeltaFile, from test class '$sClass'");
}
if (!in_array($sDeltaFile, $this->aCustomDatamodelFiles)) {
$this->aCustomDatamodelFiles[] = $sDeltaFile;
if (strlen($sDeltaFile) > 0) {
if (!is_file($sDeltaFile)) {
throw new \Exception("Unknown delta file: $sDeltaFile, from test class '$sClass'");
}
if (!in_array($sDeltaFile, $this->aCustomDatamodelFiles)) {
$this->aCustomDatamodelFiles[] = $sDeltaFile;
}
}
$aExtensionsPaths = $oTestClassInstance->GetAdditionalFeaturePaths();
$this->aAdditionExtensionFoldersByCode = array_merge($this->aAdditionExtensionFoldersByCode, $aExtensionsPaths);
}
}
return $this->aCustomDatamodelFiles;
}
private function FindFilesModifiedAfter(float $fReferenceTimestamp, string $sPathToScan, array &$aModifiedFiles)

View File

@@ -1,63 +0,0 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Application;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
class LoginTest extends ItopDataTestCase
{
protected $sConfigTmpBackupFile;
protected $sConfigPath;
protected $sLoginMode;
protected function setUp(): void
{
parent::setUp();
clearstatcache();
// The test consists in requesting UI.php from outside iTop with a specific configuration
// Hence the configuration file must be tweaked on disk (and restored)
$this->sConfigPath = MetaModel::GetConfig()->GetLoadedFile();
$this->sConfigTmpBackupFile = tempnam(sys_get_temp_dir(), "config_");
file_put_contents($this->sConfigTmpBackupFile, file_get_contents($this->sConfigPath));
$oConfig = new \Config($this->sConfigPath);
$this->sLoginMode = "unimplemented_loginmode";
$oConfig->AddAllowedLoginTypes($this->sLoginMode);
@chmod($this->sConfigPath, 0770);
$oConfig->WriteToFile();
@chmod($this->sConfigPath, 0444);
}
protected function tearDown(): void
{
if (! is_null($this->sConfigTmpBackupFile) && is_file($this->sConfigTmpBackupFile)) {
//put config back
@chmod($this->sConfigPath, 0770);
file_put_contents($this->sConfigPath, file_get_contents($this->sConfigTmpBackupFile));
@chmod($this->sConfigPath, 0444);
@unlink($this->sConfigTmpBackupFile);
}
parent::tearDown();
}
protected function CallItopUrlByCurl($sUri, ?array $aPostFields = [])
{
$ch = curl_init();
$sUrl = MetaModel::GetConfig()->Get('app_root_url')."/$sUri";
curl_setopt($ch, CURLOPT_URL, $sUrl);
if (0 !== sizeof($aPostFields)) {
curl_setopt($ch, CURLOPT_POST, 1);// set post data to true
curl_setopt($ch, CURLOPT_POSTFIELDS, $aPostFields);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$sOutput = curl_exec($ch);
curl_close($ch);
return $sOutput;
}
}

View File

@@ -143,34 +143,12 @@ class QueryTest extends ItopDataTestCase
{
// compute request url
$url = $oQuery->GetExportUrl();
$aCurlOptions = [
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => self::USER.':'.self::PASSWORD,
];
// open curl
$curl = curl_init();
// curl options
curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($curl, CURLOPT_USERPWD, self::USER.':'.self::PASSWORD);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
// Force disable of certificate check as most of dev / test env have a self-signed certificate
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
// execute curl
$result = curl_exec($curl);
if (curl_errno($curl)) {
$info = curl_getinfo($curl);
var_export($info);
var_dump([
'url' => $url,
'app_root_url:' => MetaModel::GetConfig()->Get('app_root_url'),
'GetAbsoluteUrlAppRoot:' => \utils::GetAbsoluteUrlAppRoot(),
]);
}
// close curl
curl_close($curl);
return $result;
return $this->CallUrl($url, [], $aCurlOptions);
}
/** @inheritDoc */

View File

@@ -7,30 +7,26 @@
*
*
*/
$MySettings = array(
$MySettings = [
// app_root_url: Root URL used for navigating within the application, or from an email to the application (you can put $SERVER_NAME$ as a placeholder for the server's name)
// default: ''
'app_root_url' => 'http://%server(SERVER_NAME)?:localhost%/itop/iTop/',
);
];
/**
*
* Modules specific settings
*
*/
$MyModuleSettings = array(
);
$MyModuleSettings = [
];
/**
*
* Data model modules to be loaded. Names are specified as relative paths
*
*/
$MyModules = array(
);
?>
$MyModules = [
];

View File

@@ -7,30 +7,26 @@
*
*
*/
$MySettings = array(
$MySettings = [
// app_root_url: Root URL used for navigating within the application, or from an email to the application (you can put $SERVER_NAME$ as a placeholder for the server's name)
// default: ''
'app_root_url' => 'http://' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost') . '/itop/iTop/',
'app_root_url' => 'http://'.(isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost').'/itop/iTop/',
);
];
/**
*
* Modules specific settings
*
*/
$MyModuleSettings = array(
);
$MyModuleSettings = [
];
/**
*
* Data model modules to be loaded. Names are specified as relative paths
*
*/
$MyModules = array(
);
?>
$MyModules = [
];

View File

@@ -1,50 +0,0 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class ModuleDiscoveryTest extends ItopTestCase
{
public function GetModuleNameProvider()
{
return [
'nominal' => [
'sModuleId' => 'a/1.2.3',
'name' => 'a',
'version' => '1.2.3',
],
'develop' => [
'sModuleId' => 'a/1.2.3-dev',
'name' => 'a',
'version' => '1.2.3-dev',
],
'missing version => 1.0.0' => [
'sModuleId' => 'a/',
'name' => 'a',
'version' => '1.0.0',
],
'missing everything except name' => [
'sModuleId' => 'a',
'name' => 'a',
'version' => '1.0.0',
],
];
}
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
}
/**
* @dataProvider GetModuleNameProvider
*/
public function testGetModuleName($sModuleId, $expectedName, $expectedVersion)
{
$this->assertEquals([$expectedName, $expectedVersion], \ModuleDiscovery::GetModuleName($sModuleId));
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use AnalyzeInstallation;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use ModuleInstallationRepository;
class AnalyzeInstallationTest extends ItopTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/AnalyzeInstallation.php');
$this->RequireOnceItopFile('setup/ModuleInstallationRepository.php');
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
$this->RequireOnceItopFile('setup/runtimeenv.class.inc.php');
}
public static function AnalyzeInstallationProvider()
{
//$aModules = json_decode(file_get_contents(__DIR__.'/ressources/priv_modules.json'), true);
$aAnalyzeInstallationOutput = json_decode(file_get_contents(__DIR__.'/ressources/analyze_installation_output.json'), true);
$aAvailableModules = json_decode(file_get_contents(__DIR__.'/ressources/available_modules.json'), true);
return [
'new modules not in DB setup history' => [
'aAvailableModules' => [
'mandatory_module/1.0.0' => [
"mandatory" => true,
"ga" => "bu",
],
'optional_module/6.6.6' => [
"mandatory" => false,
"zo" => "meu",
],
],
'aInstalledModules' => [],
'expected' => [
'_Root_' => [
'installed_version' => '',
'available_version' => 'ITOP_VERSION_FULL',
'name_code' => 'ITOP_APPLICATION',
],
'mandatory_module' => [
"mandatory" => true,
'installed_version' => '',
'available_version' => '1.0.0',
'install' => [
'flag' => 2,
'message' => 'the module is part of the application',
],
"ga" => "bu",
],
'optional_module' => [
"mandatory" => false,
'installed_version' => '',
'available_version' => '6.6.6',
"zo" => "meu",
'install' => [
'flag' => 1,
'message' => '',
],
],
],
],
'new modules ALREADY in DB setup history' => [
'aAvailableModules' => [
'mandatory_module/1.0.0' => [
"mandatory" => true,
"ga" => "bu",
],
'optional_module/6.6.6' => [
"mandatory" => false,
"zo" => "meu",
],
],
'aInstalledModules' => json_decode(file_get_contents(__DIR__.'/ressources/priv_modules_simpleusecase.json'), true),
'expected' => [
'_Root_' => [
'installed_version' => '3.3.0-dev-svn',
'available_version' => 'ITOP_VERSION_FULL',
'name_code' => 'ITOP_APPLICATION',
],
'mandatory_module' => [
"mandatory" => true,
'installed_version' => '3.3.0',
'available_version' => '1.0.0',
"ga" => "bu",
'install' => [
'flag' => 2,
'message' => 'the module is part of the application',
],
'uninstall' => [
'flag' => 3,
'message' => 'the module is part of the application',
],
],
'optional_module' => [
"mandatory" => false,
'installed_version' => '3.3.0',
'available_version' => '6.6.6',
"zo" => "meu",
'install' => [
'flag' => 1,
'message' => '',
],
'uninstall' => [
'flag' => 1,
'message' => '',
],
],
],
],
'dummyfirst installation' => [
'aAvailableModules' => [],
'aInstalledModules' => [],
'expected' => [
'_Root_' => [
'installed_version' => '',
'available_version' => 'ITOP_VERSION_FULL',
'name_code' => 'ITOP_APPLICATION',
],
],
],
'dummy 2nd installation' => [
'aAvailableModules' => [],
'aInstalledModules' => json_decode(file_get_contents(__DIR__.'/ressources/priv_modules2.json'), true),
'expected' => [
'_Root_' => [
'installed_version' => '3.3.0-dev-svn',
'available_version' => 'ITOP_VERSION_FULL',
'name_code' => 'ITOP_APPLICATION',
],
],
],
'real_case' => [
'aAvailableModules' => $aAvailableModules,
'aInstalledModules' => json_decode(file_get_contents(__DIR__.'/ressources/priv_modules2.json'), true),
'expected' => $aAnalyzeInstallationOutput,
],
];
}
/**
* @dataProvider AnalyzeInstallationProvider
*/
public function testAnalyzeInstallation($aAvailableModules, $aInstalledModules, $expected)
{
$sContent = str_replace(['ITOP_VERSION_FULL', 'ITOP_APPLICATION'], [ITOP_VERSION_FULL, ITOP_APPLICATION], json_encode($expected));
$expected = json_decode($sContent, true);
$this->SetNonPublicProperty(AnalyzeInstallation::GetInstance(), "aAvailableModules", $aAvailableModules);
//$aModules = json_decode(file_get_contents(__DIR__.'/ressources/priv_modules2.json'), true);
$this->SetNonPublicProperty(ModuleInstallationRepository::GetInstance(), "aSelectInstall", $aInstalledModules);
$oConfig = $this->createMock(\Config::class);
$modulesPath = [
APPROOT.'extensions',
];
$aModules = AnalyzeInstallation::GetInstance()->AnalyzeInstallation($oConfig, $modulesPath, false, null);
$this->assertEquals($expected, $aModules);
}
}

View File

@@ -2,7 +2,9 @@
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use iTopExtension;
use MissingDependencyException;
use ModuleDiscovery;
@@ -15,6 +17,12 @@ class ModuleDiscoveryTest extends ItopTestCase
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
}
protected function tearDown(): void
{
parent::tearDown();
ModuleDiscovery::DeclareRemovedExtensions([]);
}
public function testOrderModulesByDependencies_RealExample()
{
$aModules = json_decode(file_get_contents(__DIR__.'/ressources/reallife_discovered_modules.json'), true);
@@ -77,4 +85,126 @@ TXT;
ModuleDiscovery::OrderModulesByDependencies($aModules, true, $aChoices);
}
public function testOrderModulesByDependencies_FailWhenChoosenModuleDependsOnRemovedExtensionModule()
{
$aChoices = ['id1', 'id2'];
$sModuleFilePath = $this->GetTemporaryFilePath();
$sModuleFilePath2 = $this->GetTemporaryFilePath();
$aModules = [
"id1/1" => [
'dependencies' => [ 'id2/2'],
'label' => 'label1',
ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath,
],
"id2/2" => [
'dependencies' => [],
'label' => 'label2',
ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath2,
],
];
$oExtension = $this->GivenExtensionWithModule('id2', '2', $sModuleFilePath2);
ModuleDiscovery::DeclareRemovedExtensions([$oExtension]);
$sExpectedMessage = <<<TXT
The following modules have unmet dependencies:
label1 (id: id1/1) depends on: ❌ id2/2
TXT;
$this->expectException(MissingDependencyException::class);
$this->expectExceptionMessage($sExpectedMessage);
ModuleDiscovery::OrderModulesByDependencies($aModules, true, $aChoices);
}
public function GetModuleNameProvider()
{
return [
'nominal' => [
'sModuleId' => 'a/1.2.3',
'name' => 'a',
'version' => '1.2.3',
],
'develop' => [
'sModuleId' => 'a/1.2.3-dev',
'name' => 'a',
'version' => '1.2.3-dev',
],
'missing version => 1.0.0' => [
'sModuleId' => 'a/',
'name' => 'a',
'version' => '1.0.0',
],
'missing everything except name' => [
'sModuleId' => 'a',
'name' => 'a',
'version' => '1.0.0',
],
];
}
/**
* @dataProvider GetModuleNameProvider
*/
public function testGetModuleName($sModuleId, $expectedName, $expectedVersion)
{
$this->assertEquals([$expectedName, $expectedVersion], ModuleDiscovery::GetModuleName($sModuleId));
}
public function testIsModuleInExtensionList_NoRemovedExtension()
{
$this->assertFalse($this->InvokeNonPublicStaticMethod(ModuleDiscovery::class, "IsModuleInExtensionList", [[], 'module_name', '123', []]));
}
public function testIsModuleInExtensionList_ModuleWithAnotherVersionIncludedInRemoveExtension()
{
$sModuleFilePath = $this->GetTemporaryFilePath();
$aExtensionList = [$this->GivenExtensionWithModule('module_name', '456', $sModuleFilePath)];
$this->AssertModuleIsPartOfRemovedExtension($aExtensionList, 'module_name', '123', $sModuleFilePath, false);
}
public function testIsModuleInExtensionList_AnotherModuleWithSameVersionIncludedInRemoveExtension()
{
$sModuleFilePath = $this->GetTemporaryFilePath();
$aExtensionList = [$this->GivenExtensionWithModule('module_name', '456', $sModuleFilePath)];
$this->AssertModuleIsPartOfRemovedExtension($aExtensionList, 'another_module_name', '456', $sModuleFilePath, false);
}
public function testIsModuleInExtensionList_SameExtensionComingFromAnotherLocation()
{
$sModuleFilePath = $this->GetTemporaryFilePath();
$sModuleFilePath2 = $this->GetTemporaryFilePath();
$aExtensionList = [$this->GivenExtensionWithModule('module_name', '456', $sModuleFilePath2)];
$this->AssertModuleIsPartOfRemovedExtension($aExtensionList, 'module_name', '456', $sModuleFilePath, false);
}
public function testIsModuleInExtensionList_ModuleShouldBeExcluded()
{
$sModuleFilePath = $this->GetTemporaryFilePath();
$aExtensionList = [$this->GivenExtensionWithModule('module_name', '456', $sModuleFilePath)];
$this->AssertModuleIsPartOfRemovedExtension($aExtensionList, 'module_name', '456', $sModuleFilePath, true);
}
public function AssertModuleIsPartOfRemovedExtension($aExtensionList, $sModuleName, $sModuleVersion, $sModuleFilePath, $bExpected)
{
$aCurrentModuleInfo = [
ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath,
];
$this->assertEquals(
$bExpected,
$this->InvokeNonPublicStaticMethod(ModuleDiscovery::class, "IsModuleInExtensionList", [$aExtensionList, $sModuleName, $sModuleVersion, $aCurrentModuleInfo])
);
}
private function GivenExtensionWithModule(string $sModuleName, string $sVersion, bool|string $sModuleFilePath): iTopExtension
{
$oExt = new iTopExtension();
$oExt->aModuleVersion[$sModuleName] = $sVersion;
$oExt->aModuleInfo[$sModuleName] = [
ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath,
];
return $oExt;
}
}

View File

@@ -0,0 +1,15 @@
<?php
class WizStepModulesChoiceFake extends WizStepModulesChoice
{
public function __construct(WizardController $oWizard, $sCurrentState)
{
$this->oWizard = $oWizard;
$this->sCurrentState = $sCurrentState;
}
public function setExtensionMap(iTopExtensionsMap $oMap)
{
$this->oExtensionsMap = $oMap;
}
}

View File

@@ -0,0 +1,562 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
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();
$this->RequireOnceItopFile('/setup/unattended-install/InstallationFileService.php');
require_once __DIR__.'/iTopExtensionsMapFake.php';
require_once __DIR__.'/WizStepModulesChoiceFake.php';
$this->oStep = new WizStepModulesChoiceFake(new WizardController('', ''), '');
ModuleDiscovery::ResetCache();
}
public function ProviderComputeChoiceFlags()
{
return [
'A not selected, not installed extension should not be checked and be enabled' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
],
'bCurrentSelected' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => false,
'checked' => false,
],
],
'A selected but not installed extension should be checked and enabled' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
],
'bCurrentSelected' => true,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => false,
'checked' => true,
],
],
'A missing extension should be disabled and unchecked' => [
'aExtensionsOnDiskOrDb' => [
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'missing' => true,
'uninstallable' => true,
],
'bCurrentSelected' => 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,
'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,
'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' => [
'installed' => true,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
],
'bCurrentSelected' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => true,
'disabled' => false,
'checked' => false,
],
],
'An installed non uninstallable extension should be checked and disabled' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => true,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => false,
],
'bCurrentSelected' => false,
'aExpectedFlags' => [
'uninstallable' => false,
'missing' => false,
'installed' => true,
'disabled' => true,
'checked' => true,
],
],
'A mandatory extension should be checked and disabled' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => true,
'uninstallable' => true,
],
'bCurrentSelected' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => true,
'checked' => true,
],
],
'An optional sub extension should not force its parent flags' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
],
'itop-ext1-1' => [
'installed' => false,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'sub_options' => [
'options' => [
[
'extension_code' => 'itop-ext1-1',
'mandatory' => false,
'uninstallable' => true,
],
],
],
],
'bCurrentSelected' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => false,
'checked' => false,
],
],
'A mandatory sub extension should force its parent to be checked and disabled' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
],
'itop-ext1-1' => [
'installed' => false,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'sub_options' => [
'options' => [
[
'extension_code' => 'itop-ext1-1',
'mandatory' => true,
'uninstallable' => true,
],
],
],
],
'bCurrentSelected' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => true,
'checked' => true,
],
],
'An installed non uninstallable sub extension should force its parent to be checked and disabled' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => true,
],
'itop-ext1-1' => [
'installed' => true,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'sub_options' => [
'options' => [
[
'extension_code' => 'itop-ext1-1',
'mandatory' => false,
'uninstallable' => false,
],
],
],
],
'bCurrentSelected' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => true,
'disabled' => true,
'checked' => true,
],
],
'A non installed non uninstallable sub extension should not force its parent flags' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => true,
],
'itop-ext1-1' => [
'installed' => false,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'sub_options' => [
'options' => [
[
'extension_code' => 'itop-ext1-1',
'mandatory' => false,
'uninstallable' => false,
],
],
],
],
'bCurrentSelected' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => true,
'disabled' => false,
'checked' => false,
],
],
];
}
/**
* @dataProvider ProviderComputeChoiceFlags
*/
public function testComputeChoiceFlags($aExtensionsOnDiskOrDb, $aWizardStepDefinition, $bIsCurrentSelected, $aExpectedFlags)
{
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsOnDiskOrDb));
$aFlags = $this->oStep->ComputeChoiceFlags($aWizardStepDefinition, '_0', $bIsCurrentSelected ? ['_0' => '_0'] : [], false, false, true);
$this->assertEquals($aExpectedFlags, $aFlags);
}
public function ProviderGetAddedAndRemovedExtensions()
{
return [
'no extensions' => [
'aExtensionsOnDiskOrDb' => [],
'aSelected' => [],
'aExpectedAddedList' => [],
'aExpectedRemovedList' => [],
],
'no extensions added nor removed' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
],
],
'aSelected' => [],
'aExpectedAddedList' => [],
'aExpectedRemovedList' => [],
],
'One added extension' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
],
],
'aSelected' => ['itop-ext1'],
'aExpectedAddedList' => ['itop-ext1' => 'itop-ext1'],
'aExpectedRemovedList' => [],
],
'One removed extension' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => true,
],
],
'aSelected' => [],
'aExpectedAddedList' => [],
'aExpectedRemovedList' => ['itop-ext1' => 'itop-ext1'],
],
'Forced removed extension' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => true,
'uninstallable' => false,
],
],
'aSelected' => [],
'aExpectedAddedList' => [],
'aExpectedRemovedList' => ['itop-ext1' => 'itop-ext1'],
],
'added and removed extensions' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext-added1' => [
'installed' => false,
],
'itop-ext-added2' => [
'installed' => false,
],
'itop-ext-removed1' => [
'installed' => true,
],
'itop-ext-removed2' => [
'installed' => true,
],
],
'aSelected' => ['itop-ext-added1', 'itop-ext-added2'],
'aExpectedAddedList' => ['itop-ext-added1' => 'itop-ext-added1', 'itop-ext-added2' => 'itop-ext-added2'],
'aExpectedRemovedList' => ['itop-ext-removed1' => 'itop-ext-removed1', 'itop-ext-removed2' => 'itop-ext-removed2'],
],
];
}
/**
* @dataProvider ProviderGetAddedAndRemovedExtensions
*/
public function testGetAddedAndRemovedExtensions($aExtensionsOnDiskOrDb, $aSelectedExtensions, $aExpectedAddedList, $aExpectedRemovedList)
{
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsOnDiskOrDb));
[$aAddedList, $aRemovedList, $aNotUninstallableList] = $this->oStep->GetAddedAndRemovedExtensions($aSelectedExtensions);
$this->assertEquals($aExpectedAddedList, $aAddedList);
$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;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup\FeatureRemoval;
use Combodo\iTop\Setup\FeatureRemoval\ModelReflectionSerializer;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
class ModelSerializationTest extends ItopDataTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('/setup/feature_removal/ModelReflectionSerializer.php');
}
public function testGetModelFromEnvironment()
{
$aModel = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->GetTestEnvironment());
$this->assertEqualsCanonicalizing(MetaModel::GetClasses(), $aModel);
}
public function testGetModelFromEnvironmentFailure()
{
$this->expectException(\CoreException::class);
$this->expectExceptionMessage("Cannot get classes");
ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment('gabuzomeu');
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup\FeatureRemoval;
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
use Combodo\iTop\Setup\FeatureRemoval\InplaceSetupAudit;
use Combodo\iTop\Setup\FeatureRemoval\ModelReflectionSerializer;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use Combodo\iTop\Test\UnitTest\Service\UnitTestRunTimeEnvironment;
use MetaModel;
class SetupAuditTest extends ItopCustomDatamodelTestCase
{
public const ENVT = 'php-unit-extensionremoval-tests';
public function GetDatamodelDeltaAbsPath(): string
{
//no delta: empty path provided
return "";
}
public function GetAdditionalFeaturePaths(): array
{
$aFeaturePaths = [];
foreach (glob(__DIR__."/additional_features/*", GLOB_ONLYDIR) as $aFeaturePath) {
$sCode = basename($aFeaturePath);
$aFeaturePaths[$sCode] = $aFeaturePath;
}
return $aFeaturePaths;
}
protected function setUp(): void
{
static::LoadRequiredItopFiles();
$this->oEnvironment = new UnitTestRunTimeEnvironment(self::ENVT);
$this->oEnvironment->bUseDelta = false;
$this->oEnvironment->bUseAdditionalFeatures = true;
parent::setUp();
$this->RequireOnceItopFile('/setup/feature_removal/SetupAudit.php');
$this->RequireOnceItopFile('/setup/feature_removal/InplaceSetupAudit.php');
$this->RequireOnceItopFile('/setup/feature_removal/DryRemovalRuntimeEnvironment.php');
}
public function GetTestEnvironment(): string
{
return self::ENVT;
}
public function testComputeDryRemoval()
{
$oDryRemovalRuntimeEnvt = new DryRemovalRuntimeEnvironment();
$oDryRemovalRuntimeEnvt->Prepare($this->GetTestEnvironment(), ['nominal_ext1', 'finalclass_ext2']);
$oDryRemovalRuntimeEnvt->CompileFrom($this->GetTestEnvironment());
$oSetupAudit = new SetupAudit(MetaModel::GetEnvironment(), DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV);
$expected = [
"Feature1Module1MyClass",
"FinalClassFeature2Module1MyClass",
"FinalClassFeature2Module1MyFinalClassFromLocation",
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetRemovedClasses());
$expected = [
"FinalClassFeature2Module1MyFinalClassFromLocation" => 0,
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetIssues());
}
public function testGetRemovedClassesFromSetupWizard()
{
$sEnv = MetaModel::GetEnvironment();
$aClassesBeforeRemoval = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($sEnv);
$aClassesBeforeRemoval[] = "GabuZomeu";
$oSetupAudit = new InplaceSetupAudit($aClassesBeforeRemoval, $sEnv);
$oSetupAudit->ComputeClasses();
$this->assertEquals(["GabuZomeu"], $oSetupAudit->GetRemovedClasses());
}
public function testGetIssues()
{
$sUID = "AuditExtensionsCleanupRules_".uniqid();
$oOrg = $this->CreateOrganization($sUID);
$this->createObject('FinalClassFeature1Module1MyFinalClassFromLocation', ['org_id' => $oOrg->GetKey(), 'name' => $sUID, 'name2' => uniqid()]);
$oSetupAudit = new SetupAudit(MetaModel::GetEnvironment(), DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV);
$aRemovedClasses = [
"Feature1Module1MyClass",
"FinalClassFeature1Module1MyClass",
"FinalClassFeature1Module1MyFinalClassFromLocation",
"FinalClassFeature2Module1MyClass",
"FinalClassFeature2Module1MyFinalClassFromLocation",
];
//avoid setup dry computation
$this->SetNonPublicProperty($oSetupAudit, 'aRemovedClasses', $aRemovedClasses);
$expected = [
"FinalClassFeature1Module1MyFinalClassFromLocation" => 1,
"FinalClassFeature2Module1MyFinalClassFromLocation" => 0,
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetIssues());
}
public function testAuditExtensionsCleanupRulesFailASAP()
{
$sUID = "AuditExtensionsCleanupRules_".uniqid();
$oOrg = $this->CreateOrganization($sUID);
$this->createObject('FinalClassFeature1Module1MyFinalClassFromLocation', ['org_id' => $oOrg->GetKey(), 'name' => $sUID, 'name2' => uniqid()]);
$this->createObject('FinalClassFeature2Module1MyFinalClassFromLocation', ['org_id' => $oOrg->GetKey(), 'name' => $sUID, 'name2' => uniqid()]);
$oSetupAudit = new SetupAudit(MetaModel::GetEnvironment(), DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV);
$aRemovedClasses = [
"Feature1Module1MyClass",
"FinalClassFeature1Module1MyClass",
"FinalClassFeature1Module1MyFinalClassFromLocation",
"FinalClassFeature2Module1MyClass",
"FinalClassFeature2Module1MyFinalClassFromLocation",
];
//avoid setup dry computation
$this->SetNonPublicProperty($oSetupAudit, 'aRemovedClasses', $aRemovedClasses);
$expected = [
"FinalClassFeature1Module1MyFinalClassFromLocation" => 1,
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetIssues(true));
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension format="1.0">
<extension_code>finalclass_ext1</extension_code>
<company>Combodo SARL</company>
<author><![CDATA[Odain]]></author>
<label><![CDATA[Ext For Test]]></label>
<description><![CDATA[Ext For Test]]></description>
<version>6.6.6</version>
<modules type="array">
<module>
<id>finalclass_ext1_module1</id>
<version>tags/6.6.6</version>
</module>
</modules>
<release_date>2023-07-19</release_date>
<version_description><![CDATA[
]]></version_description>
<itop_version_min>3.2.0</itop_version_min>
<status></status>
<mandatory>false</mandatory>
<more_info_url></more_info_url>
</extension>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0">
<classes>
<class id="FinalClassFeature1Module1MyFinalClassFromLocation" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>FinalClassFeature1Module1MyFinalClassFromLocation</db_table>
<naming>
<attributes>
<attribute id="name2"/>
<attribute id="finalclass"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name2"/>
<attribute id="finalclass"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name2" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name2" xsi:type="AttributeString">
<sql>name2</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
<methods/>
<presentation/>
<parent>Location</parent>
</class>
<class id="FinalClassFeature1Module1MyClass" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>FinalClassFeature1Module1MyClass</db_table>
<naming>
<attributes>
<attribute id="name"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
</field>
</fields>
<methods/>
<presentation/>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<menus/>
<user_rights/>
<module_parameters/>
</itop_design>

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 use supply a datamodel.xxxx.xml file with your module
//// the 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) 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,51 @@
<?php
/*
* @copyright Copyright (C) 2010-2021 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
'finalclass_ext1_module1/6.6.6',
[
// Identification
//
'label' => 'Ext For Test',
'category' => 'business',
// Setup
//
'dependencies' => [
'itop-structure/3.2.0',
],
'mandatory' => false,
'visible' => true,
'installer' => '',
// Components
//
'datamodel' => [
'model.finalclass_ext1_module1.php',
],
'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,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension format="1.0">
<extension_code>finalclass_ext2</extension_code>
<company>Combodo SARL</company>
<author><![CDATA[Odain]]></author>
<label><![CDATA[Ext For Test]]></label>
<description><![CDATA[Ext For Test]]></description>
<version>6.6.6</version>
<modules type="array">
<module>
<id>finalclass_ext2_module1</id>
<version>tags/6.6.6</version>
</module>
</modules>
<release_date>2023-07-19</release_date>
<version_description><![CDATA[
]]></version_description>
<itop_version_min>3.2.0</itop_version_min>
<status></status>
<mandatory>false</mandatory>
<more_info_url></more_info_url>
</extension>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0">
<classes>
<class id="FinalClassFeature2Module1MyFinalClassFromLocation" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>FinalClassFeature2Module1MyFinalClassFromLocation</db_table>
<naming>
<attributes>
<attribute id="name2"/>
<attribute id="finalclass"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name2"/>
<attribute id="finalclass"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name2" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name2" xsi:type="AttributeString">
<sql>name2</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
<methods/>
<presentation/>
<parent>Location</parent>
</class>
<class id="FinalClassFeature2Module1MyClass" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>FinalClassFeature2Module1MyClass</db_table>
<naming>
<attributes>
<attribute id="name"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
</field>
</fields>
<methods/>
<presentation/>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<menus/>
<user_rights/>
<module_parameters/>
</itop_design>

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 use supply a datamodel.xxxx.xml file with your module
//// the 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) 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,51 @@
<?php
/*
* @copyright Copyright (C) 2010-2021 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
'finalclass_ext2_module1/6.6.6',
[
// Identification
//
'label' => 'Ext For Test',
'category' => 'business',
// Setup
//
'dependencies' => [
'itop-structure/3.2.0',
],
'mandatory' => false,
'visible' => true,
'installer' => '',
// Components
//
'datamodel' => [
'model.finalclass_ext2_module1.php',
],
'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,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension format="1.0">
<extension_code>nominal_ext1</extension_code>
<company>Combodo SARL</company>
<author><![CDATA[Odain]]></author>
<label><![CDATA[Ext For Test]]></label>
<description><![CDATA[Ext For Test]]></description>
<version>6.6.6</version>
<modules type="array">
<module>
<id>nominal_ext1_module1</id>
<version>tags/6.6.6</version>
</module>
</modules>
<release_date>2023-07-19</release_date>
<version_description><![CDATA[
]]></version_description>
<itop_version_min>3.2.0</itop_version_min>
<status></status>
<mandatory>false</mandatory>
<more_info_url></more_info_url>
</extension>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0">
<classes>
<class id="Feature1Module1MyClass" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>Feature1Module1MyClass</db_table>
<naming>
<attributes>
<attribute id="name"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
</field>
</fields>
<methods/>
<presentation/>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<menus/>
<user_rights/>
<module_parameters/>
</itop_design>

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 use supply a datamodel.xxxx.xml file with your module
//// the 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) is via the XML definition.
//// This file remains in the module's template only for the cases where there is:
//// - either no new class or menu defined in the XML file
//// - or no XML file at all supplied by the module

View File

@@ -1,39 +1,41 @@
<?php
/*
* @copyright Copyright (C) 2010-2021 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
'itop-flow-map/3.3.0',
'nominal_ext1_module1/6.6.6',
[
// Identification
//
'label' => 'Map applications data flows',
'category' => 'business',
'label' => 'Ext For Test',
'category' => 'business',
// Setup
//
'dependencies' => [
'itop-config-mgmt/3.2.0',
'itop-structure/3.2.0',
],
'mandatory' => false,
'visible' => true,
'installer' => '',
// Components
//
'datamodel' => [
'model.itop-flow-map.php', // Contains the PHP code generated by the "compilation" of datamodel.Combodo-flow-map.xml
'model.nominal_ext1_module1.php',
],
'webservice' => [
'webservice' => [],
'data.struct' => [// add your 'structure' definition XML files here,
],
'data.struct' => [
'data/en_us.data.itop-flow-map.xml',
],
'data.sample' => [
// add your sample data XML files here,
'data.sample' => [// add your sample data XML files here,
],
// Documentation
@@ -43,8 +45,7 @@ SetupWebPage::AddModule(
// Default settings
//
'settings' => [
// Module specific settings go here, if any
'settings' => [// Module specific settings go here, if any
],
]
);

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

@@ -0,0 +1,29 @@
<?php
class iTopExtensionsMapFake extends iTopExtensionsMap
{
public function __construct($sFromEnvironment = 'production', $aExtraDirs = [])
{
$this->aExtensions = [];
$this->aExtensionsByCode = [];
$this->aScannedDirs = [];
}
public static function createFromArray($aExtensions)
{
$oMap = new static();
foreach ($aExtensions as $sCode => $aExtension) {
$oExtension = new iTopExtension();
$oExtension->sCode = $sCode;
$oExtension->sLabel = $sCode;
$oExtension->bInstalled = $aExtension['installed'];
$oExtension->aModules = $aExtension['modules'] ?? [];
$oExtension->bCanBeUninstalled = $aExtension['uninstallable'] ?? null;
$oExtension->sVersion = $aExtension['version'] ?? '1.0.0';
$oExtension->aModuleInfo = [];
$oMap->AddExtension($oExtension);
}
return $oMap;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8"?>
<installation>
<steps type="array">
<step>
<title>Configuration Management options</title>
<description><![CDATA[<h2>The options below allow you to configure the type of elements that are to be managed inside iTop.</h2>]]></description>
<banner>/images/modules.png</banner>
<options type="array">
<choice>
<extension_code>itop-config-mgmt-core</extension_code>
<title>Configuration Management Core</title>
<description>All the base objects that are mandatory in the iTop CMDB: Organizations, Locations, Teams, Persons, etc.</description>
<modules type="array">
<module>itop-config-mgmt</module>
<module>itop-attachments</module>
<module>itop-profiles-itil</module>
<module>itop-welcome-itil</module>
<module>itop-tickets</module>
<module>itop-files-information</module>
<module>combodo-db-tools</module>
<module>itop-core-update</module>
<module>itop-hub-connector</module>
<module>itop-oauth-client</module>
</modules>
<mandatory>true</mandatory>
</choice>
<choice>
<extension_code>itop-config-mgmt-datacenter</extension_code>
<title>Data Center Devices</title>
<description>Manage Data Center devices such as Racks, Enclosures, PDUs, etc.</description>
<modules type="array">
<module>itop-datacenter-mgmt</module>
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-config-mgmt-end-user</extension_code>
<title>End-User Devices</title>
<description>Manage devices related to end-users: PCs, Phones, Tablets, etc.</description>
<modules type="array">
<module>itop-endusers-devices</module>
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-config-mgmt-storage</extension_code>
<title>Storage Devices</title>
<description>Manage storage devices such as NAS, SAN Switches, Tape Libraries and Tapes, etc.</description>
<modules type="array">
<module>itop-storage-mgmt</module>
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-config-mgmt-virtualization</extension_code>
<title>Virtualization</title>
<description>Manage Hypervisors, Virtual Machines and Farms.</description>
<modules type="array">
<module>itop-virtualization-mgmt</module>
</modules>
<default>true</default>
</choice>
</options>
</step>
<step>
<title>Service Management options</title>
<description><![CDATA[<h2>Select the choice that best describes the relationships between the services and the IT infrastructure in your IT environment.</h2>]]></description>
<banner>./wizard-icons/service.png</banner>
<alternatives type="array">
<choice>
<extension_code>itop-service-mgmt-enterprise</extension_code>
<title>Service Management for Enterprises</title>
<description>Select this option if the IT delivers services based on a shared infrastructure. For example if different organizations within your company subscribe to services (like Mail and Print services) delivered by a single shared backend.</description>
<modules type="array">
<module>itop-service-mgmt</module>
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-service-mgmt-service-provider</extension_code>
<title>Service Management for Service Providers</title>
<description>Select this option if the IT manages the infrastructure of independent customers. This is the most flexible model, since the services can be delivered with a mix of shared and customer specific infrastructure devices.</description>
<modules type="array">
<module>itop-service-mgmt-provider</module>
</modules>
</choice>
</alternatives>
</step>
<step>
<title>Tickets Management options</title>
<description><![CDATA[<h2>Select the type of tickets you want to use in order to respond to user requests and incidents.</h2>]]></description>
<banner>./itop-incident-mgmt-itil/images/incident-escalated.svg</banner>
<alternatives type="array">
<choice>
<extension_code>itop-ticket-mgmt-simple-ticket</extension_code>
<title>Simple Ticket Management</title>
<description>Select this option to use one single type of tickets for all kind of requests.</description>
<modules type="array">
<module>itop-request-mgmt</module>
</modules>
<default>true</default>
<sub_options>
<options type="array">
<choice>
<extension_code>itop-ticket-mgmt-simple-ticket-enhanced-portal</extension_code>
<title>Customer Portal</title>
<description><![CDATA[Modern & responsive portal for the end-users]]></description>
<modules type="array">
<module>itop-portal</module>
<module>itop-portal-base</module>
</modules>
<default>true</default>
</choice>
</options>
</sub_options>
</choice>
<choice>
<extension_code>itop-ticket-mgmt-itil</extension_code>
<title>ITIL Compliant Tickets Management</title>
<description>Select this option to have different types of ticket for managing user requests and incidents. Each type of ticket has a specific life cycle and specific fields</description>
<sub_options>
<options type="array">
<choice>
<extension_code>itop-ticket-mgmt-itil-user-request</extension_code>
<title>User Request Management</title>
<description>Manage User Request tickets in iTop</description>
<modules type="array">
<module>itop-request-mgmt-itil</module>
</modules>
</choice>
<choice>
<extension_code>itop-ticket-mgmt-itil-incident</extension_code>
<title>Incident Management</title>
<description>Manage Incidents tickets in iTop</description>
<modules type="array">
<module>itop-incident-mgmt-itil</module>
</modules>
</choice>
<choice>
<extension_code>itop-ticket-mgmt-itil-enhanced-portal</extension_code>
<title>Customer Portal</title>
<description><![CDATA[Modern & responsive portal for the end-users]]></description>
<modules type="array">
<module>itop-portal</module>
<module>itop-portal-base</module>
</modules>
<default>true</default>
</choice>
</options>
</sub_options>
</choice>
<choice>
<extension_code>itop-ticket-mgmt-none</extension_code>
<title>No Tickets Management</title>
<description>Don't manage incidents or user requests in iTop</description>
<modules type="array">
</modules>
</choice>
</alternatives>
</step>
<step>
<title>Change Management options</title>
<description><![CDATA[<h2>Select the type of tickets you want to use in order to manage changes to the IT infrastructure.</h2>]]></description>
<banner>./itop-change-mgmt/images/change.svg</banner>
<alternatives type="array">
<choice>
<extension_code>itop-change-mgmt-simple</extension_code>
<title>Simple Change Management</title>
<description>Select this option to use one type of ticket for all kind of changes.</description>
<modules type="array">
<module>itop-change-mgmt</module>
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-change-mgmt-itil</extension_code>
<title>ITIL Change Management</title>
<description>Select this option to use Normal/Routine/Emergency change tickets.</description>
<modules type="array">
<module>itop-change-mgmt-itil</module>
</modules>
</choice>
<choice>
<extension_code>itop-change-mgmt-none</extension_code>
<title>No Change Management</title>
<description>Don't manage changes in iTop</description>
<modules type="array">
</modules>
</choice>
</alternatives>
</step>
<step>
<title>Additional ITIL tickets</title>
<description><![CDATA[<h2>Pick from the list below the additional ITIL processes that are to be implemented in iTop.</h2>]]></description>
<banner>./itop-knownerror-mgmt/images/known-error.svg</banner>
<options type="array">
<choice>
<extension_code>itop-kown-error-mgmt</extension_code>
<title>Known Errors Management</title>
<description>Select this option to track "Known Errors" and FAQs in iTop.</description>
<modules type="array">
<module>itop-knownerror-mgmt</module>
</modules>
</choice>
<choice>
<extension_code>itop-problem-mgmt</extension_code>
<title>Problem Management</title>
<description>Select this option track "Problems" in iTop.</description>
<modules type="array">
<module>itop-problem-mgmt</module>
</modules>
</choice>
</options>
</step>
</steps>
</installation>

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