Compare commits

...

53 Commits

Author SHA1 Message Date
Timothee
8aac64cc19 N°8864 Added tests 2026-01-05 10:34:42 +01:00
Timothee
f668d94bd9 N°8864 Style fix 2025-12-23 16:20:33 +01:00
Timothee
00e071228e N°8864 List forced uninstall 2025-12-23 16:20:33 +01:00
Timothee
19b7f851f7 N°8864 List extensions added or removed in recap 2025-12-23 16:20:33 +01:00
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
Anne-Cath
df567fb9fe N°8786 - configuration: allow conditions on the allowed_login_types field 2025-12-09 18:24:44 +01:00
Timothee
028767768c N°8763 Code style fix 2025-12-09 16:04:50 +01:00
Timothee
11ec80830e N°8763 Fix issue with previously installed extensions not installed after upgrade 2025-12-09 15:46:10 +01:00
odain
edf992ef2b N°8724 - reduce log verbosity on ModuleInstallerAPI calls 2025-12-09 10:28:24 +01:00
Anne-Cath
7c8fb1a51d N°8852 - improve display attachment in properties tab 2025-12-09 09:15:51 +01:00
Anne-Cath
aa27b3601b N°8786 - configuration: allow conditions on the allowed_login_types field 2025-12-08 16:47:30 +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
70 changed files with 14901 additions and 581 deletions

View File

