Compare commits

...

63 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
Timmy38
73f868ac83 N°8763 Halt setup if non-uninstallable extension is missing (#781)
* N°8763 Halt setup if an installed & non-uninstallable extension is missing from disk
2025-12-04 11:01:31 +01:00
odain
5a2157ba21 N°8724 - refactoring for maintenability 2025-11-27 15:47:25 +01:00
odain
03e25a226e Merge branch 'faf/moduledependency-enhancement' into develop 2025-11-26 19:25:37 +01:00
odain
24048d2b9c N°8724 - Enhance setup feedback in case of module dependency issue (#700)
code style

last test cleanup

review + enhance UI output and display only failed module dependencies

real life test cleanup

review: add more tests + refacto

code review: enhance algo and APIs

review: renaming

enhance test coverage

refactoring

renaming + reorder functions/tests

compute GetDependencyResolutionFeedback in Module class

review2 : renaming things

fix rebase + code formatting

fix code formatting

review changes

refactoring: code cleanup/standardization/remove all prototype stuffs

refactoring: code cleanup/standardization/remove all prototype stuffs

add deps validation to extension ci job

fix ci

fix ci: test broken when dir to scan did not exist like production-modules

fix tests

module dependency validation moved in a core folder + cleanup dedicated unit/integration tests

forget dependency computation optimization seen as too risky + keep only user friendly sort in case of setup error

rebase on develop + split new sort computation apart from modulediscovery

revert to previous legacy order + gather new module computation classes in a dedicated folder

make validation work (dirty way) + cleanup

make setup deterministic: complete dependency order with alphabetical one when 2 module elements are at same position

final deps validation bases on DM and PHP classes

init in beforeclass + read defined classes/interfaces by module

module discovery classes renaming to avoid collision with customer DM definitions

read module file data apart from ModuleDiscovery

cleanup

cleanup

fix inconsistent module dependencies

fix integration check

save tmp work before trying to fetch other wml deps

fix module dependencies

fix DM filename typo

rename ModuleXXX classes by iTopCoreModuleXXX to reduce collisions with extensions

add phpdoc + add more tests

module dependency optimization - refacto + dependency new sort order

module dependency optimization - stop computation when no new dependency is resolved

enhance module dependency computation for optimization and admin feedback
2025-11-26 19:23:26 +01:00
odain
d8121b563a synchro code: fix Deprecated: strlen(): Passing null to parameter 2025-11-21 10:38:26 +01:00
Anne-Cath
f266f5ff36 N°8911 - Attachment visualisation broken 2025-11-20 08:06:00 +01:00
odain
4a6b129eb8 Merge branch 'support/3.2' into develop 2025-11-18 15:36:47 +01:00
odain
4187f552a9 Merge branch 'support/3.2.1' into support/3.2 2025-11-18 15:36:32 +01:00
odain
7f7ce0837e Merge branch 'designer-3.2.1' into support/3.2.1 2025-11-18 15:35:40 +01:00
odain
7df09541ac N°8306 - ci fixes 2025-11-16 08:11:34 +01:00
98 changed files with 16797 additions and 1844 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",

File diff suppressed because one or more lines are too long

View File

@@ -311,29 +311,35 @@ fieldset {
}
.module-selection-body {
overflow: auto;
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, .06) !important;
background-color: #F7FAFC;
padding: 10px;
.wiz-choice:checked ~ .description {
#itop-ticket-mgmt-simple-ticket-enhanced-portal:not(:checked),
#itop-ticket-mgmt-itil-enhanced-portal:not(:checked) {
~ .description::after {
content: "Legacy portal is no longer part of iTop, by leaving this option unchecked your portal users won't be able to access iTop anymore.";
display: block;
margin-top: 0.5em;
font-weight: bold;
color: $legacy-portal-removal-text-color;
}
}
}
overflow: auto;
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, .06) !important;
background-color: #F7FAFC;
padding: 10px;
.wiz-choice{
&:checked ~ .description {
#itop-ticket-mgmt-simple-ticket-enhanced-portal:not(:checked),
#itop-ticket-mgmt-itil-enhanced-portal:not(:checked) {
~ .description::after {
content: "Legacy portal is no longer part of iTop, by leaving this option unchecked your portal users won't be able to access iTop anymore.";
display: block;
margin-top: 0.5em;
font-weight: bold;
color: $legacy-portal-removal-text-color;
}
}
}
&:not(:checked) ~ label .setup-extension-tag.checked{
display:none;
}
&:checked ~ label .setup-extension-tag.unchecked{
display:none;
}
}
}
body {
font-size: 1.17rem;
font-family: "Raleway";
@@ -595,6 +601,35 @@ body {
color: $ibo-color-blue-700;
font-size: $ibo-font-size-200;
}
.setup-extension--missing .setup-extension--icon{
color:#a00000;
}
.setup-extension-tag {
background-color: grey;
border-radius: 8px;
padding-left: 3px;
padding-right: 3px;
margin-right: 3px;
&.installed{
background-color:#9eff9e
}
&.notinstalled{
background-color:#ed9eff
}
&.tobeinstalled{
background-color:#9ef0ff
}
&.tobeuninstalled{
background-color:#ff9e9e
}
&.notuninstallable{
background-color:#ffc98c
}
&.removed{
background-color: #969594
}
}
.setup--wizard-choice--label + .setup--wizard-choice--more-info {
margin-left: 0.5rem;
}

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

@@ -67,15 +67,17 @@ class EventListener implements iEventServiceSetup
/** @var \DBObject $oAttachment */
$oAttachment = $oEventData->Get('object');
$oHostObj = MetaModel::GetObject($oAttachment->Get('item_class'), $oAttachment->Get('item_id'), false /* false to avoid exception during trigger */, true);
/** @var \ormDocument $oDocument */
$oDocument = $oEventData->Get('document');
if ($oHostObj != null) {
/** @var \ormDocument $oDocument */
$oDocument = $oEventData->Get('document');
$this->OnAttachmentActivateTriggers(
$oHostObj,
$oAttachment,
$oDocument,
TriggerOnAttachmentDownload::class
);
$this->OnAttachmentActivateTriggers(
$oHostObj,
$oAttachment,
$oDocument,
TriggerOnAttachmentDownload::class
);
}
}
/**

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

@@ -1,6 +1,7 @@
<?php
class HubRunTimeEnvironment extends RunTimeEnvironment
{
{
/**
* Constructor
* @param string $sEnvironment
@@ -9,21 +10,18 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
public function __construct($sEnvironment = 'production', $bAutoCommit = true)
{
parent::__construct($sEnvironment, $bAutoCommit);
if ($sEnvironment != $this->sTargetEnv)
{
if (is_dir(APPROOT.'/env-'.$this->sTargetEnv))
{
SetupUtils::rrmdir(APPROOT.'/env-'.$this->sTargetEnv);
}
if (is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules'))
{
SetupUtils::rrmdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules');
}
if ($sEnvironment != $this->sTargetEnv) {
if (is_dir(APPROOT.'/env-'.$this->sTargetEnv)) {
SetupUtils::rrmdir(APPROOT.'/env-'.$this->sTargetEnv);
}
if (is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) {
SetupUtils::rrmdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules');
}
SetupUtils::copydir(APPROOT.'/data/'.$sEnvironment.'-modules', APPROOT.'/data/'.$this->sTargetEnv.'-modules');
}
}
/**
* Update the includes for the target environment
* @param Config $oConfig
@@ -32,7 +30,7 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
{
$oConfig->UpdateIncludes('env-'.$this->sTargetEnv); // TargetEnv != FinalEnv
}
/**
* Move an extension (path to folder of this extension) to the target environment
* @param string $sExtensionDirectory The folder of the extension
@@ -40,21 +38,23 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
*/
public function MoveExtension($sExtensionDirectory)
{
if (!is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules'))
{
if (!mkdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) throw new Exception("ERROR: failed to create directory:'".(APPROOT.'/data/'.$this->sTargetEnv.'-modules')."'");
if (!is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) {
if (!mkdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) {
throw new Exception("ERROR: failed to create directory:'".(APPROOT.'/data/'.$this->sTargetEnv.'-modules')."'");
}
}
$sDestinationPath = APPROOT.'/data/'.$this->sTargetEnv.'-modules/';
// Make sure that the destination directory of the extension does not already exist
if (is_dir($sDestinationPath.basename($sExtensionDirectory)))
{
// Cleanup before moving...
SetupUtils::rrmdir($sDestinationPath.basename($sExtensionDirectory));
if (is_dir($sDestinationPath.basename($sExtensionDirectory))) {
// Cleanup before moving...
SetupUtils::rrmdir($sDestinationPath.basename($sExtensionDirectory));
}
if (!rename($sExtensionDirectory, $sDestinationPath.basename($sExtensionDirectory))) {
throw new Exception("ERROR: failed move directory:'$sExtensionDirectory' to '".$sDestinationPath.basename($sExtensionDirectory)."'");
}
if (!rename($sExtensionDirectory, $sDestinationPath.basename($sExtensionDirectory))) throw new Exception("ERROR: failed move directory:'$sExtensionDirectory' to '".$sDestinationPath.basename($sExtensionDirectory)."'");
}
/**
* Move the selected extensions located in the given directory in data/<target-env>-modules
* @param string $sDownloadedExtensionsDir The directory to scan
@@ -63,10 +63,8 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
*/
public function MoveSelectedExtensions($sDownloadedExtensionsDir, $aSelectedExtensionDirs)
{
foreach(glob($sDownloadedExtensionsDir.'*', GLOB_ONLYDIR) as $sExtensionDir)
{
if (in_array(basename($sExtensionDir), $aSelectedExtensionDirs))
{
foreach (glob($sDownloadedExtensionsDir.'*', GLOB_ONLYDIR) as $sExtensionDir) {
if (in_array(basename($sExtensionDir), $aSelectedExtensionDirs)) {
$this->MoveExtension($sExtensionDir);
}
}

View File

@@ -20,7 +20,7 @@ function DisplayStatus(WebPage $oPage)
if (is_dir($sPath)) {
$aExtraDirs[] = $sPath; // Also read the extra downloaded-modules directory
}
$oExtensionsMap = new iTopExtensionsMap('production', true, $aExtraDirs);
$oExtensionsMap = new iTopExtensionsMap('production', $aExtraDirs);
$oExtensionsMap->LoadChoicesFromDatabase(MetaModel::GetConfig());
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {
@@ -154,7 +154,7 @@ function DoInstall(WebPage $oPage)
if (is_dir($sPath)) {
$aExtraDirs[] = $sPath; // Also read the extra downloaded-modules directory
}
$oExtensionsMap = new iTopExtensionsMap('production', true, $aExtraDirs);
$oExtensionsMap = new iTopExtensionsMap('production', $aExtraDirs);
$oExtensionsMap->LoadChoicesFromDatabase(MetaModel::GetConfig());
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {

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

@@ -17,6 +17,7 @@ SetupWebPage::AddModule(
//
'dependencies' => [
'itop-welcome-itil/3.1.0,',
'itop-profiles-itil/3.1.0', //SuperUser id 117
],
'mandatory' => false,
'visible' => true,

View File

@@ -28,6 +28,7 @@ SetupWebPage::AddModule(
'category' => 'Portal',
// Setup
'dependencies' => [
'itop-attachments/3.2.1', //CMDBChangeOpAttachmentRemoved
],
'mandatory' => true,
'visible' => false,

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

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

View File

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

View File

@@ -14,7 +14,10 @@ if (PHP_VERSION_ID < 50600) {
echo $err;
}
}
throw new RuntimeException($err);
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';

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

@@ -61,7 +61,7 @@ return array(
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'Pelago\\Emogrifier\\' => array($vendorDir . '/pelago/emogrifier/src'),
'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-google/src', $vendorDir . '/league/oauth2-client/src'),
'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src', $vendorDir . '/league/oauth2-google/src'),
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),

View File

@@ -340,8 +340,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
),
'League\\OAuth2\\Client\\' =>
array (
0 => __DIR__ . '/..' . '/league/oauth2-google/src',
1 => __DIR__ . '/..' . '/league/oauth2-client/src',
0 => __DIR__ . '/..' . '/league/oauth2-client/src',
1 => __DIR__ . '/..' . '/league/oauth2-google/src',
),
'GuzzleHttp\\Psr7\\' =>
array (

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

@@ -36,7 +36,8 @@ if ($issues) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

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

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,155 @@
<?php
namespace Combodo\iTop\Setup\ModuleDependency;
require_once(APPROOT.'/setup/runtimeenv.class.inc.php');
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use ModuleFileReaderException;
use RunTimeEnvironment;
/**
* Class that handles a module dependency
* Dependency expression example : (moduleA/123 || moduleB>456)
*/
class DependencyExpression
{
private static PhpExpressionEvaluator $oPhpExpressionEvaluator;
private string $sDependencyExpression;
private bool $bValid = true;
private bool $bResolved = false;
/**
* @var array<string, bool> $aRemainingModuleNamesToResolve
*/
private array $aRemainingModuleNamesToResolve;
/**
* @var array<string, array> $aParamsPerModuleId
*/
private array $aParamsPerModuleId;
public function __construct(string $sDependencyExpression)
{
$this->sDependencyExpression = $sDependencyExpression;
$this->aParamsPerModuleId = [];
$this->aRemainingModuleNamesToResolve = [];
if (preg_match_all('/([^\(\)&| ]+)/', $sDependencyExpression, $aMatches)) {
foreach ($aMatches as $aMatch) {
foreach ($aMatch as $sModuleId) {
if (!array_key_exists($sModuleId, $this->aParamsPerModuleId)) {
// $sModuleId in the dependency string is made of a <name>/<optional_operator><version>
// where the operator is < <= = > >= (by default >=)
$aModuleMatches = [];
if (preg_match('|^([^/]+)/(<?>?=?)([^><=]+)$|', $sModuleId, $aModuleMatches)) {
$sModuleName = $aModuleMatches[1];
$this->aRemainingModuleNamesToResolve[$sModuleName] = true;
$sOperator = $aModuleMatches[2];
if ($sOperator == '') {
$sOperator = '>=';
}
$sExpectedVersion = $aModuleMatches[3];
$this->aParamsPerModuleId[$sModuleId] = [$sModuleName, $sOperator, $sExpectedVersion];
}
}
}
}
} else {
$this->bValid = false;
}
}
private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
{
if (!isset(static::$oPhpExpressionEvaluator)) {
static::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
}
return static::$oPhpExpressionEvaluator;
}
/**
* Return module names potentially required by current dependency
*
* @return array
*/
public function GetRemainingModuleNamesToResolve(): array
{
return array_keys($this->aRemainingModuleNamesToResolve);
}
public function IsResolved(): bool
{
return $this->bResolved;
}
/**
* Check if dependency is resolved with current list of module versions
*
* @param array $aResolvedModuleVersions : versions by module names dict
* @param array $aAllModuleNames : modules names dict
*
* @return void
*/
public function UpdateModuleResolutionState(array $aResolvedModuleVersions, array $aAllModuleNames): void
{
if (!$this->bValid) {
return;
}
$aReplacements = [];
$bDelayEvaluation = false;
foreach ($this->aParamsPerModuleId as $sModuleId => list($sModuleName, $sOperator, $sExpectedVersion)) {
if (array_key_exists($sModuleName, $aResolvedModuleVersions)) {
// module is resolved, check the version
$sCurrentVersion = $aResolvedModuleVersions[$sModuleName];
if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) {
if (array_key_exists($sModuleName, $this->aRemainingModuleNamesToResolve)) {
unset($this->aRemainingModuleNamesToResolve[$sModuleName]);
}
$aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing
// a function call that results in a runtime fatal error
} else {
$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
// a function call that results in a runtime fatal error
}
} else {
// module is not resolved yet
if (array_key_exists($sModuleName, $aAllModuleNames)) {
//Weird piece of code that covers below usecase:
//module B dependency: 'moduleA || true'
// if moduleA not present on disk, whole expression can be evaluated and may be resolved
// if moduleA present on disk, we need to sort moduleB after moduleA. expression cannot be resolved yet
$bDelayEvaluation = true;
} else {
$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
}
}
}
if ($bDelayEvaluation) {
return;
}
$bResult = false;
$sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $this->sDependencyExpression);
try {
$bResult = self::GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($sBooleanExpr);
} catch (ModuleFileReaderException $e) {
//logged already
echo "Failed to parse the boolean Expression = '$sBooleanExpr'<br/>";
}
$this->bResolved = $bResult;
}
public function IsValid(): bool
{
return $this->bValid;
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Combodo\iTop\Setup\ModuleDependency;
require_once(__DIR__.'/dependencyexpression.class.inc.php');
use ModuleDiscovery;
/**
* Class that handles a modules and all its dependencies
*/
class Module
{
private string $sModuleId;
private string $sModuleName;
private string $sVersion;
/**
* @var array<string> $aInitialDependencyExpressions
*/
private array $aInitialDependencyExpressions;
/**
* @var array<string, DependencyExpression> $aRemainingDependenciesToResolve
*/
public array $aRemainingDependenciesToResolve;
public function __construct(string $sModuleId)
{
$this->sModuleId = $sModuleId;
list($this->sModuleName, $this->sVersion) = ModuleDiscovery::GetModuleName($sModuleId);
}
public function IsDependencyExpressionResolved(string $sDependencyExpression): bool
{
return ! array_key_exists($sDependencyExpression, $this->aRemainingDependenciesToResolve);
}
public function GetDependencyResolutionFeedback(): array
{
$aDepsWithIcons = [];
foreach ($this->aInitialDependencyExpressions as $sDependencyExpression) {
if (! $this->IsDependencyExpressionResolved($sDependencyExpression)) {
$aDepsWithIcons[] = '❌ '.$sDependencyExpression;
}
}
return $aDepsWithIcons;
}
/**
* @return string
*/
public function GetModuleName()
{
return $this->sModuleName;
}
/**
* @return string
*/
public function GetVersion()
{
return $this->sVersion;
}
/**
* @return string
*/
public function GetModuleId()
{
return $this->sModuleId;
}
/**
* @param array $aAllDependencyExpressions: list of dependencies (string)
*
* @return void
*/
public function SetDependencies(array $aAllDependencyExpressions): void
{
$this->aInitialDependencyExpressions = $aAllDependencyExpressions;
$this->aRemainingDependenciesToResolve = [];
foreach ($aAllDependencyExpressions as $sDependencyExpression) {
$this->aRemainingDependenciesToResolve[$sDependencyExpression] = new DependencyExpression($sDependencyExpression);
}
}
public function IsResolved(): bool
{
return (0 === count($this->aRemainingDependenciesToResolve));
}
/**
* Check if module dependencies are resolved with current list of module versions
* @param array<string, string> $aResolvedModuleVersions : versions by module names dict
* @param array<string> $aAllModuleNames : resolved modules names
*
* @return void
*/
public function UpdateModuleResolutionState(array $aResolvedModuleVersions, array $aAllModuleNames): void
{
$aNextDependencies = [];
foreach ($this->aRemainingDependenciesToResolve as $sDependencyExpression => $oModuleDependency) {
/** @var DependencyExpression $oModuleDependency*/
$oModuleDependency->UpdateModuleResolutionState($aResolvedModuleVersions, $aAllModuleNames);
if (!$oModuleDependency->IsResolved()) {
$aNextDependencies[$sDependencyExpression] = $oModuleDependency;
}
}
$this->aRemainingDependenciesToResolve = $aNextDependencies;
}
/**
* @return array: list of unique module names
*/
public function GetUnresolvedDependencyModuleNames(): array
{
$aRes = [];
foreach ($this->aRemainingDependenciesToResolve as $sDependencyExpression => $oModuleDependency) {
/** @var DependencyExpression $oModuleDependency */
$aRes = array_merge($aRes, $oModuleDependency->GetRemainingModuleNamesToResolve());
}
return array_unique($aRes);
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace Combodo\iTop\Setup\ModuleDependency;
require_once(__DIR__.'/module.class.inc.php');
use MissingDependencyException;
/**
* Class that sorts module dependencies
*/
class ModuleDependencySort
{
private static ModuleDependencySort $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): ModuleDependencySort
{
if (!isset(static::$oInstance)) {
static::$oInstance = new static();
}
return static::$oInstance;
}
final public static function SetInstance(?ModuleDependencySort $oInstance): void
{
static::$oInstance = $oInstance;
}
/**
* Sort a list of modules, based on their (inter) dependencies
*
* @param array $aModules The list of modules to process: 'id' => $aModuleInfo
* @param bool $bAbortOnMissingDependency ...
*
* @return array
* @throws \MissingDependencyException
*/
public function GetModulesOrderedForInstallation($aModules, $bAbortOnMissingDependency = false)
{
// Filter modules to compute
$aUnresolvedDependencyModules = [];
$aAllModuleNames = [];
foreach ($aModules as $sModuleId => $aModule) {
$oModule = new Module($sModuleId);
$sModuleName = $oModule->GetModuleName();
$oModule->SetDependencies($aModule['dependencies']);
$aUnresolvedDependencyModules[$sModuleId] = $oModule;
$aAllModuleNames[$sModuleName] = true;
}
// Make sure order is deterministic (alphabtical order)
ksort($aUnresolvedDependencyModules);
//Attempt to resolve module dependencies
$aOrderedModules = [];
$aResolvedModuleVersions = [];
$iPreviousUnresolvedCount = -1;
//loop until no dependency is resolved
while ($iPreviousUnresolvedCount !== count($aUnresolvedDependencyModules)) {
$iPreviousUnresolvedCount = count($aUnresolvedDependencyModules);
if ($iPreviousUnresolvedCount === 0) {
break;
}
foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) {
/** @var Module $oModule */
$oModule->UpdateModuleResolutionState($aResolvedModuleVersions, $aAllModuleNames);
if ($oModule->IsResolved()) {
$aOrderedModules[] = $sModuleId;
$aResolvedModuleVersions[$oModule->GetModuleName()] = $oModule->GetVersion();
unset($aUnresolvedDependencyModules[$sModuleId]);
}
}
}
// Report unresolved dependencies
if ($bAbortOnMissingDependency && count($aUnresolvedDependencyModules) > 0) {
$this->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$aUnresolvedModulesInfo = [];
$aModuleDeps = [];
foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) {
$aModule = $aModules[$sModuleId];
$aDepsWithIcons = $oModule->GetDependencyResolutionFeedback();
$aModuleDeps[] = "{$aModule['label']} (id: $sModuleId) depends on: ".implode(' + ', $aDepsWithIcons);
$aUnresolvedModulesInfo[$sModuleId] = ['module' => $aModule, 'dependencies' => $aDepsWithIcons];
}
$sMessage = "The following modules have unmet dependencies:\n".implode(",\n", $aModuleDeps);
$oException = new MissingDependencyException($sMessage);
$oException->aModulesInfo = $aUnresolvedModulesInfo;
throw $oException;
}
// Return the ordered list, so that the dependencies are met...
$aResult = [];
foreach ($aOrderedModules as $sId) {
$aResult[$sId] = $aModules[$sId];
}
return $aResult;
}
/**
* This method is key as it sorts modules by their dependencies (topological sort).
* Modules with less dependencies are first.
* When module A depends from module B with same amount of dependencies, moduleB is first.
* This order can deal with
* - cyclic dependencies
* - further versions of same module (name)
*
* @param array $aUnresolvedDependencyModules : dict of Module objects by moduleId key
*
* @return void
*/
protected function SortModulesByCountOfDepencenciesDescending(array &$aUnresolvedDependencyModules): void
{
$aCountDepsByModuleId = [];
$aDependsOnModuleName = [];
foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) {
/** @var Module $oModule */
$aDependsOnModuleName[$oModule->GetModuleName()] = [];
}
foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) {
$iInDegreeCounter = 0;
/** @var Module $oModule */
$aUnresolvedDependencyModuleNames = $oModule->GetUnresolvedDependencyModuleNames();
foreach ($aUnresolvedDependencyModuleNames as $sModuleName) {
if (array_key_exists($sModuleName, $aDependsOnModuleName)) {
$aDependsOnModuleName[$sModuleName][] = $sModuleId;
$iInDegreeCounter++;
}
}
//include all modules
$iInDegreeCounterIncludingOutsideModules = count($oModule->GetUnresolvedDependencyModuleNames());
$aCountDepsByModuleId[$sModuleId] = [$iInDegreeCounter, $iInDegreeCounterIncludingOutsideModules, $sModuleId];
}
$aRes = [];
while (count($aUnresolvedDependencyModules) > 0) {
asort($aCountDepsByModuleId);
uasort($aCountDepsByModuleId, function (array $aDeps1, array $aDeps2) {
//compare $iInDegreeCounter
$res = $aDeps1[0] - $aDeps2[0];
if ($res != 0) {
return $res;
}
//compare $iInDegreeCounterIncludingOutsideModules
$res = $aDeps1[1] - $aDeps2[1];
if ($res != 0) {
return $res;
}
//alphabetical order at least
return strcmp($aDeps1[2], $aDeps2[2]);
});
$bOneLoopAtLeast = false;
foreach ($aCountDepsByModuleId as $sModuleId => $iInDegreeCounter) {
$oModule = $aUnresolvedDependencyModules[$sModuleId];
if ($bOneLoopAtLeast && $iInDegreeCounter > 0) {
break;
}
unset($aUnresolvedDependencyModules[$sModuleId]);
unset($aCountDepsByModuleId[$sModuleId]);
$aRes[$sModuleId] = $oModule;
//when 2 versions of the same module (name) below array has been removed already
if (array_key_exists($oModule->GetModuleName(), $aDependsOnModuleName)) {
foreach ($aDependsOnModuleName[$oModule->GetModuleName()] as $sModuleId2) {
if (!array_key_exists($sModuleId2, $aCountDepsByModuleId)) {
continue;
}
$aDepCount = $aCountDepsByModuleId[$sModuleId2];
$iInDegreeCounter = $aDepCount[0] - 1;
$iInDegreeCounterIncludingOutsideModules = $aDepCount[1];
$aCountDepsByModuleId[$sModuleId2] = [$iInDegreeCounter, $iInDegreeCounterIncludingOutsideModules, $sModuleId2];
}
unset($aDependsOnModuleName[$oModule->GetModuleName()]);
}
$bOneLoopAtLeast = true;
}
}
$aUnresolvedDependencyModules = $aRes;
}
}

150
setup/modulediscovery.class.inc.php Normal file → Executable file
View File

@@ -21,10 +21,14 @@
*/
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use Combodo\iTop\Setup\ModuleDependency\Module;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
require_once(APPROOT.'setup/modulediscovery/ModuleFileReader.php');
require_once(__DIR__.'/moduledependency/moduledependencysort.class.inc.php');
use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort;
class MissingDependencyException extends CoreException
{
@@ -116,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'");
@@ -211,76 +211,23 @@ class ModuleDiscovery
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
* @return array
* @throws \MissingDependencyException
*/
*/
public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
// Order the modules to take into account their inter-dependencies
$aDependencies = [];
$aSelectedModules = [];
foreach ($aModules as $sId => $aModule) {
list($sModuleName, ) = self::GetModuleName($sId);
if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad)) {
$aDependencies[$sId] = $aModule['dependencies'];
$aSelectedModules[$sModuleName] = true;
}
}
ksort($aDependencies);
$aOrderedModules = [];
$iLoopCount = 0;
while (($iLoopCount < count($aModules)) && (count($aDependencies) > 0)) {
foreach ($aDependencies as $sId => $aRemainingDeps) {
$bDependenciesSolved = true;
foreach ($aRemainingDeps as $sDepId) {
if (!self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules)) {
$bDependenciesSolved = false;
}
}
if ($bDependenciesSolved) {
$aOrderedModules[] = $sId;
unset($aDependencies[$sId]);
if (is_null($aModulesToLoad)) {
$aFilteredModules = $aModules;
} else {
$aFilteredModules = [];
foreach ($aModules as $sModuleId => $aModule) {
$oModule = new Module($sModuleId);
$sModuleName = $oModule->GetModuleName();
if (in_array($sModuleName, $aModulesToLoad)) {
$aFilteredModules[$sModuleId] = $aModule;
}
}
$iLoopCount++;
}
if ($bAbortOnMissingDependency && count($aDependencies) > 0) {
$aModulesInfo = [];
$aModuleDeps = [];
foreach ($aDependencies as $sId => $aDeps) {
$aModule = $aModules[$sId];
$aDepsWithIcons = [];
foreach ($aDeps as $sIndex => $sDepId) {
if (self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules)) {
$aDepsWithIcons[$sIndex] = '✅ '.$sDepId;
} else {
$aDepsWithIcons[$sIndex] = '❌ '.$sDepId;
}
}
$aModuleDeps[] = "{$aModule['label']} (id: $sId) depends on: ".implode(' + ', $aDepsWithIcons);
$aModulesInfo[$sId] = ['module' => $aModule, 'dependencies' => $aDepsWithIcons];
}
$sMessage = "The following modules have unmet dependencies:\n".implode(",\n", $aModuleDeps);
$oException = new MissingDependencyException($sMessage);
$oException->aModulesInfo = $aModulesInfo;
throw $oException;
}
// Return the ordered list, so that the dependencies are met...
$aResult = [];
foreach ($aOrderedModules as $sId) {
$aResult[$sId] = $aModules[$sId];
}
return $aResult;
}
/**
* Remove the duplicate modules (i.e. modules with the same name but with a different version) from the supplied list of modules
* @param array $aModules
* @return array The ordered modules as a duplicate-free list of modules
*/
public static function RemoveDuplicateModules($aModules)
{
// No longer needed, kept only for compatibility
// The de-duplication is now done directly by the AddModule method
return $aModules;
return ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aFilteredModules, $bAbortOnMissingDependency);
}
private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
@@ -292,73 +239,6 @@ class ModuleDiscovery
return static::$oPhpExpressionEvaluator;
}
protected static function DependencyIsResolved($sDepString, $aOrderedModules, $aSelectedModules)
{
$bResult = false;
$aModuleVersions = [];
// Separate the module names from their version for an easier comparison later
foreach ($aOrderedModules as $sModuleId) {
list($sModuleName, $sVersion) = self::GetModuleName($sModuleId);
$aModuleVersions[$sModuleName] = $sVersion;
}
if (preg_match_all('/([^\(\)&| ]+)/', $sDepString, $aMatches)) {
$aReplacements = [];
$aPotentialPrerequisites = [];
foreach ($aMatches as $aMatch) {
foreach ($aMatch as $sModuleId) {
// $sModuleId in the dependency string is made of a <name>/<optional_operator><version>
// where the operator is < <= = > >= (by default >=)
$aModuleMatches = [];
if (preg_match('|^([^/]+)/(<?>?=?)([^><=]+)$|', $sModuleId, $aModuleMatches)) {
$sModuleName = $aModuleMatches[1];
$aPotentialPrerequisites[$sModuleName] = true;
$sOperator = $aModuleMatches[2];
if ($sOperator == '') {
$sOperator = '>=';
}
$sExpectedVersion = $aModuleMatches[3];
if (array_key_exists($sModuleName, $aModuleVersions)) {
// module is present, check the version
$sCurrentVersion = $aModuleVersions[$sModuleName];
if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) {
$aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing
// a function call that results in a runtime fatal error
} else {
$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
// a function call that results in a runtime fatal error
}
} else {
// module is not present
$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
// a function call that results in a runtime fatal error
}
}
}
}
$bMissingPrerequisite = false;
foreach (array_keys($aPotentialPrerequisites) as $sModuleName) {
if (array_key_exists($sModuleName, $aSelectedModules)) {
// This module is actually a prerequisite
if (!array_key_exists($sModuleName, $aModuleVersions)) {
$bMissingPrerequisite = true;
}
}
}
if ($bMissingPrerequisite) {
$bResult = false;
} else {
$sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $sDepString);
try {
$bResult = self::GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($sBooleanExpr);
} catch (ModuleFileReaderException $e) {
//logged already
echo "Failed to parse the boolean Expression = '$sBooleanExpr'<br/>";
}
}
}
return $bResult;
}
/**
* Search (on the disk) for all defined iTop modules, load them and returns the list (as an array)
* of the possible iTop modules to install

View File

@@ -89,6 +89,7 @@ class ExtensionInstallation extends cmdbAbstractObject
MetaModel::Init_AddAttribute(new AttributeString("source", ["allowed_values" => null, "sql" => "source", "default_value" => null, "is_null_allowed" => false, "depends_on" => []]));
MetaModel::Init_AddAttribute(new AttributeEnum("uninstallable", ["allowed_values" => new ValueSetEnum('yes,no,maybe'), "sql" => "uninstallable", "default_value" => 'yes', "is_null_allowed" => false, "depends_on" => []]));
MetaModel::Init_AddAttribute(new AttributeDateTime("installed", ["allowed_values" => null, "sql" => "installed", "default_value" => 'NOW()', "is_null_allowed" => false, "depends_on" => []]));
MetaModel::Init_AddAttribute(new AttributeText("description", ["allowed_values" => null, "sql" => "description", "default_value" => null, "is_null_allowed" => true, "depends_on" => []]));
// Display lists
MetaModel::Init_SetZListItems('details', ['code', 'label', 'version', 'installed', 'source']); // Attributes to be displayed for the complete details

File diff suppressed because it is too large Load Diff

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

@@ -79,7 +79,7 @@ class InstallationFileService
public function GetItopExtensionsMap(): ItopExtensionsMap
{
if (is_null($this->oItopExtensionsMap)) {
$this->oItopExtensionsMap = new iTopExtensionsMap($this->sTargetEnvironment, true);
$this->oItopExtensionsMap = new iTopExtensionsMap($this->sTargetEnvironment);
}
return $this->oItopExtensionsMap;
}

View File

@@ -210,12 +210,12 @@ HTML;
}
}
$oPage->LinkScriptFromAppRoot('setup/setup.js');
$oPage->add_script("function CanMoveForward()\n{\n".$oStep->JSCanMoveForward()."\n}\n");
$oPage->add_script("function CanMoveBackward()\n{\n".$oStep->JSCanMoveBackward()."\n}\n");
$oPage->add('<form id="wiz_form" class="ibo-setup--wizard" method="post">');
$oPage->add('<div class="ibo-setup--wizard--content">');
$oStep->Display($oPage);
$oPage->add('</div>');
$oPage->add_script("function CanMoveForward()\n{\n".$oStep->JSCanMoveForward()."\n}\n");
$oPage->add_script("function CanMoveBackward()\n{\n".$oStep->JSCanMoveBackward()."\n}\n");
// Add the back / next buttons and the hidden form
// to store the parameters

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
@@ -1310,14 +1311,16 @@ EOF
*/
class WizStepModulesChoice extends WizardStep
{
protected static $SEP = '_';
protected $bUpgrade = false;
protected static string $SEP = '_';
protected bool $bUpgrade = false;
protected bool $bCanMoveForward = true;
protected ?Config $oConfig = null;
/**
*
* @var iTopExtensionsMap
*/
protected $oExtensionsMap;
protected iTopExtensionsMap $oExtensionsMap;
protected PhpExpressionEvaluator $oPhpExpressionEvaluator;
@@ -1325,7 +1328,7 @@ class WizStepModulesChoice extends WizardStep
* Whether we were able to load the choices from the database or not
* @var bool
*/
protected $bChoicesFromDatabase;
protected bool $bChoicesFromDatabase;
public function __construct(WizardController $oWizard, $sCurrentState)
{
@@ -1343,12 +1346,13 @@ class WizStepModulesChoice extends WizardStep
// only called if the config file exists : we are updating a previous installation !
// WARNING : we can't load this config directly, as it might be from another directory with a different approot_url (N°2684)
if ($sConfigPath !== null) {
$oConfig = new Config($sConfigPath);
$this->oConfig = new Config($sConfigPath);
$aParamValues = $oWizard->GetParamForConfigArray();
$oConfig->UpdateFromParams($aParamValues);
$this->oConfig->UpdateFromParams($aParamValues);
$this->bChoicesFromDatabase = $this->oExtensionsMap->LoadChoicesFromDatabase($oConfig);
$this->oExtensionsMap->LoadChoicesFromDatabase($this->oConfig);
$this->bChoicesFromDatabase = true;
}
}
@@ -1364,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:
@@ -1393,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' => ''];
}
@@ -1452,7 +1507,7 @@ class WizStepModulesChoice extends WizardStep
}
$oPage->add('<img src="'.$sBannerUrl.'"/>');
}
$sDescription = isset($aStepInfo['description']) ? $aStepInfo['description'] : '';
$sDescription = $aStepInfo['description'] ?? '';
$oPage->add('<span>'.$sDescription.'</span>');
$oPage->add('</div>');
@@ -1558,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;
@@ -1602,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;
}
@@ -1660,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] = [];
@@ -1846,6 +1901,7 @@ EOF
}
return $index;
}
protected function GetStepInfo($idx = null)
{
$aStepInfo = null;
@@ -1866,12 +1922,12 @@ EOF
// Additional step for the "extensions"
$aStepDefinition = [
'title' => 'Extensions',
'description' => '<h2>Select additional extensions to install. You can launch the installation again to install new extensions, but you cannot remove already installed extensions.</h2>',
'description' => '<h2>Select additional extensions to install. You can launch the installation again to install new extensions or remove installed ones.</h2>',
'banner' => '/images/icons/icons8-puzzle.svg',
'options' => [],
];
foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) {
foreach ($this->oExtensionsMap->GetAllExtensionsWithPreviouslyInstalled() as $oExtension) {
if (($oExtension->sSource !== iTopExtension::SOURCE_WIZARD) && ($oExtension->bVisible) && (count($oExtension->aMissingDependencies) == 0)) {
$aStepDefinition['options'][] = [
'extension_code' => $oExtension->sCode,
@@ -1882,16 +1938,19 @@ EOF
'modules' => $oExtension->aModules,
'mandatory' => $oExtension->bMandatory || ($oExtension->sSource === iTopExtension::SOURCE_REMOTE),
'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource),
'uninstallable' => $oExtension->CanBeUninstalled(),
'missing' => $oExtension->bRemovedFromDisk,
];
}
}
// Display this step of the wizard only if there is something to display
if (count($aStepDefinition['options']) !== 0) {
$aSteps[] = $aStepDefinition;
$this->oWizard->SetParameter('additional_extensions_modules', json_encode($aStepDefinition['options']));
}
} else {
// No wizard configuration provided, build a standard one with just one big list
// No wizard configuration provided, build a standard one with just one big list. All items are mandatory, only works when there are no conflicted modules.
$aStepDefinition = [
'title' => 'Modules Selection',
'description' => '<h2>Select the modules to install. You can launch the installation again to install new modules, but you cannot remove already installed modules.</h2>',
@@ -1945,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);
@@ -1956,20 +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);
$aFlags = static::ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $this->bUpgrade);
$bCanBeUninstalled = $this->oExtensionsMap->Get($aChoice['extension_code'])->CanBeUninstalled();
$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId);
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $bIsDefault && !$bCanBeUninstalled && !$bDisableUninstallCheck;
;
$bDisabled = $bMandatory || $bAllDisabled;
$bChecked = $bMandatory || $bSelected;
$sChecked = $bChecked ? ' checked ' : '';
$sDisabled = $bDisabled ? ' disabled data-disabled="disabled" ' : '';
$sUnremovable = !$bCanBeUninstalled ? ' unremovable ' : '';
$sHiddenInput = $bDisabled && $bChecked ? '<input type="hidden" name="choice['.$sChoiceId.']" value="'.$sChoiceId.'"/>' : '';
$oPage->add('<div class="choice" '.$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, $bCanBeUninstalled);
$sTooltip = '';
$sUnremovable = '';
if ($aFlags['missing']) {
$sTooltip .= '<span class="setup-extension-tag removed">source removed</span>';
}
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 (!$aFlags['uninstallable']) {
$sTooltip .= '<span class="setup-extension-tag notuninstallable">cannot be uninstalled</span>';
}
if ($aFlags['disabled'] && !$aFlags['checked'] && !$aFlags['uninstallable'] && !$bDisableUninstallCheck) {
$this->bCanMoveForward = false;//Disable "Next"
}
$sChecked = $aFlags['checked'] ? ' checked ' : '';
$sDisabled = $aFlags['disabled'] ? ' disabled data-disabled="disabled" ' : '';
$sMissingModule = $aFlags['missing'] ? 'setup-extension--missing' : '';
$sHiddenInput = $aFlags['disabled'] && $aFlags['checked'] ? '<input type="hidden" name="choice['.$sChoiceId.']" value="'.$sChoiceId.'"/>' : '';
$oPage->add('<div class="choice '.$sMissingModule.'" '.$sDataId.'><input class="wiz-choice '.$sUnremovable.'" id="'.$sId.'" name="choice['.$sChoiceId.']" type="checkbox" value="'.$sChoiceId.'" '.$sDisabled.$sChecked.'/>'.$sHiddenInput.'&nbsp;');
$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $aFlags['disabled'], $sTooltip);
$oPage->add('</div>');
}
$sChoiceName = null;
@@ -2004,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
}
@@ -2030,14 +2138,13 @@ EOF
}
}
protected function DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled = false, $bUninstallable = true)
protected function DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled = false, $sTooltip = '')
{
$sMoreInfo = (isset($aChoice['more_info']) && ($aChoice['more_info'] != '')) ? '<a class="setup--wizard-choice--more-info" target="_blank" href="'.$aChoice['more_info'].'">More information</a>' : '';
$sSourceLabel = isset($aChoice['source_label']) ? $aChoice['source_label'] : '';
$sSourceLabel = $aChoice['source_label'] ?? '';
$sId = utils::EscapeHtml($aChoice['extension_code']);
$sUninstallationWarning = $bUninstallable ? '' : '<span style="color:orangered" title="Once this extension has been installed, it cannot be removed">(!)</span>';
$oPage->add('<label class="setup--wizard-choice--label" for="'.$sId.'">'.$sSourceLabel.'<b>'.utils::EscapeHtml($aChoice['title']).'</b>'.'</label>&nbsp;'.$sUninstallationWarning.' '.$sMoreInfo.'');
$oPage->add('<label class="setup--wizard-choice--label" for="'.$sId.'">'.$sSourceLabel.'<b>'.utils::EscapeHtml($aChoice['title']).'</b>'.'&nbsp;'.$sTooltip.'</label> '.$sMoreInfo.'');
$sDescription = isset($aChoice['description']) ? utils::EscapeHtml($aChoice['description']) : '';
$oPage->add('<div class="setup--wizard-choice--description description">'.$sDescription.'<span id="sub_choices'.$sId.'">');
if (isset($aChoice['sub_options'])) {
@@ -2052,6 +2159,22 @@ EOF
return $sSourceDir.'/installation.xml';
}
public function CanMoveForward()
{
return true;
}
public function JSCanMoveForward()
{
return $this->bCanMoveForward ? 'return true;' : 'return false;';
}
public function GetNextButtonLabel()
{
return $this->bCanMoveForward ? 'Next' : 'Non-uninstallable extension missing';
}
}
/**
@@ -2069,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();
@@ -2133,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

@@ -2747,7 +2747,7 @@ class SynchroReplica extends DBObject implements iDisplay
$aRows = [];
foreach ($aData as $sKey => $value) {
if (strpos(CMDBSource::GetFieldType($sSQLTable, $sKey), 'blob') !== false) {
$aRows[] = ['attcode' => $sKey, 'data' => sprintf('<i>%s (%s)</i>', Dict::S('Core:AttributeBlob'), utils::BytesToFriendlyFormat(strlen($value)))];
$aRows[] = ['attcode' => $sKey, 'data' => sprintf('<i>%s (%s)</i>', Dict::S('Core:AttributeBlob'), utils::BytesToFriendlyFormat(utils::StrLen($value)))];
} else {
$aRows[] = ['attcode' => $sKey, 'data' => utils::EscapeHtml($value)];
}

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

@@ -31,6 +31,7 @@
<testsuite name="ModuleIntegration">
<file>integration-tests/DictionariesConsistencyAfterSetupTest.php</file>
<file>integration-tests/DictionariesConsistencyTest.php</file>
<file>integration-tests/iTopModulesDependencyValidationServiceTest.php</file>
</testsuite>
</testsuites>

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,26 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use ItopExtensionsMap;
use ModuleDiscovery;
class ExtensionsMapTest extends ItopTestCase
{
protected function setUp(): void
{
parent::setUp(); // TODO: Change the autogenerated stub
$this->RequireOnceItopFile('/setup/unattended-install/InstallationFileService.php');
ModuleDiscovery::ResetCache();
}
public function testGetAllExtensionsWithPreviouslyInstalledDoesNotCrash()
{
$oExtensionsMap = new iTopExtensionsMap();
$aExtensions = $oExtensionsMap->GetAllExtensionsWithPreviouslyInstalled();
$this->assertGreaterThan(0, count($aExtensions));
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use MissingDependencyException;
use ModuleDiscovery;
class ModuleDiscoveryTest extends ItopTestCase
{
public function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/runtimeenv.class.inc.php');
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
}
public function testOrderModulesByDependencies_RealExample()
{
$aModules = json_decode(file_get_contents(__DIR__.'/ressources/reallife_discovered_modules.json'), true);
$aResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true);
$aExpected = json_decode(file_get_contents(__DIR__.'/ressources/reallife_expected_ordered_modules.json'), true);
$this->assertEquals($aExpected, array_keys($aResult));
}
public function testOrderModulesByDependencies_LoadOnlyChoosenModules()
{
$aChoices = ['id1', 'id2'];
$aModules = [
"id1/1" => [
'dependencies' => [ 'id2/2'],
'label' => 'label1',
],
"id2/2" => [
'dependencies' => [],
'label' => 'label2',
],
"id3/3" => [
'dependencies' => [],
'label' => 'label3',
],
];
$aResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true, $aChoices);
$aExpected = [
"id2/2",
"id1/1",
];
$this->assertEquals($aExpected, array_keys($aResult));
}
public function testOrderModulesByDependencies_FailWhenChoosenModuleDependsOnUnchoosenModule()
{
$aChoices = ['id1'];
$aModules = [
"id1/1" => [
'dependencies' => [ 'id2/2'],
'label' => 'label1',
],
"id2/2" => [
'dependencies' => [],
'label' => 'label2',
],
];
$sExpectedMessage = <<<TXT
The following modules have unmet dependencies:
label1 (id: id1/1) depends on: ❌ id2/2
TXT;
$this->expectException(MissingDependencyException::class);
$this->expectExceptionMessage($sExpectedMessage);
ModuleDiscovery::OrderModulesByDependencies($aModules, true, $aChoices);
}
}

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

View File

@@ -0,0 +1,217 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Setup\ModuleDependency\DependencyExpression;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class DependencyExpressionTest extends ItopTestCase
{
public function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/moduledependency/dependencyexpression.class.inc.php');
}
public function testModuleDependencyInit_Invalid()
{
$oModuleDependency = new DependencyExpression('||');
$this->assertFalse($oModuleDependency->IsValid());
$this->assertFalse($oModuleDependency->IsResolved());
}
public static function WithOperatorProvider()
{
return [
"nominal case" => [
"dep" => "itop-config-mgmt/2.4.0",
'expected_operator' => '>=',
],
">" => [
"dep" => "itop-config-mgmt/>2.4.0",
'expected_operator' => '>',
],
">=" => [
"dep" => "itop-config-mgmt/>=2.4.0",
'expected_operator' => '>=',
],
"<" => [
"dep" => "itop-config-mgmt/<2.4.0",
'expected_operator' => '<',
],
"<=" => [
"dep" => "itop-config-mgmt/<=2.4.0",
'expected_operator' => '<=',
],
];
}
/**
* @dataProvider WithOperatorProvider
*/
public function testModuleDependencyInit_WithOperator($sDepId, $sExpectedOperator)
{
$oModuleDependency = new DependencyExpression($sDepId);
$this->assertEquals([$sDepId => ['itop-config-mgmt', $sExpectedOperator, '2.4.0']], $this->GetNonPublicProperty($oModuleDependency, 'aParamsPerModuleId'));
$this->assertTrue($oModuleDependency->IsValid());
$this->assertFalse($oModuleDependency->IsResolved());
;
$this->assertEquals(['itop-config-mgmt'], $oModuleDependency->GetRemainingModuleNamesToResolve());
}
public static function WithVariousOperatorProvider()
{
$aInternalStructure = ['itop-structure/3.0.0' => ['itop-structure', ">=", '3.0.0'], 'itop-portal/<3.2.1' => ['itop-portal', "<", '3.2.1']];
return [
'&&' => [
'sDepId' => 'itop-structure/3.0.0 && itop-portal/<3.2.1',
'expected_structure' => $aInternalStructure,
],
'&& with parenthesis' => [
'sDepId' => '(itop-structure/3.0.0) && (itop-portal/<3.2.1)',
'expected_structure' => $aInternalStructure,
],
'||' => [
'sDepId' => 'itop-structure/3.0.0 || itop-portal/<3.2.1',
'expected_structure' => $aInternalStructure,
],
'|| with parenthesis' => [
'sDepId' => '(itop-structure/3.0.0) || (itop-portal/<3.2.1)',
'expected_structure' => $aInternalStructure,
],
];
}
/**
* @dataProvider WithVariousOperatorProvider
*/
public function testModuleDependencyInit_WithOperand($sDepId, $sExpected)
{
$oModuleDependency = new DependencyExpression($sDepId);
$this->assertEquals($sExpected, $this->GetNonPublicProperty($oModuleDependency, 'aParamsPerModuleId'));
$this->assertTrue($oModuleDependency->IsValid());
;
$this->assertEquals(['itop-structure', 'itop-portal'], $oModuleDependency->GetRemainingModuleNamesToResolve());
}
public static function SimpleDependencyExpressionIsResolvedProvider()
{
return [
'unresolved with major version' => [
'expr' => 'itop-config-mgmt/2.4.0',
'resolved_module_versions' => ['itop-config-mgmt' => '1.2.3'],
'expected_is_resolved' => false,
],
'unresolved with minor version' => [
'expr' => 'itop-config-mgmt/2.4.1',
'resolved_module_versions' => ['itop-config-mgmt' => '2.4.0-1'],
'expected_is_resolved' => false,
],
'resolution OK with major version' => [
'expr' => 'itop-config-mgmt/2.4.0',
'resolved_module_versions' => ['itop-config-mgmt' => '2.4.2'],
'expected_is_resolved' => true,
],
'resolution OK with minor version' => [
'expr' => 'itop-config-mgmt/2.4.0',
'resolved_module_versions' => ['itop-config-mgmt' => '2.4.0-1'],
'expected_is_resolved' => true,
],
'unproper use of api' => [
'expr' => 'itop-config-mgmt/2.4.0',
'resolved_module_versions' => [],
'expected_is_resolved' => false,
],
];
}
/**
* @dataProvider SimpleDependencyExpressionIsResolvedProvider
*/
public function testSimpleDependencyExpressionIsResolved($sExpression, $aModuleVersions, $bExpectedResolved)
{
$oModuleDependency = new DependencyExpression($sExpression);
$oModuleDependency->UpdateModuleResolutionState($aModuleVersions, ['itop-config-mgmt' => true]);
$this->assertEquals($bExpectedResolved, $oModuleDependency->IsResolved());
if ($bExpectedResolved) {
$this->assertEquals([], $oModuleDependency->GetRemainingModuleNamesToResolve());
}
}
public static function ComplexDependencyExpressionIsResolvedProvider()
{
$aAllModules = ['itop-structure' => true, 'itop-portal' => true];
return [
'and + unresolved due to missing itop-portal' => [
'expr' => 'itop-structure/3.0.0 && itop-portal/3.2.1',
'resolved_module_versions' => ['itop-structure' => '3.0.0'],
'all_modules' => $aAllModules,
'expected_is_resolved' => false,
'remaining_module_names' => ['itop-portal'],
],
'and + unresolved due to unsifficient itop-portal version' => [
'expr' => 'itop-structure/3.0.0 && itop-portal/3.2.1',
'resolved_module_versions' => ['itop-structure' => '3.0.0', 'itop-portal' => '1.0.0'],
'all_modules' => $aAllModules,
'expected_is_resolved' => false,
'remaining_module_names' => ['itop-portal'],
],
'and + resolved' => [
'expr' => 'itop-structure/3.0.0 && itop-portal/3.2.1',
'resolved_module_versions' => ['itop-structure' => '3.0.0', 'itop-portal' => '3.3.3'],
'all_modules' => $aAllModules,
'expected_is_resolved' => true,
'remaining_module_names' => [],
],
'or||true (step1) + dependency expression evaluation is delayed for sorting purpose' => [
'expr' => 'itop-structure/3.0.0||true',
'resolved_module_versions' => [],
'all_modules' => $aAllModules,
'expected_is_resolved' => false,
'remaining_module_names' => ['itop-structure'],
],
'or||true (step2) + expression is evaluated because itop-structure has been resolved' => [
'expr' => 'itop-structure/3.0.0||true',
'resolved_module_versions' => ['itop-structure' => '3.0.0'],
'all_modules' => $aAllModules,
'expected_is_resolved' => true,
'remaining_module_names' => [],
],
'or||true + resolved DIRECTLY as itop-structure is not on disk (all_modules)' => [
'expr' => 'itop-structure/3.0.0||true',
'resolved_module_versions' => [],
'all_modules' => [],
'expected_is_resolved' => true,
'remaining_module_names' => ['itop-structure'],
],
'or + unresolved because dependency trick used to sort as well' => [
'expr' => 'itop-structure/3.0.0 || itop-portal/3.2.1',
'resolved_module_versions' => ['itop-structure' => '3.0.0'],
'all_modules' => $aAllModules,
'expected_is_resolved' => false,
'remaining_module_names' => ['itop-portal'],
],
'1 can be used as a boolean' => [
'expr' => 'true||1',
'resolved_module_versions' => [],
'all_modules' => [],
'expected_is_resolved' => true,
'remaining_module_names' => [],
],
];
}
/**
* @dataProvider ComplexDependencyExpressionIsResolvedProvider
*/
public function testComplexDependencyExpressionIsResolved($sExpression, $aModuleVersions, $aAllModules, $bExpectedResolved, $aRemainingModuleNames)
{
$oModuleDependency = new DependencyExpression($sExpression);
$oModuleDependency->UpdateModuleResolutionState($aModuleVersions, $aAllModules);
$this->assertEquals($aRemainingModuleNames, $oModuleDependency->GetRemainingModuleNamesToResolve());
$this->assertEquals($bExpectedResolved, $oModuleDependency->IsResolved());
}
}

