refactoring: code cleanup/standardization/remove all prototype stuffs

This commit is contained in:
odain
2025-09-15 11:14:14 +02:00
parent 609215857b
commit 5dea3c7ce2
13 changed files with 521 additions and 1232 deletions

View File

@@ -1,84 +0,0 @@
<?php
class XmlModule {
public string $sModuleName;
public array $aDependencyModulesNames=[];
public array $aAllDependencyModulesNames=[];
public array $aXMlMetaInfosByModuleNames=[];
public function __construct(string $sModuleName)
{
$this->sModuleName = $sModuleName;
}
public function AddDependency(string $sXmlMetaInfoUID, array $aDefiningModuleNames, array $aModules)
{
$aRemainingModules=[];
foreach ($aDefiningModuleNames as $sDefiningModuleName) {
if ($sDefiningModuleName === $this->sModuleName) {
continue;
}
if ($sDefiningModuleName === "core" || $sDefiningModuleName === "application") {
continue;
}
$aRemainingModules[]=$sDefiningModuleName;
}
if (count($aRemainingModules)==0){
return;
}
/*$aLog = ['itop-bridge-datacenter-mgmt-services', 'itop-datacenter-mgmt'];
if (in_array($sDefiningModuleName, $aLog) && in_array($this->sModuleName, $aLog)){
echo $this->sModuleName . " => $sDefiningModuleName === " . $sXmlMetaInfoUID . "\n";
}*/
$sKey=implode(' || ', $aRemainingModules);
if (! array_key_exists($sKey, $this->aXMlMetaInfosByModuleNames)){
$this->aXMlMetaInfosByModuleNames[$sKey]=[$sXmlMetaInfoUID];
} else {
if (! in_array($sXmlMetaInfoUID, $this->aXMlMetaInfosByModuleNames[$sKey])){
$this->aXMlMetaInfosByModuleNames[$sKey][]=$sXmlMetaInfoUID;
}
}
if (! array_key_exists($sKey, $this->aDependencyModulesNames)){
$aCurrentModules=[];
foreach ($aRemainingModules as $sDefiningModuleName) {
/** @var XmlModule $oXmlModule */
$oXmlModule = $aModules[$sDefiningModuleName];
$aCurrentModules[]=$oXmlModule;
}
$this->aDependencyModulesNames[$sKey]=$aCurrentModules;
}
}
public function Depends(string $sModuleName) : bool
{
return array_key_exists($sModuleName, $this->aDependencyModulesNames) || array_key_exists($sModuleName, $this->aAllDependencyModulesNames);
}
public function __toString(): string
{
return sprintf("%s (%s)", $this->sModuleName, implode(' & ', array_keys($this->aDependencyModulesNames)));
}
public function CompleteModuleDependencies(array $aAllModules) : void
{
foreach ($this->aDependencyModulesNames as $sDirectDependency => $oXmlModules){
/** @var \Combodo\iTop\Test\UnitTest\XmlModule $oDirectDepXmlModule */
$oDirectDepXmlModule = $aAllModules[$sDirectDependency] ?? null;
if (! is_null($oDirectDepXmlModule)) {
foreach ($oDirectDepXmlModule->aDependencyModulesNames as $sDirectDependency2 => $oXmlModules2) {
if (!array_key_exists($sDirectDependency2, $this->aDependencyModulesNames) && !in_array($sDirectDependency2, $this->aAllDependencyModulesNames)) {
$this->aAllDependencyModulesNames[] = $sDirectDependency2;
}
}
}
}
}
}

View File

@@ -1,27 +0,0 @@
<?php
class XmlModuleMetaInfo {
public string $sLastNodeId;
public string $sNodeName;
public string $sPath;
public string $sDelta;
public function __construct(string $sLastNodeId, string $sNodeName, string $sPath, string $sDelta)
{
$this->sLastNodeId = $sLastNodeId;
$this->sNodeName = $sNodeName;
$this->sPath = $sPath;
$this->sDelta = $sDelta;
}
public function IsDefine() : bool {
return
$this->sDelta === 'define_if_not_exists'
|| $this->sDelta === 'define';
}
public function GetUID() : string
{
return $this->sNodeName . '_' . $this->sPath;
}
}

View File