@@ -211,6 +211,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => true,
],
'allowed_login_types' => [
'type' => 'string',
'description' => 'List of login types allowed (separated by | ): form, external, basic, token',
'default' => DEFAULT_ALLOWED_LOGIN_TYPES,
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => true,
],
'app_icon_url' => [
'type' => 'string',
'description' => 'Hyperlink to redirect the user when clicking on the application icon (in the main window, or login/logoff pages)',
@@ -2410,6 +2418,7 @@ class Config
public function SetAllowedLoginTypes($aAllowedLoginTypes)
{
$this->m_sAllowedLoginTypes = implode('|', $aAllowedLoginTypes);
$this->Set('allowed_login_types', implode('|', $aAllowedLoginTypes));
}
/**

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",

View File

@@ -5,10 +5,12 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSetUIBlockFactory;
use Combodo\iTop\Application\WebPage\WebPage;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Service\Events\iEventServiceSetup;
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
class AttachmentPlugIn implements iApplicationUIExtension, iEventServiceSetup
{
@@ -236,10 +238,14 @@ class AttachmentPlugIn implements iApplicationUIExtension, iEventServiceSetup
}
$oAttachmentsRenderer = AttachmentsRendererFactory::GetInstance($oPage, $sObjClass, $iObjKey, $sTransactionId);
$iCount = $oAttachmentsRenderer->GetAttachmentsSet()->Count() + $oAttachmentsRenderer->GetTempAttachmentsSet()->Count();
$sTitle = ($iCount > 0) ? Dict::Format('Attachments:TabTitle_Count', $iCount) : Dict::S('Attachments:EmptyTabTitle');
if ($this->GetAttachmentsPosition() === 'relations') {
$iCount = $oAttachmentsRenderer->GetAttachmentsSet()->Count() + $oAttachmentsRenderer->GetTempAttachmentsSet()->Count();
$sTitle = ($iCount > 0) ? Dict::Format('Attachments:TabTitle_Count', $iCount) : Dict::S('Attachments:EmptyTabTitle');
$oPage->SetCurrentTab('Attachments:Tab', $sTitle);
} else {
$oBlock = FieldSetUIBlockFactory::MakeStandard($sTitle);
$oBlock->AddSubBlock(new Html(''));
$oPage->AddUiBlock($oBlock);
}
$bIsReadOnlyState = self::IsReadonlyState($oObject, $oObject->GetState(), AttachmentPlugIn::ENUM_GUI_BACKOFFICE);

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

@@ -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

@@ -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 = ModuleInstallationService::GetInstance()->ReadFromDB($oConfig);
foreach ($aInstalledModules as $aDBInfo) {
$aConfiguration['itop_modules'][$aDBInfo['name']] = $aDBInfo['version'];

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

@@ -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__.'/ModuleInstallationService.php';
class AnalyzeInstallation
{
private static AnalyzeInstallation $oInstance;
private ?array $aAvailableModules = null;
private ?array $aSelectInstall = 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
{
static::$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 = ModuleInstallationService::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,184 @@
<?php
class ModuleInstallationService
{
private static ModuleInstallationService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): ModuleInstallationService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModuleInstallationService();
}
return self::$oInstance;
}
final public static function SetInstance(?ModuleInstallationService $oInstance): void
{
static::$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
$this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name'));
$this->log_error('Exception '.$e->getMessage());
return false;
}
$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'];
}
$this->log_info("GetApplicationVersion returns: product_name: ".$aResult['product_name'].', product_version: '.$aResult['product_version']);
return empty($aResult) ? false : $aResult;
}
private function ComputeInstalledModules(array $aSelectInstall): array
{
$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;
}
}

View File

@@ -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';
}

View File

@@ -148,32 +148,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 +190,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 +269,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 +279,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,13 +305,31 @@ class iTopExtensionsMap
*/
public function GetFromExtensionCode(string $sExtensionCode): ?iTopExtension
{
foreach ($this->aExtensions as $oExtension) {
if ($oExtension->sCode === $sExtensionCode) {
return $oExtension;
return $this->aExtensionsByCode[$sExtensionCode] ?? null;
}
/*public function GetMissingExtensions(array $aSelectedExtensions)
{
\SetupLog::Info(__METHOD__, null, ['selected' => $aSelectedExtensions]);
$aExtensionsFromDb = array_keys($this->aExtensionsByCode);
sort($aExtensionsFromDb);
\SetupLog::Info(__METHOD__, null, ['found' => $aExtensionsFromDb]);
$aRes = [];
foreach (array_diff($aExtensionsFromDb, $aSelectedExtensions) as $sExtensionCode) {
$oExtension = $this->GetFromExtensionCode($sExtensionCode);
if (!is_null($oExtension) && $oExtension->bVisible && $oExtension->sSource != iTopExtension::SOURCE_WIZARD) {
\SetupLog::Info(__METHOD__."$sExtensionCode", null, ['visible' => $oExtension->bVisible, 'mandatory' => $oExtension->bMandatory]);
$aRes [] = $sExtensionCode;
} else {
\SetupLog::Info(__METHOD__." MISSING $sExtensionCode");
}
}
return null;
}
\SetupLog::Info(__METHOD__, null, $aRes);
return $aRes;
}*/
/**
* Read (recursively) a directory to find if it contains extensions (or modules)
@@ -346,19 +389,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 +421,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 +449,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 +463,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
@@ -454,7 +499,8 @@ class iTopExtensionsMap
*/
public function GetAllExtensionsWithPreviouslyInstalled(): array
{
return array_merge($this->aExtensions, $this->aInstalledExtensions ?? []);
//Mind the order, local extensions data must overwrite installed extensions data since installed extensions does not have the associated modules.
return array_merge($this->aInstalledExtensions ?? [], $this->aExtensions);
}
/**
@@ -465,11 +511,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;
}
}
@@ -480,11 +532,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;
}
@@ -496,11 +548,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;
}
}
@@ -526,7 +576,12 @@ 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);
}
@@ -584,8 +639,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,97 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use MetaModel;
use RunTimeEnvironment;
use SetupUtils;
class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
{
public const DRY_REMOVAL_AUDIT_ENV = "extension-removal";
protected array $aExtensionsByCode;
private bool $bExtensionMapModified = false;
/**
* Toolset for building a run-time environment
*
* @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");
if (count($aExtensionCodesToRemove) > 0) {
$this->RemoveExtensionsLocally($aExtensionCodesToRemove);
}
$oDryRemovalConfig = clone(MetaModel::GetConfig());
$oDryRemovalConfig->ChangeModulesPath($sSourceEnv, $this->sFinalEnv);
$this->WriteConfigFileSafe($oDryRemovalConfig);
}
private function RemoveExtensionsLocally(array $aExtensionCodes): void
{
$oExtensionsMap = new \iTopExtensionsMap($this->sFinalEnv);
foreach ($aExtensionCodes as $sCode) {
/** @var \iTopExtension $oExtension */
$oExtension = $oExtensionsMap->GetFromExtensionCode($sCode);
if (!is_null($oExtension)) {
$sDir = $oExtension->sSourceDir;
\IssueLog::Info(__METHOD__.": remove extension locally", null, [$oExtension->sCode => $sDir]);
SetupUtils::rrmdir($sDir);
} else {
\IssueLog::Warning(__METHOD__." cannot find extensions", null, ['env' => $this->sFinalEnv, 'code' => $sCode]);
}
}
}
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");
}
/**
* @return \iTopExtensionsMap|null
*/
/*protected function GetExtensionMap(): ?iTopExtensionsMap
{
if (is_null(parent::GetExtensionMap())) {
return null;
}
if (!$this->bExtensionMapModified) {
$this->bExtensionMapModified = true;
foreach ($this->aExtensionsByCode as $sCode) {
parent::GetExtensionMap()->RemoveExtension($sCode);
}
}
return parent::GetExtensionMap();
}*/
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use CoreException;
use Exception;
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(\MetaModel::GetConfig()->Get('php_path'));
$sOutput = "";
$iRes = 0;
exec(sprintf("$sPHPExec %s/get_model_reflection.php --env='%s'", __DIR__, $sEnv), $sOutput, $iRes);
if ($iRes != 0) {
\IssueLog::Error("Cannot get classes", null, ['env' => $sEnv, 'code' => $iRes, "output" => $sOutput]);
throw new CoreException("Cannot get classes");
}
$aClasses = json_decode($sOutput[0] ?? null, true);
if (false === $aClasses) {
\IssueLog::Error("Invalid JSON", null, ["output" => $sOutput]);
throw new Exception("cannot get classes");
}
if (!is_array($aClasses)) {
\IssueLog::Error("not an array", null, ["classes" => $aClasses]);
throw new Exception("cannot get classes");
}
return $aClasses;
}
}