View File

@@ -0,0 +1,374 @@
<?php
namespace Combodo\iTop\Test\Setup\ModuleDependency;
use Combodo\iTop\Setup\ModuleDependency\Module;
use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use MissingDependencyException;
class ModuleDependencySortTest extends ItopTestCase
{
public function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
$this->RequireOnceItopFile('setup/moduledependency/moduledependencysort.class.inc.php');
}
public function testOrderModulesByDependencies_CheckExceptionWhenAllModuleUnresolved()
{
$aModules = [
"id1/123" => [
'dependencies' => [ 'id3/666', 'id4/666'],
'label' => 'label1',
],
"id2/456" => [
'dependencies' => ['id3/666'],
'label' => 'label2',
],
];
$sExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label2 (id: id2/456) depends on: ❌ id3/666,
label1 (id: id1/123) depends on: ❌ id3/666 + ❌ id4/666
MSG;
$this->expectException(MissingDependencyException::class);
$this->expectExceptionMessage($sExpectedMessage);
ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true);
}
public function testOrderModulesByDependencies_CheckExceptionWhenSomeModuleUnresolved()
{
$aModules = [
"id1/123" => [
'dependencies' => [ 'id2/456', 'id4/666', 'id3/789'],
'label' => 'label1',
],
"id2/456" => [
'dependencies' => [],
'label' => 'label2',
],
"id3/789" => [
'dependencies' => [ 'id2/456', 'id4/666'],
'label' => 'label3',
],
];
$sExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label3 (id: id3/789) depends on: ❌ id4/666,
label1 (id: id1/123) depends on: ❌ id4/666 + ❌ id3/789
MSG;
$this->expectException(MissingDependencyException::class);
$this->expectExceptionMessage($sExpectedMessage);
ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true);
}
public function testOrderModulesByDependencies_CheckExceptionWhenCircularDependencies()
{
$aModules = [
"id1/1" => [
'dependencies' => [ 'id2/2'],
'label' => 'label1',
],
"id2/2" => [
'dependencies' => ['id3/3'],
'label' => 'label2',
],
"id3/3" => [
'dependencies' => ['id4/4'],
'label' => 'label3',
],
"id4/4" => [
'dependencies' => ['id1/1'],
'label' => 'label4',
],
];
$sExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label1 (id: id1/1) depends on: ❌ id2/2,
label4 (id: id4/4) depends on: ❌ id1/1,
label3 (id: id3/3) depends on: ❌ id4/4,
label2 (id: id2/2) depends on: ❌ id3/3
MSG;
$this->expectException(MissingDependencyException::class);
$this->expectExceptionMessage($sExpectedMessage);
ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true);
}
public function testOrderModulesByDependencies_KeepGoingEvenWithFailure()
{
$aModules = [
"id1/123" => [
'dependencies' => [ 'id2/456', 'id4/666', 'id3/789'],
'label' => 'label1',
],
"id2/456" => [
'dependencies' => [],
'label' => 'label2',
],
"id3/789" => [
'dependencies' => [ 'id2/456', 'id4/666'],
'label' => 'label3',
],
];
$aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, false);
$aExpected = [
'id2/456',
];
$this->assertEquals($aExpected, array_keys($aResult));
}
public function testOrderModulesByDependencies_Nominalcase()
{
$aModules = [
"id0/1" => [
'dependencies' => [ 'id2/2'],
'label' => 'label1',
],
"id1/1" => [
'dependencies' => [ 'id2/2'],
'label' => 'label1',
],
"id2/2" => [
'dependencies' => ['id3/3'],
'label' => 'label2',
],
"id3/3" => [
'dependencies' => ['id4/4'],
'label' => 'label3',
],
"id4/4" => [
'dependencies' => [],
'label' => 'label4',
],
];
$aExpected = [
"id4/4",
"id3/3",
"id2/2",
"id0/1",
"id1/1",
];
$aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true);
$this->assertEquals($aExpected, array_keys($aResult));
}
//warning : tricky usecase
public function testOrderModulesByDependencies_AllTermsOfOrExpressionWillImpactTheOrder()
{
$aModules = [
"id0/1" => [
'dependencies' => [ 'id2/2 || id1/1'],
'label' => 'label1',
],
"id1/1" => [
'dependencies' => [ 'id2/2'],
'label' => 'label1',
],
"id2/2" => [
'dependencies' => [],
'label' => 'label2',
],
];
$aExpected = [
"id2/2",
"id1/1",
"id0/1",
];
$aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true);
$this->assertEquals($aExpected, array_keys($aResult));
}
//WARNING: alphabetical order make setup are determinititic
public function testOrderModulesByDependencies_ResolveNoDependendenciesOrderByAlphabeticalOrder()
{
$aModules = [
"id2/2" => [
'dependencies' => [],
'label' => 'label2',
],
"id1/1" => [
'dependencies' => [],
'label' => 'label1',
],
"id3/3" => [
'dependencies' => [],
'label' => 'label3',
],
"id4/4" => [
'dependencies' => [],
'label' => 'label4',
],
"id0/1" => [
'dependencies' => [],
'label' => 'label0',
],
];
$aExpected = [
"id0/1",
"id1/1",
"id2/2",
"id3/3",
"id4/4",
];
$aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true);
$this->assertEquals($aExpected, array_keys($aResult));
}
public function testOrderModulesByDependencies_AlphabeticalOrderWithDependencies()
{
$aModules = [
"id2/2" => [
'dependencies' => ["id1/1"],
'label' => 'label2',
],
"id1/1" => [
'dependencies' => [],
'label' => 'label1',
],
"id3/3" => [
'dependencies' => ["id1/1"],
'label' => 'label3',
],
];
$aExpected = [
"id1/1",
"id2/2",
"id3/3",
];
$aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true);
$this->assertEquals($aExpected, array_keys($aResult));
}
public function testOrderModulesByDependencies_AlphabeticalOrderWithDependencies2()
{
$aModules = [
"z_id2/2" => [ //difference here
'dependencies' => ["id1/1"],
'label' => 'label2',
],
"id1/1" => [
'dependencies' => [],
'label' => 'label1',
],
"id3/3" => [
'dependencies' => ["id1/1"],
'label' => 'label3',
],
];
$aExpected = [
"id1/1",
"id3/3",
"z_id2/2",
];
$aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true);
$this->assertEquals($aExpected, array_keys($aResult));
}
public function testSortModulesByCountOfDepencenciesDescending_NoDependencies()
{
$aUnresolvedDependencyModules = [];
$this->AddModule($aUnresolvedDependencyModules, 'c', []);
$this->AddModule($aUnresolvedDependencyModules, 'b', []);
$this->AddModule($aUnresolvedDependencyModules, 'a', []);
$this->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$this->assertEquals(['a', 'b', 'c'], array_keys($aUnresolvedDependencyModules));
}
public function testSortModulesByCountOfDepencenciesDescending_NominalUseCase()
{
$aUnresolvedDependencyModules = [];
$this->AddModule($aUnresolvedDependencyModules, 'itop-change-mgmt/456', ['itop-config-mgmt/2.2.0', 'itop-tickets/2.0.0']);
$this->AddModule($aUnresolvedDependencyModules, 'itop-tickets/2.0.0', ['itop-structure/2.7.1']);
$this->AddModule($aUnresolvedDependencyModules, 'itop-config-mgmt/123', ['itop-structure/2.7.1']);
$this->AddModule($aUnresolvedDependencyModules, 'itop-structure/2.7.1', []);
$this->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$this->assertEquals(
[
'itop-structure/2.7.1',
'itop-config-mgmt/123',
'itop-tickets/2.0.0',
'itop-change-mgmt/456',
],
array_keys($aUnresolvedDependencyModules)
);
}
public function testSortModulesByCountOfDepencenciesDescending_NominalUseCaseWithMissingDependency()
{
$aUnresolvedDependencyModules = [];
$this->AddModule($aUnresolvedDependencyModules, 'itop-change-mgmt/456', ['itop-config-mgmt/2.2.0', 'itop-tickets/2.0.0']);
$this->AddModule($aUnresolvedDependencyModules, 'itop-tickets/2.0.0', ['itop-structure/2.7.1']);
$this->AddModule($aUnresolvedDependencyModules, 'itop-config-mgmt/123', ['itop-structure/2.7.1']);
$this->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$this->assertEquals(
[
'itop-config-mgmt/123',
'itop-tickets/2.0.0',
'itop-change-mgmt/456',
],
array_keys($aUnresolvedDependencyModules)
);
}
public function testSortModulesByCountOfDepencenciesDescending_FurtherVersionsOfSameModule()
{
$aUnresolvedDependencyModules = [];
$this->AddModule($aUnresolvedDependencyModules, 'moduleA/1', []);
$this->AddModule($aUnresolvedDependencyModules, 'moduleA/2', ['moduleC/1']);
$this->AddModule($aUnresolvedDependencyModules, 'moduleB/1', ['moduleA/1']);
$this->AddModule($aUnresolvedDependencyModules, 'moduleC/1', []);
$this->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$this->assertEquals(
[
'moduleA/1',
'moduleC/1',
'moduleA/2',
'moduleB/1',
],
array_keys($aUnresolvedDependencyModules)
);
}
private function AddModule(array &$aUnresolvedDependencyModules, string $sModuleId, array $aDeps)
{
$oModule = new Module($sModuleId);
$oModule->SetDependencies($aDeps);
$aUnresolvedDependencyModules[$sModuleId] = $oModule;
}
private function SortModulesByCountOfDepencenciesDescending(array &$aUnresolvedDependencyModules)
{
$this->InvokeNonPublicMethod(ModuleDependencySort::class, 'SortModulesByCountOfDepencenciesDescending', ModuleDependencySort::GetInstance(), [&$aUnresolvedDependencyModules]);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Setup\ModuleDependency\Module;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class ModuleTest extends ItopTestCase
{
public function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/moduledependency/module.class.inc.php');
}
public function testModuleInit()
{
$oModule = new Module("itop-config-mgmt/2.4.0");
$this->assertEquals("itop-config-mgmt", $oModule->GetModuleName());
$this->assertEquals("2.4.0", $oModule->GetVersion());
$this->assertEquals("itop-config-mgmt/2.4.0", $oModule->GetModuleId());
}
public function testModuleInit_NoVersion()
{
$oModule = new Module("itop-config-mgmt");
$this->assertEquals("itop-config-mgmt", $oModule->GetModuleName());
$this->assertEquals("1.0.0", $oModule->GetVersion());
$this->assertEquals("itop-config-mgmt", $oModule->GetModuleId());
}
public function testSetDependencies_ComplexExpressionsParsing()
{
$oModule = new Module("itop-bridge-datacenter-mgmt-services");
$oModule->SetDependencies([
'itop-config-mgmt/>2.7.1',
'itop-service-mgmt/=2.7.1 || itop-service-mgmt-provider/<=2.7.1',
'itop-datacenter-mgmt/3.1.0 || true && false',
]);
$this->assertEquals(
['itop-config-mgmt', 'itop-service-mgmt', 'itop-service-mgmt-provider', 'itop-datacenter-mgmt' ],
$oModule->GetUnresolvedDependencyModuleNames()
);
}
public function testIsResolved_Unresolved()
{
$oModule = new Module("itop-bridge-cmdb-ticket");
$oModule->SetDependencies(['itop-config-mgmt/2.7.1', 'itop-tickets/2.7.0']);
$this->assertEquals(['itop-config-mgmt', 'itop-tickets'], $oModule->GetUnresolvedDependencyModuleNames(), "all dependencies are unresolved");
$this->assertFalse($oModule->IsResolved());
$oModule->UpdateModuleResolutionState([], []);
$this->assertFalse($oModule->IsResolved(), "all dependencies are still unresolved");
}
public function testIsResolved_PartialResolution()
{
$oModule = new Module("itop-bridge-cmdb-ticket");
$oModule->SetDependencies(['itop-config-mgmt/2.7.1', 'itop-tickets/2.7.0']);
$oModule->UpdateModuleResolutionState(['itop-config-mgmt' => '2.7.1'], ['itop-config-mgmt' => true]);
$this->assertFalse($oModule->IsResolved(), "some dependencies are still unresolved");
$this->assertEquals(['itop-tickets'], $oModule->GetUnresolvedDependencyModuleNames(), 'one dependency is remaining');
}
public function testIsResolved_OK()
{
$oModule = new Module("itop-bridge-cmdb-ticket");
$oModule->SetDependencies(['itop-config-mgmt/2.7.1', 'itop-tickets/2.7.0']);
$oModule->UpdateModuleResolutionState(['itop-config-mgmt' => '2.7.1', 'itop-tickets' => '2.7.0'], ['itop-config-mgmt' => true, 'itop-tickets' => true]);
$this->assertTrue($oModule->IsResolved());
$this->assertEquals([], $oModule->GetUnresolvedDependencyModuleNames());
}
}

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