@@ -1,465 +0,0 @@
<?php
/**
* Copyright (C) 2013-2024 Combodo SAS
* This file is part of iTop.
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
*/
require_once __DIR__ . '/XmlModuleMetaInfo.php';
require_once __DIR__ . '/XmlModule.php';
/**
* @group modulesDependencyValidation
*/
class iTopModulesDependencyValidationService {
private static ?iTopModulesDependencyValidationService $oInstance;
public static array $aModulesDataByModuleName=[];
public array $aModules=[];
private string $sCurrentModule;
private array $aDefineNodes;
private array $aDependencyNodes;
private array $aAllDmFiles=[];
private array $aSoftDependencyNodes=[];
private array $aSubfoldersByModulename=[];
public static function GetInstance(): iTopModulesDependencyValidationService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new iTopModulesDependencyValidationService();
}
return self::$oInstance;
}
public static function SetInstance(?iTopModulesDependencyValidationService $instance): void {
self::$oInstance = $instance;
}
public static function GetModulesDataByModuleName() : array {
if (count(self::$aModulesDataByModuleName)==0){
glob(APPROOT.'data/production-modules/*', GLOB_ONLYDIR);
glob(APPROOT.'data/extensions/*', GLOB_ONLYDIR);
$aMainFolders = [
APPROOT.'datamodels/2.x',
APPROOT.'extensions',
APPROOT.'data/production-modules',
];
$aDirsToScan = [];
foreach ($aMainFolders as $sDir){
if (is_dir($sDir)){
$aDirsToScan = array_merge(
$aDirsToScan,
[$sDir],
glob(APPROOT."$sDir/*", GLOB_ONLYDIR)
);
}
}
foreach (ModuleDiscovery::GetAvailableModules($aDirsToScan, true) as $sModuleId => $aModuleData) {
list($sModuleName,) = \ModuleDiscovery::GetModuleName($sModuleId);
self::$aModulesDataByModuleName[$sModuleName] = $aModuleData;
}
}
return self::$aModulesDataByModuleName;
}
public function ListDatamodelFiles() : array
{
if (count($this->aAllDmFiles)==0){
$aGlobPAtterns = [
APPROOT.'datamodels/2.x/*',
APPROOT.'application',
APPROOT.'core',
APPROOT.'extensions',
APPROOT.'extensions/*',
APPROOT.'data/production-modules',
APPROOT.'data/production-modules/*',
];
foreach ($aGlobPAtterns as $sPattern) {
$this->aAllDmFiles = array_merge($this->aAllDmFiles, glob("$sPattern/datamodel.*.xml"));
}
}
return $this->aAllDmFiles;
}
private function CreateIfNeededAndGetXmlModule(string $sModuleName) : XmlModule {
/** @var XmlModule $oCurrentXmlModule */
$oCurrentXmlModule = $this->aModules[$sModuleName] ?? null;
if (is_null($oCurrentXmlModule)){
$oCurrentXmlModule = new XmlModule($sModuleName);
$this->aModules[$sModuleName] = $oCurrentXmlModule;
}
return $oCurrentXmlModule;
}
public function FetchAllDependenciesViaDM()
{
foreach ($this->ListDatamodelFiles() as $sFile) {
$this->FetchXmlMetaInfo($sFile);
}
foreach ($this->aDefineNodes as $sKey => $aModules){
foreach ($aModules as $sModuleName){
$this->CreateIfNeededAndGetXmlModule($sModuleName);
}
$aSoftlyDependentModules = $this->aSoftDependencyNodes[$sKey] ?? null;
if (is_null($aSoftlyDependentModules)){
continue;
}
foreach ($aSoftlyDependentModules as $sModuleName){
$oCurrentXmlModule = $this->CreateIfNeededAndGetXmlModule($sModuleName);
$oCurrentXmlModule->AddDependency($sKey, $aModules, $this->aModules);
}
}
foreach ($this->aDependencyNodes as $sKey => $aModules){
foreach ($aModules as $sModuleName){
$aDefiningModules = $this->aDefineNodes[$sKey] ?? null;
if (is_null($aDefiningModules)){
continue;
}
$oCurrentXmlModule = $this->CreateIfNeededAndGetXmlModule($sModuleName);
$oCurrentXmlModule->AddDependency($sKey, $aDefiningModules, $this->aModules);
}
}
$this->OrderModules();
$this->CompleteModuleDependencies();
}
public function FetchAllDependenciesViaModulesFiles()
{
$aFullnameClassesByModuleName=[];
foreach (self::GetModulesDataByModuleName() as $sModuleName => $aModuleData){
//echo "$sModuleName\n";
$aFiles = $aModuleData['datamodel'] ?? [];
$sDir = dirname($aModuleData['module_file_path']);
$aDeps=[];
foreach ($aFiles as $sFile){
if (preg_match("|.*model\.$sModuleName\.php|", $sFile)){
continue;
}
//echo "$sDir/$sFile\n";
$aDeps=array_merge($aDeps, $this->ListDeclaredFullnameClassesFromPhpFile("$sDir/$sFile"));
}
$aFullnameClassesByModuleName[$sModuleName]=$aDeps;
}
foreach ($aFullnameClassesByModuleName as $sModuleName => $aFullnameClasses){
foreach (self::GetModulesDataByModuleName() as $sModuleName2 => $aModuleData){
if ($sModuleName2 === $sModuleName){
continue;
}
$sDir = dirname($aModuleData['module_file_path']);
if (count($aFullnameClassesByModuleName)==0){
continue;
}
$sStr = "";
foreach ($aFullnameClasses as $sClass){
$sStr .= <<<TXT
-e "$sClass"
TXT;
}
if (strlen($sStr)==0){
continue;
}
$bFound=false;
foreach ($this->GetFolders($sModuleName2, $sDir) as $sFolderDir => $sGrepRecursiveCliOption) {
$sCliCmd = str_replace('\\', '\\\\\\\\', sprintf("grep -l%s %s $sFolderDir", $sGrepRecursiveCliOption, $sStr));
$sOutput = exec($sCliCmd);
if (strlen($sOutput) != 0) {
$bFound=true;
$sCliCmd = str_replace('\\', '\\\\\\\\', sprintf("grep -o%s %s $sFolderDir", $sGrepRecursiveCliOption, $sStr));
$sOutput = exec($sCliCmd);
//echo "|$sOutput|\n";
break;
}
}
if ($bFound){
$oCurrentXmlModule = $this->CreateIfNeededAndGetXmlModule($sModuleName);
$oCurrentXmlModule2 = $this->CreateIfNeededAndGetXmlModule($sModuleName2);
try{
$sKey = $this->GetFirstFoundDepsUID($sOutput);
} catch(\Exception $e){
var_dump($aFullnameClassesByModuleName);
var_dump(
[ $sStr, $sCliCmd, $sModuleName, $sModuleName2, $sOutput]
);
throw $e;
}
$oCurrentXmlModule2->AddDependency($sKey, [$sModuleName], $this->aModules);
//echo "$sModuleName2 => $sModuleName\n";
}
}
}
}
public function GetFirstFoundDepsUID(string $sOutput) : string {
if (preg_match_all('|.*:(.*)|m', $sOutput, $aMatches)){
//var_dump($aMatches);
return $aMatches[1][0];
}
throw new \Exception('no match: ' . $sOutput);
}
private function GetFolders($sModuleName2, $sDir) : array
{
if (array_key_exists($sModuleName2, $this->aSubfoldersByModulename)){
return $this->aSubfoldersByModulename[$sModuleName2];
}
$aRes=[];
$aFiles=[];
foreach (glob("$sDir/*") as $sPath){
if (! is_dir($sPath)){
$aFiles[]=$sPath;
continue;
}
if (strpos($sPath, '\.git') !== false){
continue;
}
if (strpos($sPath, 'vendor') !== false){
continue;
}
if (strpos($sPath, 'test') !== false){
continue;
}
$aRes[$sPath]="r";
}
$aRes=[ implode(' ', $aFiles) => "" ];
$this->aSubfoldersByModulename[$sModuleName2]=$aRes;
return $aRes;
}
private function GetModuleSuffix($sFile) : string
{
if (! preg_match('|.*datamodel\.([^\.]+)\.xml|', $sFile, $aMatches)){
throw new \Exception("Regexp issue: $sFile");
}
return $aMatches[1];
}
public function FetchXmlMetaInfo($sFile) : void {
$oDomDoc = new \DOMDocument('1.0', 'UTF-8');
libxml_clear_errors();
$oDomDoc->loadXml(file_get_contents($sFile));
$aErrors = libxml_get_errors();
if (count($aErrors) > 0)
{
throw new \Exception("Malformed XML");
}
$this->sCurrentModule = $this->GetModuleSuffix($sFile);
if (! isset($this->aDefineNodes)){
$this->aDefineNodes=[];
}
if (! isset($this->aDependencyNodes)){
$this->aDependencyNodes=[];
}
foreach ($oDomDoc->childNodes as $oDomNode){
$this->FetchMetaInfo($oDomDoc->childNodes);
}
}
private function FetchMetaInfo(\DOMNodeList $oDomNodeList, ?string $sPath=null)
{
/** @var \DOMNode $oDomNode */
foreach ($oDomNodeList as $oDomNode) {
/** @var \DOMAttr $oDelta */
$oDelta = $oDomNode->attributes['_delta'] ?? null;
/** @var \DOMAttr $oId */
$oId = $oDomNode->attributes['id'] ?? null;
if (! is_null($oId)) {
$sId = $oId->nodeValue;
$sCurrentPath = $sPath ? $sPath."->".$sId : $sId;
if (!is_null($oDelta)) {
$oXmlModuleMetaInfo = new XmlModuleMetaInfo($sId, $oDomNode->nodeName, $sCurrentPath, $oDelta->nodeValue);
$sKey = $oXmlModuleMetaInfo->GetUID();
if ($oXmlModuleMetaInfo->IsDefine()) {
if (array_key_exists($sKey, $this->aDefineNodes)) {
$this->aDefineNodes[$sKey][] = $this->sCurrentModule;
} else {
$this->aDefineNodes[$sKey] = [$this->sCurrentModule];
}
} else {
if (array_key_exists($sKey, $this->aDependencyNodes)) {
$this->aDependencyNodes[$sKey][] = $this->sCurrentModule;
} else {
$this->aDependencyNodes[$sKey] = [$this->sCurrentModule];
}
}
} else {
$oXmlModuleMetaInfo = new XmlModuleMetaInfo($sId, $oDomNode->nodeName, $sCurrentPath, "nodelta");
$sKey = $oXmlModuleMetaInfo->GetUID();
if (array_key_exists($sKey, $this->aSoftDependencyNodes)) {
$this->aSoftDependencyNodes[$sKey][] = $this->sCurrentModule;
} else {
$this->aSoftDependencyNodes[$sKey] = [$this->sCurrentModule];
}
}
} else if ($oDomNode instanceof \DOMElement){
$sCurrentPath = $sPath ? $sPath . '->' . $oDomNode->nodeName : $oDomNode->nodeName;
} else{
$sCurrentPath = $sPath;
}
$this->FetchMetaInfo($oDomNode->childNodes, $sCurrentPath);
}
}
private function OrderModules()
{
$aModuleDepsCount = [];
/** @var XmlModule $oXmlModule */
foreach ($this->aModules as $oXmlModule) {
$aModuleDepsCount[$oXmlModule->sModuleName] = count($oXmlModule->aDependencyModulesNames);
}
$aOrderModules=[];
while (count($aModuleDepsCount)>0) {
asort($aModuleDepsCount);
foreach ($aModuleDepsCount as $sModuleName => $iCount){
if ($iCount>0){
throw new \Exception("still deps with $sModuleName");
}
unset($aModuleDepsCount[$sModuleName]);
$aOrderModules[$sModuleName] = $this->aModules[$sModuleName];
break;
}
//echo "$sModuleName\n";
foreach ($aModuleDepsCount as $sStillToProcessModuleName => $c){
/** @var XmlModule $oXmlStillToProcessModule */
$oXmlStillToProcessModule = $this->aModules[$sStillToProcessModuleName];
if ($oXmlStillToProcessModule->Depends($sModuleName)){
$aModuleDepsCount[$sStillToProcessModuleName] = $c - 1 ;
}
}
}
$this->aModules = $aOrderModules;
}
private function CompleteModuleDependencies()
{
/** @var XmlModule $oXmlModule */
foreach ($this->aModules as $oXmlModule) {
$oXmlModule->CompleteModuleDependencies($this->aModules);
}
}
/**
* Read declared classes/interfaces in modules.php file (either directly listed files or inside autoload)
* @param string : module file path
*
* @return array: list of fullname classes
*/
public function ListDeclaredFullnameClassesFromPhpFile(string $sPath) : array
{
if (false !== strpos($sPath, 'autoload.php')){
return $this->ListDeclaredFullnameClassesFromAutoloadFile($sPath);
}
$aRes=[];
$sContent = file_get_contents($sPath);
$sNamespace='';
if (preg_match('|namespace (.*)[ ]*;|', $sContent, $aMatches)){
$sNamespace=trim($aMatches[1]) . '\\';
}
if (preg_match_all('|^class ([a-zA-Z]*) |m', $sContent, $aMatches)){
foreach($aMatches[1] as $sClass){
$aRes[]=$sNamespace.$sClass;
}
}
if (preg_match_all('|^interface ([a-zA-Z]*)|m', $sContent, $aMatches)){
foreach($aMatches[1] as $sInterface){
$aRes[]=$sNamespace.$sInterface;
}
}
return $aRes;
}
/**
* Read declared classes/interfaces autoload file
*
* @param string : module file path
*
* @return array: list of fullname classes
*/
private function ListDeclaredFullnameClassesFromAutoloadFile(string $sPath) : array
{
$sAutoloadClassMap = dirname($sPath) . "/composer/autoload_classmap.php";
//echo $sAutoloadClassMap . '\n';
if (!is_file($sAutoloadClassMap)) {
return [];
}
$sTempfile = tempnam(sys_get_temp_dir(), 'autoload_');
$sContent = file_get_contents($sAutoloadClassMap);
$sReplace=<<<TXT
\$aModuleFiles=
TXT;
$sContent = preg_replace('|return|', $sReplace, $sContent);
//var_dump($sContent);
file_put_contents($sTempfile, $sContent);
require_once $sTempfile;
@unlink($sTempfile);
$aRes=[];
foreach (array_keys($aModuleFiles) as $sClass){
if (strpos($sClass, 'InstalledVersions')){
continue;
}
$aRes[]=$sClass;
}
return $aRes;
}
}