View File

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

View File

@@ -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

@@ -120,10 +120,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'");

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
@@ -145,12 +162,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 +185,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 +231,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 +248,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 +288,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;
}
}
@@ -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,7 +547,6 @@ class RunTimeEnvironment
// Record installed modules and extensions
//
$aAvailableExtensions = [];
$aAvailableModules = $this->AnalyzeInstallation($oConfig, $this->GetBuildDir());
foreach ($aSelectedModuleCodes as $sModuleId) {
if (!array_key_exists($sModuleId, $aAvailableModules)) {
@@ -669,7 +554,7 @@ class RunTimeEnvironment
}
$aModuleData = $aAvailableModules[$sModuleId];
$sName = $sModuleId;
$sVersion = $aModuleData['version_code'];
$sVersion = $aModuleData['available_version'];
$sUninstallable = $aModuleData['uninstallable'] ?? 'yes';
$aComments = [];
$aComments[] = $sShortComment;
@@ -702,16 +587,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 +625,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 = ModuleInstallationService::GetInstance()->ReadFromDB($oConfig);
} catch (MySQLException $e) {
// No database or erroneous information
$this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name'));
@@ -975,7 +859,7 @@ class RunTimeEnvironment
{
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules)) {
$aArgs = [MetaModel::GetConfig(), $aModule['version_db'], $aModule['version_code']];
$aArgs = [MetaModel::GetConfig(), $aModule['installed_version'], $aModule['available_version']];
RunTimeEnvironment::CallInstallerHandler($aAvailableModules[$sModuleId], $sHandlerName, $aArgs);
}
}
@@ -997,7 +881,7 @@ class RunTimeEnvironment
return;
}
SetupLog::Info("Calling Module Handler: $sModuleInstallerClass::$sHandlerName", null, $aArgs);
SetupLog::Debug("Calling Module Handler: $sModuleInstallerClass::$sHandlerName");
$aCallSpec = [$sModuleInstallerClass, $sHandlerName];
if (is_callable($aCallSpec)) {
try {
@@ -1039,7 +923,7 @@ class RunTimeEnvironment
$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'] != '') {
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']);

View File

@@ -1555,17 +1555,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($oWizard)
{
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
$oConfig = new Config();
$sSourceDir = $oWizard->GetParameter('source_dir', '');
@@ -1580,7 +1571,25 @@ JS
$aParamValues = $oWizard->GetParamForConfigArray();
$aParamValues['source_dir'] = $sRelativeSourceDir;
$oConfig->UpdateFromParams($aParamValues, null);
$aDirsToScan = [$sSourceDir];
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';

View File

@@ -50,6 +50,7 @@ require_once(APPROOT.'setup/applicationinstaller.class.inc.php');
require_once(APPROOT.'setup/parameters.class.inc.php');
require_once(APPROOT.'core/mutex.class.inc.php');
require_once(APPROOT.'setup/extensionsmap.class.inc.php');
require_once APPROOT.'setup/feature_removal/SetupAudit.php';
/**
* First step of the iTop Installation Wizard: Welcome screen, requirements
@@ -1367,6 +1368,52 @@ 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;
}
}
$sExtensionsAdded = '';
if (count($aExtensionsAdded) > 0) {
$sExtensionsAdded = '<ul>';
foreach ($aExtensionsAdded as $sExtensionCode) {
$sExtensionsAdded .= '<li>'.$sExtensionCode.'</li>';
}
$sExtensionsAdded .= '</ul>';
} else {
$sExtensionsAdded = '<ul><li>No extension added.</li></ul>';
}
$sExtensionsRemoved = '';
if (count($aExtensionsRemoved) > 0) {
$sExtensionsRemoved = '<ul>';
foreach ($aExtensionsRemoved as $sCode => $sExtensionCode) {
$sForcedUninstall = '';
if (isset($aExtensionsNotUninstallable[$sCode])) {
$sForcedUninstall = ' (forced uninstallation)';
}
$sExtensionsRemoved .= '<li>'.$sExtensionCode.$sForcedUninstall.'</li>';
}
$sExtensionsRemoved .= '</ul>';
} else {
$sExtensionsRemoved = '<ul><li>No extension removed.</li></ul>';
}
return [$sExtensionsAdded, $sExtensionsRemoved];
}
public function ProcessParams($bMoveForward = true)
{
// Accumulates the selected modules:
@@ -1396,9 +1443,14 @@ class WizStepModulesChoice extends WizardStep
if (class_exists('CreateITILProfilesInstaller')) {
$this->oWizard->SetParameter('old_addon', true);
}
[$sExtensionsAdded, $sExtensionsRemoved] = $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', $sExtensionsAdded);
$this->oWizard->SetParameter('extensions_removed', $sExtensionsRemoved);
return ['class' => 'WizStepSummary', 'state' => ''];
}
@@ -1561,7 +1613,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 +1657,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 +1715,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] = [];
@@ -1952,10 +2004,47 @@ EOF
return '<i class="setup-extension--icon '.$sDecorationClass.'" data-tooltip-content="'.$sResult.'"></i>';
}
public function ComputeChoiceFlags(array $aChoice, string $sChoiceId, array $aSelectedComponents, bool $bAllDisabled, bool $bDisableUninstallCheck, bool $bUpgradeMode)
{
$oITopExtension = $this->oExtensionsMap->GetFromExtensionCode($aChoice['extension_code']);
$bCanBeUninstalled = isset($aChoice['uninstallable']) ? $aChoice['uninstallable'] === true || $aChoice['uninstallable'] === 'yes' : $oITopExtension->CanBeUninstalled();
$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId);
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $bUpgradeMode && $oITopExtension->bInstalled && !$bCanBeUninstalled && !$bDisableUninstallCheck;
$bMissingFromDisk = isset($aChoice['missing']) && $aChoice['missing'] === true;
$bInstalled = $bMissingFromDisk || $oITopExtension->bInstalled;
$bDisabled = $bMandatory || $bAllDisabled || $bMissingFromDisk;
$bChecked = $bMandatory || $bSelected;
if (isset($aChoice['sub_options'])) {
$aOptions = $aChoice['sub_options']['options'] ?? [];
foreach ($aOptions as $index => $aSubChoice) {
$sSubChoiceId = $sChoiceId.self::$SEP.$index;
$aSubFlags = $this->ComputeChoiceFlags($aSubChoice, $sSubChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $bUpgradeMode);
if ($aSubFlags['checked']) {
$bChecked = true;
if ($aSubFlags['disabled']) {
//If some sub options are enabled and cannot be disabled, this choice should also cannot be disabled since it would disable all its sub options
$bDisabled = true;
}
}
}
}
return [
'uninstallable' => $bCanBeUninstalled,
'missing' => $bMissingFromDisk,
'installed' => $bInstalled,
'disabled' => $bDisabled,
'checked' => $bChecked,
];
}
protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults, $sParentId = '', $bAllDisabled = false)
{
$aOptions = 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 +2052,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) {
if ($aFlags['missing']) {
$sTooltip .= '<span class="setup-extension-tag removed">source removed</span>';
}
if ($bInstalled) {
if ($aFlags['installed']) {
$sTooltip .= '<span class="setup-extension-tag checked installed">installed</span>';
$sTooltip .= '<span class="setup-extension-tag unchecked tobeuninstalled">to be uninstalled</span>';
} else {
$sTooltip .= '<span class="setup-extension-tag checked tobeinstalled">to be installed</span>';
$sTooltip .= '<span class="setup-extension-tag unchecked notinstalled">not installed</span>';
}
if (!$bCanBeUninstalled) {
if (!$aFlags['uninstallable']) {
$sTooltip .= '<span class="setup-extension-tag notuninstallable">cannot be uninstalled</span>';
}
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 +2113,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
}
@@ -2114,6 +2192,18 @@ class WizStepSummary extends WizardStep
$this->bDependencyCheck = true;
try {
SetupUtils::AnalyzeInstallation($this->oWizard, true, $aSelectedModules);
/*$sInstallMode = utils::ReadParam('install_mode');
\SetupLog::Info(__METHOD__, null, ['$sInstallMode' => $sInstallMode]);
//if ($sInstallMode === "upgrade") {
$aExtensions = json_decode($this->oWizard->GetParameter('selected_extensions'), true);
$oSetupAudit = new SetupAudit([]);
$oConfig = SetupUtils::GetConfig($this->oWizard);
$oSetupAudit->SetSelectedExtensions($oConfig, $aExtensions);
//$oSetupAudit->AuditExtensionsCleanupRules(true);
//}
*/
} catch (MissingDependencyException $e) {
$this->bDependencyCheck = false;
$this->sDependencyIssue = $e->getHtmlDesc();
@@ -2178,6 +2268,14 @@ 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>');
$oPage->add($this->oWizard->GetParameter('extensions_added'));
$oPage->add('</div>');
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Extensions to be uninstalled</span>');
$oPage->add($this->oWizard->GetParameter('extensions_removed'));
$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>');

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

@@ -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

@@ -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

@@ -18,6 +18,7 @@ use ArchivedObjectException;
use CMDBObject;
use CMDBSource;
use Combodo\iTop\Service\Events\EventService;
use Config;
use Contact;
use CoreException;
use CoreUnexpectedValue;
@@ -70,6 +71,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 +128,8 @@ abstract class ItopDataTestCase extends ItopTestCase
{
parent::setUp();
\IssueLog::Error($this->getName());
$this->PrepareEnvironment();
if (static::USE_TRANSACTION) {
@@ -190,6 +196,8 @@ abstract class ItopDataTestCase extends ItopTestCase
CMDBObject::SetCurrentChange(null);
$this->RestoreConfiguration();
parent::tearDown();
}
@@ -1517,4 +1525,35 @@ 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);
}
}

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,62 @@ 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);
}
}