@@ -0,0 +1,267 @@
{
"authent-cas\/3.2.1": {
"label": "CAS SSO",
"dependencies": []
},
"authent-external\/3.2.1": {
"label": "External user authentication",
"dependencies": []
},
"authent-ldap\/3.2.1": {
"label": "User authentication based on LDAP",
"dependencies": []
},
"authent-local\/3.2.1": {
"label": "User authentication based on the local DB",
"dependencies": []
},
"combodo-backoffice-darkmoon-theme\/3.2.1": {
"label": "Backoffice: Darkmoon theme",
"dependencies": []
},
"combodo-backoffice-fullmoon-high-contrast-theme\/3.2.1": {
"label": "Backoffice: Fullmoon with high contrast accessibility theme",
"dependencies": []
},
"combodo-backoffice-fullmoon-protanopia-deuteranopia-theme\/3.2.1": {
"label": "Backoffice: Fullmoon with protonopia & deuteranopia accessibility theme",
"dependencies": []
},
"combodo-backoffice-fullmoon-tritanopia-theme\/3.2.1": {
"label": "Backoffice: Fullmoon with tritanopia accessibility theme",
"dependencies": []
},
"combodo-db-tools\/3.2.1": {
"label": "Database maintenance tools",
"dependencies": [
"itop-structure\/3.0.0"
]
},
"itop-attachments\/3.2.1": {
"label": "Tickets Attachments",
"dependencies": []
},
"itop-backup\/3.2.1": {
"label": "Backup utilities",
"dependencies": []
},
"itop-bridge-cmdb-services\/3.2.1": {
"label": "Bridge for CMDB and Services",
"dependencies": [
"itop-config-mgmt\/2.7.1",
"itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1"
]
},
"itop-bridge-cmdb-ticket\/3.2.1": {
"label": "Bridge for CMDB and Ticket",
"dependencies": [
"itop-config-mgmt\/2.7.1",
"itop-tickets\/2.7.0"
]
},
"itop-bridge-datacenter-mgmt-services\/3.2.1": {
"label": "Bridge for CMDB Virtualization objects and Services",
"dependencies": [
"itop-config-mgmt\/2.7.1",
"itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1",
"itop-datacenter-mgmt\/3.1.0"
]
},
"itop-bridge-endusers-devices-services\/3.2.1": {
"label": "Bridge for CMDB endusers objects and Services",
"dependencies": [
"itop-config-mgmt\/2.7.1",
"itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1",
"itop-endusers-devices\/3.1.0"
]
},
"itop-bridge-storage-mgmt-services\/3.2.1": {
"label": "Bridge for CMDB Virtualization objects and Services",
"dependencies": [
"itop-config-mgmt\/2.7.1",
"itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1",
"itop-storage-mgmt\/3.1.0"
]
},
"itop-bridge-virtualization-mgmt-services\/3.2.1": {
"label": "Bridge for CMDB Virtualization objects and Services",
"dependencies": [
"itop-config-mgmt\/2.7.1",
"itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1",
"itop-virtualization-mgmt\/3.1.0"
]
},
"itop-bridge-virtualization-storage\/3.2.1": {
"label": "Links between virtualization and storage",
"dependencies": [
"itop-storage-mgmt\/2.2.0",
"itop-virtualization-mgmt\/2.2.0"
]
},
"itop-change-mgmt-itil\/3.2.1": {
"label": "Change Management ITIL",
"dependencies": [
"itop-config-mgmt\/2.2.0",
"itop-tickets\/2.0.0"
]
},
"itop-change-mgmt\/3.2.1": {
"label": "Change Management",
"dependencies": [
"itop-config-mgmt\/2.2.0",
"itop-tickets\/2.0.0"
]
},
"itop-config-mgmt\/3.2.1": {
"label": "Configuration Management (CMDB)",
"dependencies": [
"itop-structure\/2.7.1"
]
},
"itop-config\/3.2.1": {
"label": "Configuration editor",
"dependencies": []
},
"itop-core-update\/3.2.1": {
"label": "iTop Core Update",
"dependencies": [
"itop-files-information\/2.7.0",
"combodo-db-tools\/2.7.0"
]
},
"itop-datacenter-mgmt\/3.2.1": {
"label": "Datacenter Management",
"dependencies": [
"itop-config-mgmt\/2.2.0"
]
},
"itop-endusers-devices\/3.2.1": {
"label": "End-user Devices Management",
"dependencies": [
"itop-config-mgmt\/2.2.0"
]
},
"itop-faq-light\/3.2.1": {
"label": "Frequently Asked Questions Database",
"dependencies": [
"itop-structure\/3.0.0 || itop-portal\/3.0.0"
]
},
"itop-files-information\/3.2.1": {
"label": "iTop files information",
"dependencies": []
},
"itop-full-itil\/3.2.1": {
"label": "Bridge - Request management ITIL + Incident management ITIL",
"dependencies": [
"itop-request-mgmt-itil\/2.3.0",
"itop-incident-mgmt-itil\/2.3.0"
]
},
"itop-hub-connector\/3.2.1": {
"label": "iTop Hub Connector",
"dependencies": [
"itop-config-mgmt\/2.4.0"
]
},
"itop-incident-mgmt-itil\/3.2.1": {
"label": "Incident Management ITIL",
"dependencies": [
"itop-config-mgmt\/2.4.0",
"itop-tickets\/2.4.0",
"itop-profiles-itil\/2.3.0"
]
},
"itop-knownerror-mgmt\/3.2.1": {
"label": "Known Errors Database",
"dependencies": [
"itop-config-mgmt\/2.2.0"
]
},
"itop-oauth-client\/3.2.1": {
"label": "OAuth 2.0 client",
"dependencies": [
"itop-welcome-itil\/3.1.0,"
]
},
"itop-portal-base\/3.2.1": {
"label": "Portal Development Library",
"dependencies": []
},
"itop-portal\/3.2.1": {
"label": "Enhanced Customer Portal",
"dependencies": [
"itop-portal-base\/2.7.0"
]
},
"itop-problem-mgmt\/3.2.1": {
"label": "Problem Management",
"dependencies": [
"itop-tickets\/2.0.0"
]
},
"itop-profiles-itil\/3.2.1": {
"label": "Create standard ITIL profiles",
"dependencies": []
},
"itop-request-mgmt-itil\/3.2.1": {
"label": "User request Management ITIL",
"dependencies": [
"itop-tickets\/2.4.0"
]
},
"itop-request-mgmt\/3.2.1": {
"label": "Simple Ticket Management",
"dependencies": [
"itop-tickets\/2.4.0"
]
},
"itop-service-mgmt-provider\/3.2.1": {
"label": "Service Management for Service Providers",
"dependencies": [
"itop-tickets\/2.0.0"
]
},
"itop-service-mgmt\/3.2.1": {
"label": "Service Management",
"dependencies": [
"itop-tickets\/2.0.0"
]
},
"itop-sla-computation\/3.2.1": {
"label": "SLA Computation",
"dependencies": []
},
"itop-storage-mgmt\/3.2.1": {
"label": "Advanced Storage Management",
"dependencies": [
"itop-config-mgmt\/2.4.0"
]
},
"itop-structure\/3.2.1": {
"label": "Core iTop Structure",
"dependencies": []
},
"itop-themes-compat\/3.2.1": {
"label": "Light grey and Test red themes compatibility",
"dependencies": [
"itop-structure\/3.1.0"
]
},
"itop-tickets\/3.2.1": {
"label": "Tickets Management",
"dependencies": [
"itop-structure\/2.7.1"
]
},
"itop-virtualization-mgmt\/3.2.1": {
"label": "Virtualization Management",
"dependencies": [
"itop-config-mgmt\/2.4.0"
]
},
"itop-welcome-itil\/3.2.1": {
"label": "ITIL skin",
"dependencies": []
}
}