View File

@@ -1,17 +1,20 @@
<?php
require_once(__DIR__ . '/ItopCoreModuleDependency.class.inc.php');
namespace Combodo\iTop\Setup\ModuleDependency;
require_once(__DIR__.'/moduledependency.class.inc.php');
use ModuleDiscovery;
/**
* Class that handles a modules and all its dependencies
*/
class iTopCoreModule {
class Module {
private string $sModuleId;
private string $sModuleName;
private string $sVersion;
public array $aAllDependencies;
public array $aOngoingDependencies;
public array $aInitialDependencies;
public array $aRemainingDependenciesToResolve;
public function __construct(string $sModuleId)
{
@@ -54,11 +57,11 @@ class iTopCoreModule {
*/
public function SetDependencies(array $aAllDependencies): void
{
$this->aAllDependencies = $aAllDependencies;
$this->aOngoingDependencies = [];
$this->aInitialDependencies = $aAllDependencies;
$this->aRemainingDependenciesToResolve = [];
foreach ($aAllDependencies as $sDepString){
$this->aOngoingDependencies[$sDepString]= new iTopCoreModuleDependency($sDepString);
foreach ($aAllDependencies as $sDependencyExpression){
$this->aRemainingDependenciesToResolve[$sDependencyExpression]= new ModuleDependency($sDependencyExpression);
}
}
@@ -73,9 +76,9 @@ class iTopCoreModule {
{
$aNextDependencies=[];
$bDependenciesSolved = true;
foreach($this->aOngoingDependencies as $sDepId => $oModuleDependency)
foreach($this->aRemainingDependenciesToResolve as $sDepId => $oModuleDependency)
{
/** @var iTopCoreModuleDependency $oModuleDependency*/
/** @var ModuleDependency $oModuleDependency*/
if (!$oModuleDependency->IsDependencyResolved($aModuleVersions, $aSelectedModules))
{
$aNextDependencies[$sDepId]=$oModuleDependency;
@@ -83,7 +86,7 @@ class iTopCoreModule {
}
}
$this->aOngoingDependencies=$aNextDependencies;
$this->aRemainingDependenciesToResolve=$aNextDependencies;
return $bDependenciesSolved;
}
@@ -94,8 +97,8 @@ class iTopCoreModule {
public function GetUnresolvedDependencyModuleNames(): array
{
$aRes=[];
foreach($this->aOngoingDependencies as $sDepId => $oModuleDependency) {
/** @var iTopCoreModuleDependency $oModuleDependency */
foreach($this->aRemainingDependenciesToResolve as $sDepId => $oModuleDependency) {
/** @var ModuleDependency $oModuleDependency */
$aRes = array_merge($aRes, $oModuleDependency->GetPotentialPrerequisiteModuleNames());
}

View File

@@ -1,25 +1,29 @@
<?php
namespace Combodo\iTop\Setup\ModuleDependency;
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use ModuleFileReaderException;
use RunTimeEnvironment;
/**
* Class that handles a module dependency
*/
class iTopCoreModuleDependency {
class ModuleDependency {
private static PhpExpressionEvaluator $oPhpExpressionEvaluator;
private array $aPotentialPrerequisites;
private string $sDependencyExpression;
private bool $bInvalidDependencyRegexp=false;
private array $aDepencyModuleNames;
private array $aParamsPerModuleId;
private string $sDepString;
private bool $bAlwaysUnresolved=false;
public function __construct(string $sDepString)
public function __construct(string $sDependencyExpression)
{
$this->sDepString = $sDepString;
$this->sDependencyExpression = $sDependencyExpression;
$this->aParamsPerModuleId = [];
$this->aPotentialPrerequisites = [];
$this->aDepencyModuleNames = [];
if (preg_match_all('/([^\(\)&| ]+)/', $sDepString, $aMatches))
if (preg_match_all('/([^\(\)&| ]+)/', $sDependencyExpression, $aMatches))
{
foreach($aMatches as $aMatch)
{
@@ -31,7 +35,7 @@ class iTopCoreModuleDependency {
$aModuleMatches = array();
if (preg_match('|^([^/]+)/(<?>?=?)([^><=]+)$|', $sModuleId, $aModuleMatches)) {
$sModuleName = $aModuleMatches[1];
$this->aPotentialPrerequisites[$sModuleName] = true;
$this->aDepencyModuleNames[$sModuleName] = true;
$sOperator = $aModuleMatches[2];
if ($sOperator == '') {
$sOperator = '>=';
@@ -43,7 +47,7 @@ class iTopCoreModuleDependency {
}
}
} else {
$this->bAlwaysUnresolved=true;
$this->bInvalidDependencyRegexp=true;
}
}
@@ -62,7 +66,7 @@ class iTopCoreModuleDependency {
*/
public function GetPotentialPrerequisiteModuleNames() : array
{
return array_keys($this->aPotentialPrerequisites);
return array_keys($this->aDepencyModuleNames);
}
/**
@@ -74,7 +78,7 @@ class iTopCoreModuleDependency {
*/
public function IsDependencyResolved(array $aModuleVersions, array $aSelectedModules) : bool
{
if ($this->bAlwaysUnresolved){
if ($this->bInvalidDependencyRegexp){
return false;
}
@@ -86,8 +90,8 @@ class iTopCoreModuleDependency {
$sCurrentVersion = $aModuleVersions[$sModuleName];
if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator))
{
if (array_key_exists($sModuleName, $this->aPotentialPrerequisites)) {
unset($this->aPotentialPrerequisites[$sModuleName]);
if (array_key_exists($sModuleName, $this->aDepencyModuleNames)) {
unset($this->aDepencyModuleNames[$sModuleName]);
}
$aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing
// a function call that results in a runtime fatal error
@@ -106,7 +110,7 @@ class iTopCoreModuleDependency {
}
}
foreach ($this->aPotentialPrerequisites as $sModuleName)
foreach ($this->aDepencyModuleNames as $sModuleName => $c)
{
if (array_key_exists($sModuleName, $aSelectedModules))
{
@@ -119,7 +123,7 @@ class iTopCoreModuleDependency {
}
$bResult=false;
$sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $this->sDepString);
$sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $this->sDependencyExpression);
try{
$bResult = self::GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($sBooleanExpr);
} catch(ModuleFileReaderException $e){

View File

@@ -1,17 +1,21 @@
<?php
require_once(__DIR__ . '/iTopCoreModule.class.inc.php');
namespace Combodo\iTop\Setup\ModuleDependency;
require_once(__DIR__.'/module.class.inc.php');
use MissingDependencyException;
/**
* Class that sorts module dependencies
*/
class iTopCoreModuleDependencySort {
private static iTopCoreModuleDependencySort $oInstance;
class ModuleDependencySort {
private static ModuleDependencySort $oInstance;
protected function __construct() {
}
final public static function GetInstance(): iTopCoreModuleDependencySort {
final public static function GetInstance(): ModuleDependencySort {
if (!isset(static::$oInstance)) {
static::$oInstance = new static();
}
@@ -19,7 +23,7 @@ class iTopCoreModuleDependencySort {
return static::$oInstance;
}
final public static function SetInstance(?iTopCoreModuleDependencySort $oInstance): void {
final public static function SetInstance(?ModuleDependencySort $oInstance): void {
static::$oInstance = $oInstance;
}
@@ -41,13 +45,13 @@ class iTopCoreModuleDependencySort {
$aDependsOnModuleName=[];
foreach($aUnresolvedDependencyModules as $sModuleId => $oModule) {
/** @var iTopCoreModule $oModule */
/** @var Module $oModule */
$aDependsOnModuleName[$oModule->GetModuleName()]=[];
}
foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) {
$iInDegreeCounter = 0;
/** @var iTopCoreModule $oModule */
/** @var Module $oModule */
$aUnresolvedDependencyModuleNames = $oModule->GetUnresolvedDependencyModuleNames();
foreach ($aUnresolvedDependencyModuleNames as $sModuleName) {
if (array_key_exists($sModuleName, $aDependsOnModuleName)) {
@@ -115,86 +119,4 @@ class iTopCoreModuleDependencySort {
$aUnresolvedDependencyModules=$aRes;
}
/**
* Arrange an list of modules, based on their (inter) dependencies
* @param array $aModules The list of modules to process: 'id' => $aModuleInfo
* @param bool $bAbortOnMissingDependency ...
* @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
$aUnresolvedDependencyModules = [];
$aSelectedModules = [];
foreach($aModules as $sModuleId => $aModule)
{
$oModule = new iTopCoreModule($sModuleId);
$sModuleName = $oModule->GetModuleName();
if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad))
{
$oModule->SetDependencies($aModule['dependencies']);
$aUnresolvedDependencyModules[$sModuleId]=$oModule;
$aSelectedModules[$sModuleName] = true;
}
}
ksort($aUnresolvedDependencyModules);
$aOrderedModules = [];
$aModuleVersions=[];
$iLoopCount = 1;
while(($iLoopCount < count($aModules)+1) && (count($aUnresolvedDependencyModules) > 0) )
{
foreach($aUnresolvedDependencyModules as $sModuleId => $oModule)
{
/** @var iTopCoreModule $oModule */
if ($oModule->IsModuleResolved($aModuleVersions, $aSelectedModules)){
$aOrderedModules[] = $sModuleId;
$aModuleVersions[$oModule->GetModuleName()] = $oModule->GetVersion();
unset($aUnresolvedDependencyModules[$sModuleId]);
}
}
$iLoopCount++;
}
if ($bAbortOnMissingDependency && count($aUnresolvedDependencyModules) > 0)
{
self::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$aModulesInfo = [];
$aModuleDeps = [];
foreach($aUnresolvedDependencyModules as $sModuleId => $oModule)
{
$aModule = $aModules[$sModuleId];
$aDepsWithIcons = [];
foreach($oModule->aAllDependencies as $sIndex => $sDepId)
{
if (array_key_exists($sDepId, $oModule->aOngoingDependencies))
{
$aDepsWithIcons[$sIndex] = '❌ ' . $sDepId;
} else
{
$aDepsWithIcons[$sIndex] = '✅ ' . $sDepId;
}
}
$aModuleDeps[] = "{$aModule['label']} (id: $sModuleId) depends on: ".implode(' + ', $aDepsWithIcons);
$aModulesInfo[$sModuleId] = array('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;
}
}

View File

@@ -20,8 +20,11 @@
*/
require_once(APPROOT.'setup/modulediscovery/ModuleFileReader.php');
require_once(__DIR__ . '/module/iTopCoreModuleDependencySort.class.inc.php');
require_once(__DIR__ . '/moduledependency/moduledependencysort.class.inc.php');
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use \Combodo\iTop\Setup\ModuleDependency\Module;
use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort;
class MissingDependencyException extends CoreException
{
@@ -232,68 +235,70 @@ class ModuleDiscovery
public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
// Order the modules to take into account their inter-dependencies
$aDependencies = [];
$aUnresolvedDependencyModules = [];
$aSelectedModules = [];
foreach($aModules as $sId => $aModule)
foreach($aModules as $sModuleId => $aModule)
{
list($sModuleName, ) = self::GetModuleName($sId);
$oModule = new Module($sModuleId);
$sModuleName = $oModule->GetModuleName();
if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad))
{
$aDependencies[$sId] = $aModule['dependencies'];
$oModule->SetDependencies($aModule['dependencies']);
$aUnresolvedDependencyModules[$sModuleId]=$oModule;
$aSelectedModules[$sModuleName] = true;
}
}
ksort($aDependencies);
ksort($aUnresolvedDependencyModules);
$aOrderedModules = [];
$aModuleVersions=[];
$iLoopCount = 1;
while(($iLoopCount < count($aModules)+1) && (count($aDependencies) > 0) )
while(($iLoopCount < count($aModules)+1) && (count($aUnresolvedDependencyModules) > 0) )
{
foreach($aDependencies as $sId => $aRemainingDeps)
foreach($aUnresolvedDependencyModules as $sModuleId => $oModule)
{
$bDependenciesSolved = true;
foreach($aRemainingDeps as $sDepId)
{
if (!self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules))
{
$bDependenciesSolved = false;
}
}
if ($bDependenciesSolved)
{
$aOrderedModules[] = $sId;
unset($aDependencies[$sId]);
/** @var Module $oModule */
if ($oModule->IsModuleResolved($aModuleVersions, $aSelectedModules)){
$aOrderedModules[] = $sModuleId;
$aModuleVersions[$oModule->GetModuleName()] = $oModule->GetVersion();
unset($aUnresolvedDependencyModules[$sModuleId]);
}
}
$iLoopCount++;
}
if ($bAbortOnMissingDependency && count($aDependencies) > 0)
if ($bAbortOnMissingDependency && count($aUnresolvedDependencyModules) > 0)
{
ModuleDependencySort::GetInstance()->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$aModulesInfo = [];
$aModuleDeps = [];
foreach($aDependencies as $sId => $aDeps)
foreach($aUnresolvedDependencyModules as $sModuleId => $oModule)
{
$aModule = $aModules[$sId];
$aModule = $aModules[$sModuleId];
$aDepsWithIcons = [];
foreach($aDeps as $sIndex => $sDepId)
foreach($oModule->aInitialDependencies as $sIndex => $sDepId)
{
if (self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules))
{
$aDepsWithIcons[$sIndex] = '✅ ' . $sDepId;
} else
if (array_key_exists($sDepId, $oModule->aRemainingDependenciesToResolve))
{
$aDepsWithIcons[$sIndex] = '❌ ' . $sDepId;
} else
{
$aDepsWithIcons[$sIndex] = '✅ ' . $sDepId;
}
}
$aModuleDeps[] = "{$aModule['label']} (id: $sId) depends on: ".implode(' + ', $aDepsWithIcons);
$aModulesInfo[$sId] = array('module' => $aModule, 'dependencies' => $aDepsWithIcons);
$aModuleDeps[] = "{$aModule['label']} (id: $sModuleId) depends on: ".implode(' + ', $aDepsWithIcons);
$aModulesInfo[$sModuleId] = array('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 = array();
$aResult = [];
foreach($aOrderedModules as $sId)
{
$aResult[$sId] = $aModules[$sId];
@@ -301,18 +306,6 @@ class ModuleDiscovery
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;
}
private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
{
if (!isset(static::$oPhpExpressionEvaluator)) {
@@ -322,99 +315,6 @@ class ModuleDiscovery
return static::$oPhpExpressionEvaluator;
}
protected static function DependencyIsResolved($sDepString, $aOrderedModules, $aSelectedModules)
{
$bResult = false;
$aModuleVersions = array();
// Separate the module names from their version for an easier comparison later
foreach($aOrderedModules as $sModuleId)
{
$aMatches = array();
if (preg_match('|^([^/]+)/(.*)$|', $sModuleId, $aMatches))
{
$aModuleVersions[$aMatches[1]] = $aMatches[2];
}
else
{
// No version number found, assume 1.0.0
$aModuleVersions[$sModuleId] = '1.0.0';
}
}
if (preg_match_all('/([^\(\)&| ]+)/', $sDepString, $aMatches))
{
$aReplacements = array();
$aPotentialPrerequisites = array();
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 = array();
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

@@ -1,114 +0,0 @@
<?php
/**
* Copyright (C) 2013-2024 Combodo SAS
* This file is part of iTop.
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
*/
namespace Combodo\iTop\Test\UnitTest\Integration;
use ApplicationException;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use iTopModulesDependencyValidationService;
use XmlModule;
use XmlModuleMetaInfo;
use utils;
use ModuleInstallerAPI;
/**
* @package Combodo\iTop\Test\UnitTest\Setup
*/
class iTopModulesDependencyValidationServiceTest extends ItopDataTestCase {
private iTopModulesDependencyValidationService $oiTopModulesDependencyValidationService;
private array $aFilesToRemove = [];
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
$this->RequireOnceItopFile('setup/module/iTopModulesDependencyValidationService.php');
}
protected function tearDown(): void
{
parent::tearDown(); // TODO: Change the autogenerated stub
foreach ($this->aFilesToRemove as $sTmpFile){
@unlink($sTmpFile);
}
iTopModulesDependencyValidationService::SetInstance(null);
}
/**
* Module dependency validation: make sure dependencies are correct toward classes/interfaces coming from PHP/Xml datamodel files
*/
public function testReadModuleFileData()
{
iTopModulesDependencyValidationService::GetInstance()->FetchAllDependenciesViaModulesFiles();
$this->testModulesBasedOnDMFilesOnly();
}
/**
* Module dependency validation: make sure dependencies are correct toward classes/interfaces coming from Xml datamodel files
*/
public function testModulesBasedOnDMFilesOnly()
{
iTopModulesDependencyValidationService::GetInstance()->FetchAllDependenciesViaDM();
$aErrors=[];
/** @var XmlModule $oXmlModule */
foreach (iTopModulesDependencyValidationService::GetInstance()->aModules as $sModuleName => $oXmlModule) {
$aCurrentDeps = iTopModulesDependencyValidationService::GetModulesDataByModuleName()[$sModuleName]['dependencies'] ?? [];
$aModuleErrors=[];
foreach ($oXmlModule->aDependencyModulesNames as $sDepModuleName => $oXmlModule2){
$sXmlUIDs = implode('|', $oXmlModule->aXMlMetaInfosByModuleNames[$sDepModuleName]);
$bResolved=false;
foreach ($aCurrentDeps as $sDepString){
$oModuleDependency = new \iTopCoreModuleDependency($sDepString);
if (in_array($sDepModuleName, $oModuleDependency->GetPotentialPrerequisiteModuleNames())) {
$bResolved=true;
break;
}
foreach ($oModuleDependency->GetPotentialPrerequisiteModuleNames() as $sPotentialDepModuleName){
/** @var XmlModule $oXmlModule2 */
$oXmlModule2 = iTopModulesDependencyValidationService::GetInstance()->aModules[$sPotentialDepModuleName]??null;
if (! is_null($oXmlModule2) && $oXmlModule2->Depends($sDepModuleName)){
$bResolved=true;
break;
}
}
if ($bResolved) {
break;
}
}
if (! $bResolved){
$aModuleErrors []= "$sModuleName depends on $sDepModuleName but missing in module dependencies: " . implode(' & ', $aCurrentDeps) . ". ($sXmlUIDs)";
}
}
if (count($aModuleErrors)){
$aErrors[$sModuleName]=$aModuleErrors;
}
}
$this->assertEquals(0, count($aErrors), var_export($aErrors, true));
}
}

View File

@@ -3,7 +3,6 @@
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use iTopCoreModuleDependencySort;
use MissingDependencyException;
use ModuleDiscovery;
@@ -14,29 +13,7 @@ class ModuleDiscoveryTest extends ItopDataTestCase
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
}
public static function CheckMissingDependenciesAreCorrectlyOrderedInTheExceptionProvider()
{
$sLegacyExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label1 (id: id1/123) depends on: ❌ id3/666 + ❌ id4/666,
label2 (id: id2/456) depends on: ❌ id3/666
MSG;
$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;
return [
'legacy' => [ 'message' => $sLegacyExpectedMessage, 'is_legacy' => true ],
'new computation' => [ 'message' => $sExpectedMessage, 'is_legacy' => false ],
];
}
/**
* @dataProvider CheckMissingDependenciesAreCorrectlyOrderedInTheExceptionProvider
*/
public function testOrderModulesByDependencies_CheckMissingDependenciesAreCorrectlyOrderedInTheException(string $sMessage, bool $bIsLegacy)
public function testOrderModulesByDependencies_CheckMissingDependenciesAreCorrectlyOrderedInTheException()
{
$aModules=[
"id1/123" => [
@@ -49,39 +26,18 @@ MSG;
],
];
$this->expectException(MissingDependencyException::class);
$this->expectExceptionMessage($sMessage);
if ($bIsLegacy){
ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
} else {
iTopCoreModuleDependencySort::OrderModulesByDependencies($aModules, true, null);
}
}
public static function ValidateExceptionWithSomeDependenciesResolvedProvider()
{
$sExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label3 (id: id3/789) depends on: ✅ id2/456 + ❌ id4/666,
label1 (id: id1/123) depends on: id2/456 + ❌ id4/666 + ❌ id3/789
MSG;
$sLegacyExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label1 (id: id1/123) depends on: ✅ id2/456 + ❌ id4/666 + ❌ id3/789,
label3 (id: id3/789) depends on: ✅ id2/456 + ❌ id4/666
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);
return [
'legacy' => [ 'message' => $sLegacyExpectedMessage, 'is_legacy' => true ],
'new computation' => [ 'message' => $sExpectedMessage, 'is_legacy' => false ],
];
ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
}
/**
* @dataProvider ValidateExceptionWithSomeDependenciesResolvedProvider
*/
public function testOrderModulesByDependencies_ValidateExceptionWithSomeDependenciesResolved(string $sMessage, bool $bIsLegacy)
public function testOrderModulesByDependencies_ValidateExceptionWithSomeDependenciesResolved()
{
$aModules=[
"id1/123" => [
@@ -98,14 +54,15 @@ MSG;
],
];
$sExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label3 (id: id3/789) depends on: ✅ id2/456 + ❌ id4/666,
label1 (id: id1/123) depends on: ✅ id2/456 + ❌ id4/666 + ❌ id3/789
MSG;
$this->expectException(MissingDependencyException::class);
$this->expectExceptionMessage($sMessage);
$this->expectExceptionMessage($sExpectedMessage);
if ($bIsLegacy){
ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
} else {
iTopCoreModuleDependencySort::OrderModulesByDependencies($aModules, true, null);
}
ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
}
public function testOrderModulesByDependencies_KeepGoingEvenWithFailure_WithSomeDependenciesResolved()
@@ -125,44 +82,16 @@ MSG;
],
];
$aResult = iTopCoreModuleDependencySort::OrderModulesByDependencies($aModules, false, null);
$aLegacyResult = ModuleDiscovery::OrderModulesByDependencies($aModules, false, null);
$aResult = ModuleDiscovery::OrderModulesByDependencies($aModules, false, null);
$aExpected = [
'id2/456',
];
$this->assertEquals($aExpected, array_keys($aLegacyResult));
$this->assertEquals( $aLegacyResult, $aResult);
$this->assertEquals($aExpected, array_keys($aResult));
}
public static function UnResolveWithCircularDependencyProvider()
{
$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;
$sLegacyExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label1 (id: id1/1) depends on: ❌ id2/2,
label2 (id: id2/2) depends on: ❌ id3/3,
label3 (id: id3/3) depends on: ❌ id4/4,
label4 (id: id4/4) depends on: ❌ id1/1
MSG;
return [
'legacy' => [ 'message' => $sLegacyExpectedMessage, 'is_legacy' => true ],
'new computation' => [ 'message' => $sExpectedMessage, 'is_legacy' => false ],
];
}
/**
* @dataProvider UnResolveWithCircularDependencyProvider
*/
public function testOrderModulesByDependencies_UnResolveWithCircularDependency(string $sMessage, bool $bIsLegacy)
public function testOrderModulesByDependencies_UnResolveWithCircularDependency()
{
$aModules=[
"id1/1" => [
@@ -183,14 +112,17 @@ MSG;
],
];
$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($sMessage);
$this->expectExceptionMessage($sExpectedMessage);
if ($bIsLegacy){
ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
} else {
iTopCoreModuleDependencySort::OrderModulesByDependencies($aModules, true, null);
}
ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
}
public function testOrderModulesByDependencies_ResolveOk()
@@ -226,13 +158,50 @@ MSG;
"id0/1",
];
$aResult = iTopCoreModuleDependencySort::OrderModulesByDependencies($aModules, true, null);
$aLegacyResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
$aResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
$this->assertEquals($aExpected, array_keys($aLegacyResult));
$this->assertEquals( $aLegacyResult, $aResult);
$this->assertEquals($aExpected, array_keys($aResult));
}
public function testOrderModulesByDependencies_ResolveOk2()
{
$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 = ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
$this->assertEquals($aExpected, array_keys($aResult));
}
public function testOrderModulesByDependencies_ResolveNoDependendenciesOrderByAlphabeticalOrder()
{
$aModules=[
@@ -266,11 +235,9 @@ MSG;
"id4/4",
];
$aResult = iTopCoreModuleDependencySort::OrderModulesByDependencies($aModules, true, null);
$aLegacyResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
$aResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
$this->assertEquals($aExpected, array_keys($aLegacyResult));
$this->assertEquals( $aLegacyResult, $aResult);
$this->assertEquals($aExpected, array_keys($aResult));
}
public function testOrderModulesByDependencies_ResolveOk_ModulesToLoadProvided()
@@ -301,103 +268,20 @@ MSG;
"id1/1",
];
$aResult = iTopCoreModuleDependencySort::OrderModulesByDependencies($aModules, true, ['id1', 'id2', $sLastModuleNameToLoad]);
$aLegacyResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true, ['id1', 'id2', $sLastModuleNameToLoad]);
$aResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true, ['id1', 'id2', $sLastModuleNameToLoad]);
$this->assertEquals($aExpected, array_keys($aLegacyResult));
$this->assertEquals( $aLegacyResult, $aResult);
$this->assertEquals($aExpected, array_keys($aResult));
}
}
public function testOrderModulesByDependenciesNewComputation_RealExample(){
$aModules = json_decode(file_get_contents(__DIR__ . '/ressources/module_deps.json'), true);
$aResult = iTopCoreModuleDependencySort::OrderModulesByDependencies($aModules, true, null);
//$aResult = iTopCoreModuleDependencySort::OrderModulesByDependencies($aModules, true, null);
$aLegacyResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true, null);
$aExpected = json_decode(file_get_contents(__DIR__ . '/ressources/expected_ordered_module_ids.json'), true);
$this->assertEquals( $aLegacyResult, $aResult);
//$this->assertEquals( $aLegacyResult, $aResult);
$this->assertEquals($aExpected, array_keys($aLegacyResult));
}
public function testSortModulesByCountOfDepencenciesDescending_NoDependencies(){
$aUnresolvedDependencyModules = [];
foreach (['c', 'b', 'a'] as $sModuleId){
$this->AddModule($aUnresolvedDependencyModules, $sModuleId, []);
}
iTopCoreModuleDependencySort::GetInstance()->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', []);
iTopCoreModuleDependencySort::GetInstance()->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']);
iTopCoreModuleDependencySort::GetInstance()->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', []);
iTopCoreModuleDependencySort::GetInstance()->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 \iTopCoreModule($sModuleId);
$oModule->SetDependencies($aDeps);
$aUnresolvedDependencyModules[$sModuleId]= $oModule;
}
public function testSortModulesByCountOfDepencenciesDescending_RealExample(){
$aUnresolvedDependencyModules = [];
$aDependencies = json_decode(file_get_contents(__DIR__ . '/ressources/module_deps.json'), true);
foreach ($aDependencies as $sModuleId => $aModuleData){
$this->AddModule($aUnresolvedDependencyModules, $sModuleId, $aModuleData['dependencies']);
}
iTopCoreModuleDependencySort::GetInstance()->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$aExpected = json_decode(file_get_contents(__DIR__ . '/ressources/expected_ordered_module_ids2.json'), true);
$this->assertEquals(
$aExpected,
array_keys($aUnresolvedDependencyModules));
}
}

View File

@@ -1,99 +0,0 @@
<?php
/**
* Copyright (C) 2013-2024 Combodo SAS
* This file is part of iTop.
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
*/
namespace Combodo\iTop\Test\UnitTest\Setup\Module;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use iTopModulesDependencyValidationService;
/**
* @package Combodo\iTop\Test\UnitTest\Setup
*/
class iTopModulesDependencyValidationServiceTest extends ItopTestCase {
private iTopModulesDependencyValidationService $oiTopModulesDependencyValidationService;
private array $aFilesToRemove = [];
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/module/iTopModulesDependencyValidationService.php');
}
protected function tearDown(): void
{
parent::tearDown(); // TODO: Change the autogenerated stub
foreach ($this->aFilesToRemove as $sTmpFile){
@unlink($sTmpFile);
}
iTopModulesDependencyValidationService::SetInstance(null);
}
public function testListDeclaredFullnameClassesFromPhpFile()
{
$aExpected = [
'CMDBChangeOp',
'CMDBChangeOpCreate',
'CMDBChangeOpDelete',
'CMDBChangeOpSetAttribute',
'CMDBChangeOpSetAttributeScalar',
'CMDBChangeOpSetAttributeTagSet',
'CMDBChangeOpSetAttributeURL',
'CMDBChangeOpSetAttributeBlob',
'CMDBChangeOpSetAttributeOneWayPassword',
'CMDBChangeOpSetAttributeEncrypted',
'CMDBChangeOpSetAttributeText',
'CMDBChangeOpSetAttributeLongText',
'CMDBChangeOpSetAttributeHTML',
'CMDBChangeOpSetAttributeCaseLog',
'CMDBChangeOpPlugin',
'CMDBChangeOpSetAttributeLinksAddRemove',
'CMDBChangeOpSetAttributeLinksTune',
'CMDBChangeOpSetAttributeCustomFields',
'iCMDBChangeOp',
];
$this->assertEquals($aExpected, iTopModulesDependencyValidationService::GetInstance()->ListDeclaredFullnameClassesFromPhpFile(APPROOT . 'core/cmdbchangeop.class.inc.php'));
}
public function testListDeclaredFullnameClassesFromAutoloadFile()
{
$aExpected = [
'Combodo\iTop\OAuthClient\Controller\AjaxOauthClientController',
'Combodo\iTop\OAuthClient\Controller\OAuthClientController',
'Combodo\iTop\OAuthClient\Service\ApplicationUIExtension',
'Combodo\iTop\OAuthClient\Service\PopupMenuExtension',
];
$this->assertEquals($aExpected, iTopModulesDependencyValidationService::GetInstance()->ListDeclaredFullnameClassesFromPhpFile(APPROOT . 'datamodels/2.x/itop-oauth-client/vendor/autoload.php'));
}
public function testGetFirstFoundDepsUID() {
$sOutput=<<<TXT
/var/www/html/Professional-3.2.1-16428/web/datamodels/2.x/authent-token/src/Hook/MyAccountSectionTabContentExtension.php:Combodo\iTop\MyAccount\Hook\iMyAccountTabContentExtension
TXT;
$this->assertEquals('Combodo\iTop\MyAccount\Hook\iMyAccountTabContentExtension', iTopModulesDependencyValidationService::GetInstance()->GetFirstFoundDepsUID($sOutput));
$sOutput=<<<TXT
/var/www/html/Professional-3.2.1-16428/web/datamodels/2.x/authent-token/src/Hook/MyAccountSectionTabContentExtension.php:Combodo\iTop\MyAccount\Hook\iMyAccountTabContentExtension
/var/www/html/Professional-3.2.1-16428/web/datamodels/2.x/authent-token/src/Hook/MyAccountSectionTabContentExtension2.php:Combodo\iTop\MyAccount\Hook\iMyAccountTabContentExtension2
/var/www/html/Professional-3.2.1-16428/web/datamodels/2.x/authent-token/src/Hook/MyAccountSectionTabContentExtension3.php:Combodo\iTop\MyAccount\Hook\iMyAccountTabContentExtension3
TXT;
$this->assertEquals('Combodo\iTop\MyAccount\Hook\iMyAccountTabContentExtension', iTopModulesDependencyValidationService::GetInstance()->GetFirstFoundDepsUID($sOutput));
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Combodo\iTop\Test\Setup\ModuleDependency;
use Combodo\iTop\Setup\ModuleDependency\Module;
use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
class ModuleDiscoveryTest extends ItopDataTestCase
{
public function setUp(): void {
parent::setUp();
$this->RequireOnceItopFile('setup/moduledependency/moduledependencysort.class.inc.php');
}
public function testSortModulesByCountOfDepencenciesDescending_NoDependencies(){
$aUnresolvedDependencyModules = [];
foreach (['c', 'b', 'a'] as $sModuleId){
$this->AddModule($aUnresolvedDependencyModules, $sModuleId, []);
}
ModuleDependencySort::GetInstance()->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', []);
ModuleDependencySort::GetInstance()->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']);
ModuleDependencySort::GetInstance()->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', []);
ModuleDependencySort::GetInstance()->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;
}
public function testSortModulesByCountOfDepencenciesDescending_RealExample(){
$aUnresolvedDependencyModules = [];
$aDependencies = json_decode(file_get_contents(__DIR__ . '/ressources/module_deps.json'), true);
foreach ($aDependencies as $sModuleId => $aModuleData){
$this->AddModule($aUnresolvedDependencyModules, $sModuleId, $aModuleData['dependencies']);
}
ModuleDependencySort::GetInstance()->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$aExpected = json_decode(file_get_contents(__DIR__ . '/ressources/expected_ordered_module_ids2.json'), true);
$this->assertEquals(
$aExpected,
array_keys($aUnresolvedDependencyModules));
}
}

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-profiles-itil\/3.2.1","itop-sla-computation\/3.2.1","itop-structure\/3.2.1","itop-welcome-itil\/3.2.1","combodo-db-tools\/3.2.1","itop-config-mgmt\/3.2.1","itop-datacenter-mgmt\/3.2.1","itop-endusers-devices\/3.2.1","itop-hub-connector\/3.2.1","itop-knownerror-mgmt\/3.2.1","itop-oauth-client\/3.2.1","itop-portal\/3.2.1","itop-storage-mgmt\/3.2.1","itop-themes-compat\/3.2.1","itop-tickets\/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-virtualization-mgmt\/3.2.1","itop-bridge-cmdb-ticket\/3.2.1","itop-bridge-virtualization-storage\/3.2.1","itop-change-mgmt-itil\/3.2.1","itop-change-mgmt\/3.2.1","itop-core-update\/3.2.1","itop-faq-light\/3.2.1","itop-bridge-cmdb-services\/3.2.1","itop-incident-mgmt-itil\/3.2.1","itop-full-itil\/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"]

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": []
}
}