View File

@@ -26,26 +26,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,6 +55,15 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment
SetupUtils::copydir(APPROOT.'/data/'.$sSourceEnv.'-modules', $sDestModulesDir, $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);
}
}
parent::CompileFrom($sSourceEnv, $bUseSymLinks);
}
@@ -94,23 +102,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 +197,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

@@ -0,0 +1,164 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use AnalyzeInstallation;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use ModuleInstallationService;
class AnalyzeInstallationTest extends ItopTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/AnalyzeInstallation.php');
$this->RequireOnceItopFile('setup/ModuleInstallationService.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(ModuleInstallationService::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

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

View File

@@ -0,0 +1,355 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use ItopExtensionsMap;
use iTopExtensionsMapFake;
use ModuleDiscovery;
use WizardController;
class WizStepModulesChoiceTest extends ItopTestCase
{
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 [
'selected but not installed extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
],
'bUpgrade' => false,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
],
'aSelected' => ['_0' => '_0'],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => false,
'checked' => true,
],
],
'not selected, not installed extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => false,
'checked' => false,
],
],
'installed extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => true,
'disabled' => false,
'checked' => false,
],
],
'installed non uninstallable extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => false,
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => false,
'missing' => false,
'installed' => true,
'disabled' => true,
'checked' => true,
],
],
'mandatory extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => true,
'uninstallable' => true,
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => true,
'checked' => true,
],
],
'optional sub extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
'itop-ext1-1' => [
'installed' => false,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'sub_options' => [
'options' => [
[
'extension_code' => 'itop-ext1-1',
'mandatory' => false,
'uninstallable' => true,
],
],
],
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => false,
'checked' => false,
],
],
'mandatory sub extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
'itop-ext1-1' => [
'installed' => false,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'sub_options' => [
'options' => [
[
'extension_code' => 'itop-ext1-1',
'mandatory' => true,
'uninstallable' => true,
],
],
],
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => true,
'checked' => true,
],
],
'non uninstallable sub extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
],
'itop-ext1-1' => [
'installed' => true,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'sub_options' => [
'options' => [
[
'extension_code' => 'itop-ext1-1',
'mandatory' => false,
'uninstallable' => false,
],
],
],
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => true,
'disabled' => true,
'checked' => true,
],
],
];
}
/**
* @dataProvider ProviderComputeChoiceFlags
*/
public function testComputeChoiceFlags($aExtensions, $bUpgrade, $bDisableUninstallCheck, $sChoiceId, $aStepInfo, $aSelected, $aExpectedFlags)
{
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensions));
$aFlags = $this->oStep->ComputeChoiceFlags($aStepInfo, $sChoiceId, $aSelected, false, $bDisableUninstallCheck, $bUpgrade);
$this->assertEquals($aExpectedFlags, $aFlags);
}
public function ProviderGetAddedAndRemovedExtensions()
{
return [
'no extensions' => [
'aExtensions' => [],
'aSelected' => [],
'sExpectedAddedList' => '<ul><li>No extension added.</li></ul>',
'sExpectedRemovedList' => '<ul><li>No extension removed.</li></ul>',
],
'no extensions selected' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
],
'aSelected' => [],
'sExpectedAddedList' => '<ul><li>No extension added.</li></ul>',
'sExpectedRemovedList' => '<ul><li>No extension removed.</li></ul>',
],
'no extensions removed' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
],
],
'aSelected' => ['itop-ext1'],
'sExpectedAddedList' => '<ul><li>No extension added.</li></ul>',
'sExpectedRemovedList' => '<ul><li>No extension removed.</li></ul>',
],
'One added extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
],
'aSelected' => ['itop-ext1'],
'sExpectedAddedList' => '<ul><li>itop-ext1</li></ul>',
'sExpectedRemovedList' => '<ul><li>No extension removed.</li></ul>',
],
'One removed extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
],
],
'aSelected' => [],
'sExpectedAddedList' => '<ul><li>No extension added.</li></ul>',
'sExpectedRemovedList' => '<ul><li>itop-ext1</li></ul>',
],
'Forced removed extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
'uninstallable' => false,
],
],
'aSelected' => [],
'sExpectedAddedList' => '<ul><li>No extension added.</li></ul>',
'sExpectedRemovedList' => '<ul><li>itop-ext1 (forced uninstallation)</li></ul>',
],
'added and removed extensions' => [
'aExtensions' => [
'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'],
'sExpectedAddedList' => '<ul><li>itop-ext-added1</li><li>itop-ext-added2</li></ul>',
'sExpectedRemovedList' => '<ul><li>itop-ext-removed1</li><li>itop-ext-removed2</li></ul>',
],
];
}
/**
* @dataProvider ProviderGetAddedAndRemovedExtensions
*/
public function testGetAddedAndRemovedExtensions($aExtensions, $aSelectedExtensions, $sExpectedAddedList, $sExpectedRemovedList)
{
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensions));
[$sAddedList, $sRemovedList] = $this->oStep->GetAddedAndRemovedExtensions($aSelectedExtensions);
$this->assertEquals($sExpectedAddedList, $sAddedList);
$this->assertEquals($sExpectedRemovedList, $sRemovedList);
}
}

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,118 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup\FeatureRemoval;
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use Combodo\iTop\Test\UnitTest\Service\UnitTestRunTimeEnvironment;
use Exception;
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/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());
$expected = [
"Feature1Module1MyClass",
"FinalClassFeature2Module1MyClass",
"FinalClassFeature2Module1MyFinalClassFromLocation",
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetRemovedClasses());
$expected = [
"FinalClassFeature2Module1MyFinalClassFromLocation" => 0,
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetIssues());
}
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());
$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());
$aRemovedClasses = [
"Feature1Module1MyClass",
"FinalClassFeature1Module1MyClass",
"FinalClassFeature1Module1MyFinalClassFromLocation",
"FinalClassFeature2Module1MyClass",
"FinalClassFeature2Module1MyFinalClassFromLocation",
];
//avoid setup dry computation
$this->SetNonPublicProperty($oSetupAudit, 'aRemovedClasses', $aRemovedClasses);
$this->expectException(Exception::class);
$this->expectExceptionMessage('FinalClassFeature1Module1MyFinalClassFromLocation');
$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