View File

@@ -0,0 +1 @@
["authent-cas\/3.2.1","authent-external\/3.2.1","authent-ldap\/3.2.1","authent-local\/3.2.1","combodo-backoffice-darkmoon-theme\/3.2.1","combodo-backoffice-fullmoon-high-contrast-theme\/3.2.1","combodo-backoffice-fullmoon-protanopia-deuteranopia-theme\/3.2.1","combodo-backoffice-fullmoon-tritanopia-theme\/3.2.1","itop-attachments\/3.2.1","itop-backup\/3.2.1","itop-config\/3.2.1","itop-files-information\/3.2.1","itop-portal-base\/3.2.1","itop-portal\/3.2.1","itop-profiles-itil\/3.2.1","itop-sla-computation\/3.2.1","itop-structure\/3.2.1","itop-themes-compat\/3.2.1","itop-tickets\/3.2.1","itop-welcome-itil\/3.2.1","combodo-db-tools\/3.2.1","itop-config-mgmt\/3.2.1","itop-core-update\/3.2.1","itop-datacenter-mgmt\/3.2.1","itop-endusers-devices\/3.2.1","itop-faq-light\/3.2.1","itop-hub-connector\/3.2.1","itop-incident-mgmt-itil\/3.2.1","itop-knownerror-mgmt\/3.2.1","itop-oauth-client\/3.2.1","itop-problem-mgmt\/3.2.1","itop-request-mgmt-itil\/3.2.1","itop-request-mgmt\/3.2.1","itop-service-mgmt-provider\/3.2.1","itop-service-mgmt\/3.2.1","itop-storage-mgmt\/3.2.1","itop-virtualization-mgmt\/3.2.1","itop-bridge-cmdb-services\/3.2.1","itop-bridge-cmdb-ticket\/3.2.1","itop-bridge-datacenter-mgmt-services\/3.2.1","itop-bridge-endusers-devices-services\/3.2.1","itop-bridge-storage-mgmt-services\/3.2.1","itop-bridge-virtualization-mgmt-services\/3.2.1","itop-bridge-virtualization-storage\/3.2.1","itop-change-mgmt-itil\/3.2.1","itop-change-mgmt\/3.2.1","itop-full-itil\/3.2.1"]

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);