@@ -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
'nominal_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.nominal_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,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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,642 @@
[
{
"0": "330",
"id": "330",
"1": "iTop",
"name": "iTop",
"2": "3.3.0-dev-svn",
"version": "3.3.0-dev-svn",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nBuilt on $WCNOW$",
"comment": "Done by the setup program\nBuilt on $WCNOW$",
"5": "0",
"parent_id": "0",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "331",
"id": "331",
"1": "authent-cas",
"name": "authent-cas",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nVisible (during the setup)",
"comment": "Done by the setup program\nMandatory\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "332",
"id": "332",
"1": "authent-external",
"name": "authent-external",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "333",
"id": "333",
"1": "authent-ldap",
"name": "authent-ldap",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "334",
"id": "334",
"1": "authent-local",
"name": "authent-local",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nVisible (during the setup)",
"comment": "Done by the setup program\nMandatory\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "335",
"id": "335",
"1": "combodo-backoffice-darkmoon-theme",
"name": "combodo-backoffice-darkmoon-theme",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "336",
"id": "336",
"1": "combodo-backoffice-fullmoon-high-contrast-theme",
"name": "combodo-backoffice-fullmoon-high-contrast-theme",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "337",
"id": "337",
"1": "combodo-backoffice-fullmoon-protanopia-deuteranopia-theme",
"name": "combodo-backoffice-fullmoon-protanopia-deuteranopia-theme",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "338",
"id": "338",
"1": "combodo-backoffice-fullmoon-tritanopia-theme",
"name": "combodo-backoffice-fullmoon-tritanopia-theme",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "339",
"id": "339",
"1": "itop-backup",
"name": "itop-backup",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "340",
"id": "340",
"1": "itop-config",
"name": "itop-config",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "341",
"id": "341",
"1": "itop-files-information",
"name": "itop-files-information",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "342",
"id": "342",
"1": "itop-portal-base",
"name": "itop-portal-base",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "343",
"id": "343",
"1": "itop-profiles-itil",
"name": "itop-profiles-itil",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "344",
"id": "344",
"1": "itop-sla-computation",
"name": "itop-sla-computation",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "345",
"id": "345",
"1": "itop-structure",
"name": "itop-structure",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "346",
"id": "346",
"1": "itop-welcome-itil",
"name": "itop-welcome-itil",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "347",
"id": "347",
"1": "itop-config-mgmt",
"name": "itop-config-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/2.7.1",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/2.7.1",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "348",
"id": "348",
"1": "itop-attachments",
"name": "itop-attachments",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "349",
"id": "349",
"1": "itop-tickets",
"name": "itop-tickets",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/2.7.1",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/2.7.1",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "350",
"id": "350",
"1": "combodo-db-tools",
"name": "combodo-db-tools",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/3.0.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/3.0.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "351",
"id": "351",
"1": "itop-core-update",
"name": "itop-core-update",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-files-information\/2.7.0\nDepends on module: combodo-db-tools\/2.7.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-files-information\/2.7.0\nDepends on module: combodo-db-tools\/2.7.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "352",
"id": "352",
"1": "itop-hub-connector",
"name": "itop-hub-connector",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "353",
"id": "353",
"1": "itop-oauth-client",
"name": "itop-oauth-client",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-welcome-itil\/3.1.0,",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-welcome-itil\/3.1.0,",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "354",
"id": "354",
"1": "itop-themes-compat",
"name": "itop-themes-compat",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/3.1.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/3.1.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "355",
"id": "355",
"1": "itop-datacenter-mgmt",
"name": "itop-datacenter-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "356",
"id": "356",
"1": "itop-endusers-devices",
"name": "itop-endusers-devices",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "357",
"id": "357",
"1": "itop-storage-mgmt",
"name": "itop-storage-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "358",
"id": "358",
"1": "itop-virtualization-mgmt",
"name": "itop-virtualization-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "359",
"id": "359",
"1": "itop-bridge-cmdb-ticket",
"name": "itop-bridge-cmdb-ticket",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-tickets\/2.7.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-tickets\/2.7.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "360",
"id": "360",
"1": "itop-bridge-virtualization-storage",
"name": "itop-bridge-virtualization-storage",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-storage-mgmt\/2.2.0\nDepends on module: itop-virtualization-mgmt\/2.2.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-storage-mgmt\/2.2.0\nDepends on module: itop-virtualization-mgmt\/2.2.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "361",
"id": "361",
"1": "itop-service-mgmt",
"name": "itop-service-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-tickets\/2.0.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-tickets\/2.0.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "362",
"id": "362",
"1": "itop-bridge-cmdb-services",
"name": "itop-bridge-cmdb-services",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "363",
"id": "363",
"1": "itop-bridge-datacenter-mgmt-services",
"name": "itop-bridge-datacenter-mgmt-services",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-datacenter-mgmt\/3.1.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-datacenter-mgmt\/3.1.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "364",
"id": "364",
"1": "itop-bridge-endusers-devices-services",
"name": "itop-bridge-endusers-devices-services",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-endusers-devices\/3.1.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-endusers-devices\/3.1.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "365",
"id": "365",
"1": "itop-bridge-storage-mgmt-services",
"name": "itop-bridge-storage-mgmt-services",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-storage-mgmt\/3.1.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-storage-mgmt\/3.1.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "366",
"id": "366",
"1": "itop-bridge-virtualization-mgmt-services",
"name": "itop-bridge-virtualization-mgmt-services",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-virtualization-mgmt\/3.1.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-virtualization-mgmt\/3.1.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "367",
"id": "367",
"1": "itop-request-mgmt",
"name": "itop-request-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-tickets\/2.4.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-tickets\/2.4.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "368",
"id": "368",
"1": "itop-portal",
"name": "itop-portal",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-portal-base\/2.7.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-portal-base\/2.7.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "369",
"id": "369",
"1": "itop-change-mgmt",
"name": "itop-change-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0\nDepends on module: itop-tickets\/2.0.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0\nDepends on module: itop-tickets\/2.0.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
}
]

View File

@@ -0,0 +1,50 @@
[
{
"0": "330",
"id": "330",
"1": "iTop",
"name": "iTop",
"2": "3.3.0-dev-svn",
"version": "3.3.0-dev-svn",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nBuilt on $WCNOW$",
"comment": "Done by the setup program\nBuilt on $WCNOW$",
"5": "0",
"parent_id": "0",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "331",
"id": "331",
"1": "mandatory_module",
"name": "mandatory_module",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nVisible (during the setup)",
"comment": "Done by the setup program\nMandatory\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "332",
"id": "332",
"1": "optional_module",
"name": "optional_module",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "no"
}
]

View File

@@ -12,10 +12,8 @@ class CliResetSessionTest extends ItopDataTestCase
public const USE_TRANSACTION = false;
private $sCookieFile = "";
private $sUrl;
private $sLogin;
private $sPassword = "Iuytrez9876543ç_è-(";
protected $sConfigTmpBackupFile;
/**
* @throws Exception
@@ -24,16 +22,13 @@ class CliResetSessionTest extends ItopDataTestCase
{
parent::setUp();
$this->sConfigTmpBackupFile = tempnam(sys_get_temp_dir(), "config_");
MetaModel::GetConfig()->WriteToFile($this->sConfigTmpBackupFile);
$this->BackupConfiguration();
$this->sLogin = "rest-user-".date('dmYHis');
$this->CreateTestOrganization();
$this->sCookieFile = tempnam(sys_get_temp_dir(), 'jsondata_');
$this->sUrl = \MetaModel::GetConfig()->Get('app_root_url');
$oRestProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", ['name' => 'REST Services User'], true);
$oAdminProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", ['name' => 'Administrator'], true);
@@ -47,16 +42,6 @@ class CliResetSessionTest extends ItopDataTestCase
{
parent::tearDown();
if (! is_null($this->sConfigTmpBackupFile) && is_file($this->sConfigTmpBackupFile)) {
//put config back
$sConfigPath = MetaModel::GetConfig()->GetLoadedFile();
@chmod($sConfigPath, 0770);
$oConfig = new Config($this->sConfigTmpBackupFile);
$oConfig->WriteToFile($sConfigPath);
@chmod($sConfigPath, 0444);
unlink($this->sConfigTmpBackupFile);
}
if (!empty($this->sCookieFile)) {
unlink($this->sCookieFile);
}
@@ -150,26 +135,18 @@ class CliResetSessionTest extends ItopDataTestCase
*/
private function SendHTTPRequestWithCookies($sUri, $aPostFields, $sForcedLoginMode = null): string
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_COOKIEJAR, $this->sCookieFile);
curl_setopt($ch, CURLOPT_COOKIEFILE, $this->sCookieFile);
$sUrl = "$this->sUrl/$sUri";
if (!is_null($sForcedLoginMode)) {
$sUrl .= "?login_mode=$sForcedLoginMode";
$sUri .= "?login_mode=$sForcedLoginMode";
}
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_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
// 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);
$sResponse = curl_exec($ch);
$aCurlOptions = [
CURLOPT_COOKIEJAR => $this->sCookieFile,
CURLOPT_COOKIEFILE => $this->sCookieFile,
CURLOPT_HEADER => 1,
];
$sResponse = $this->CallItopUri($sUri, $aPostFields, $aCurlOptions);
var_dump($this->aLastCurlGetInfo);
/** $sResponse example
* "HTTP/1.1 200 OK
Date: Wed, 07 Jun 2023 05:00:40 GMT
@@ -177,16 +154,15 @@ class CliResetSessionTest extends ItopDataTestCase
Set-Cookie: itop-2e83d2e9b00e354fdc528621cac532ac=q7ldcjq0rvbn33ccr9q8u8e953; path=/
*/
//var_dump($sResponse);
$iHeaderSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$iHeaderSize = $this->aLastCurlGetInfo['header_size'] ?? 0;
$sBody = substr($sResponse, $iHeaderSize);
//$iHttpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE));
if (preg_match('/HTTP.* (\d*) /', $sResponse, $aMatches)) {
$sHttpCode = $aMatches[1];
} else {
$sHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$sHttpCode = $this->aLastCurlGetInfo['http_code'] ?? -1;
}
curl_close($ch);
$this->assertEquals(200, $sHttpCode, "The test logic assumes that the HTTP request is correctly handled");
return $sBody;

View File

@@ -17,7 +17,6 @@ class RestTest extends ItopDataTestCase
public const USE_TRANSACTION = false;
public const CREATE_TEST_ORG = false;
private static $sUrl;
private static $sLogin;
private static $sPassword = "Iuytrez9876543ç_è-(";
@@ -44,7 +43,6 @@ class RestTest extends ItopDataTestCase
{
parent::setUp();
static::$sUrl = MetaModel::GetConfig()->Get('app_root_url');
static::$sLogin = "rest-user-".date('dmYHis');
$this->CreateTestOrganization();
@@ -96,7 +94,6 @@ class RestTest extends ItopDataTestCase
public function testPostJSONDataAsCurlFile()
{
$sCallbackName = 'fooCallback';
$sJsonData = '{"operation": "list_operations"}';
// Test regular JSON result
@@ -297,16 +294,7 @@ JSON;
$aPostFields['callback'] = $sCallbackName;
}
curl_setopt($ch, CURLOPT_URL, static::$sUrl."/webservices/rest.php");
curl_setopt($ch, CURLOPT_POST, 1);// set post data to true
curl_setopt($ch, CURLOPT_POSTFIELDS, $aPostFields);
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);
$sJson = curl_exec($ch);
curl_close($ch);
$sJson = $this->CallItopUri('webservices/rest.php', $aPostFields);
if (!is_null($sTmpFile)) {
unlink($sTmpFile);