1<?php 2require_once(APPROOT.'/setup/parameters.class.inc.php'); 3require_once(APPROOT.'/core/cmdbsource.class.inc.php'); 4require_once(APPROOT.'/setup/modulediscovery.class.inc.php'); 5require_once(APPROOT.'/setup/moduleinstaller.class.inc.php'); 6/** 7 * Basic helper class to describe an extension, with some characteristics and a list of modules 8 */ 9class iTopExtension 10{ 11 const SOURCE_WIZARD = 'datamodels'; 12 const SOURCE_MANUAL = 'extensions'; 13 const SOURCE_REMOTE = 'data'; 14 15 /** 16 * @var string 17 */ 18 public $sCode; 19 20 /** 21 * @var string 22 */ 23 public $sVersion; 24 25 /** 26 * @var string 27 */ 28 public $sInstalledVersion; 29 30/** 31 * @var string 32 */ 33 public $sLabel; 34 35 /** 36 * @var string 37 */ 38 public $sDescription; 39 40 /** 41 * @var string 42 */ 43 public $sSource; 44 45 /** 46 * @var bool 47 */ 48 public $bMandatory; 49 50 /** 51 * @var string 52 */ 53 public $sMoreInfoUrl; 54 55 /** 56 * @var bool 57 */ 58 public $bMarkedAsChosen; 59 60 /** 61 * @var bool 62 */ 63 public $bVisible; 64 65 /** 66 * @var string[] 67 */ 68 public $aModules; 69 70 /** 71 * @var string[] 72 */ 73 public $aModuleVersion; 74 75 /** 76 * @var string 77 */ 78 public $sSourceDir; 79 80 /** 81 * 82 * @var string[] 83 */ 84 public $aMissingDependencies; 85 86 public function __construct() 87 { 88 $this->sCode = ''; 89 $this->sLabel = ''; 90 $this->sDescription = ''; 91 $this->sSource = self::SOURCE_WIZARD; 92 $this->bMandatory = false; 93 $this->sMoreInfoUrl = ''; 94 $this->bMarkedAsChosen = false; 95 $this->sVersion = ITOP_VERSION; 96 $this->sInstalledVersion = ''; 97 $this->aModules = array(); 98 $this->aModuleVersion = array(); 99 $this->sSourceDir = ''; 100 $this->bVisible = true; 101 $this->aMissingDependencies = array(); 102 } 103} 104 105/** 106 * Helper class to discover all available extensions on a given iTop system 107 */ 108class iTopExtensionsMap 109{ 110 /** 111 * The list of all discovered extensions 112 * @param string $sFromEnvironment The environment to scan 113 * @param bool $bNormailizeOldExtension true to "magically" convert some well-known old extensions (i.e. a set of modules) to the new iTopExtension format 114 * @return void 115 */ 116 protected $aExtensions; 117 118 /** 119 * The list of directories browsed using the ReadDir method when building the map 120 * @var string[] 121 */ 122 protected $aScannedDirs; 123 124 public function __construct($sFromEnvironment = 'production', $bNormalizeOldExtensions = true, $aExtraDirs = array()) 125 { 126 $this->aExtensions = array(); 127 $this->aScannedDirs = array(); 128 $this->ScanDisk($sFromEnvironment); 129 foreach($aExtraDirs as $sDir) 130 { 131 $this->ReadDir($sDir, iTopExtension::SOURCE_REMOTE); 132 } 133 $this->CheckDependencies($sFromEnvironment); 134 if ($bNormalizeOldExtensions) 135 { 136 $this->NormalizeOldExtensions(); 137 } 138 } 139 140 /** 141 * Populate the list of available (pseudo)extensions by scanning the disk 142 * where the iTop files are located 143 * @param string $sEnvironment 144 * @return void 145 */ 146 protected function ScanDisk($sEnvironment) 147 { 148 if (!$this->ReadInstallationWizard(APPROOT.'/datamodels/2.x') && !$this->ReadInstallationWizard(APPROOT.'/datamodels/2.x')) 149 { 150 if(!$this->ReadDir(APPROOT.'/datamodels/2.x', iTopExtension::SOURCE_WIZARD)) $this->ReadDir(APPROOT.'/datamodels/1.x', iTopExtension::SOURCE_WIZARD); 151 } 152 $this->ReadDir(APPROOT.'/extensions', iTopExtension::SOURCE_MANUAL); 153 $this->ReadDir(APPROOT.'/data/'.$sEnvironment.'-modules', iTopExtension::SOURCE_REMOTE); 154 } 155 156 /** 157 * Read the information contained in the "installation.xml" file in the given directory 158 * and create pseudo extensions from the list of choices described in this file 159 * @param string $sDir 160 * @return boolean Return true if the installation.xml file exists and is readable 161 */ 162 protected function ReadInstallationWizard($sDir) 163 { 164 if (!is_readable($sDir.'/installation.xml')) return false; 165 166 $oXml = new XMLParameters($sDir.'/installation.xml'); 167 foreach($oXml->Get('steps') as $aStepInfo) 168 { 169 if (array_key_exists('options', $aStepInfo)) 170 { 171 $this->ProcessWizardChoices($aStepInfo['options']); 172 } 173 if (array_key_exists('alternatives', $aStepInfo)) 174 { 175 $this->ProcessWizardChoices($aStepInfo['alternatives']); 176 } 177 } 178 return true; 179 } 180 181 /** 182 * Helper to process a "choice" array read from the installation.xml file 183 * @param array $aChoices 184 * @return void 185 */ 186 protected function ProcessWizardChoices($aChoices) 187 { 188 foreach($aChoices as $aChoiceInfo) 189 { 190 if (array_key_exists('extension_code', $aChoiceInfo)) 191 { 192 $oExtension = new iTopExtension(); 193 $oExtension->sCode = $aChoiceInfo['extension_code']; 194 $oExtension->sLabel = $aChoiceInfo['title']; 195 if (array_key_exists('modules', $aChoiceInfo)) 196 { 197 // Some wizard choices are not associated with any module 198 $oExtension->aModules = $aChoiceInfo['modules']; 199 } 200 if (array_key_exists('sub_options', $aChoiceInfo)) 201 { 202 if (array_key_exists('options', $aChoiceInfo['sub_options'])) 203 { 204 $this->ProcessWizardChoices($aChoiceInfo['sub_options']['options']); 205 } 206 if (array_key_exists('alternatives', $aChoiceInfo['sub_options'])) 207 { 208 $this->ProcessWizardChoices($aChoiceInfo['sub_options']['alternatives']); 209 } 210 } 211 $this->AddExtension($oExtension); 212 } 213 } 214 } 215 216 /** 217 * Add an extension to the list of existing extensions, taking care of removing duplicates 218 * (only the latest/greatest version is kept) 219 * @param iTopExtension $oNewExtension 220 * @return void 221 */ 222 protected function AddExtension(iTopExtension $oNewExtension) 223 { 224 foreach($this->aExtensions as $key => $oExtension) 225 { 226 if ($oExtension->sCode == $oNewExtension->sCode) 227 { 228 if (version_compare($oNewExtension->sVersion, $oExtension->sVersion, '>')) 229 { 230 // This "new" extension is "newer" than the previous one, let's replace the previous one 231 unset($this->aExtensions[$key]); 232 $this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension; 233 return; 234 } 235 else 236 { 237 // This "new" extension is not "newer" than the previous one, let's ignore it 238 return; 239 } 240 } 241 } 242 // Finally it's not a duplicate, let's add it to the list 243 $this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension; 244 } 245 246 /** 247 * Read (recursively) a directory to find if it contains extensions (or modules) 248 * @param string $sSearchDir The directory to scan 249 * @param string $sSource The 'source' value for the extensions found in this directory 250 * @param string|null $sParentExtensionId Not null if the directory is under a declared extension 251 * @return boolean 252 */ 253 protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) 254 { 255 if (!is_readable($sSearchDir)) return false; 256 $hDir = opendir($sSearchDir); 257 if ($hDir !== false) 258 { 259 if ($sParentExtensionId == null) 260 { 261 // We're not recursing, let's add the directory to the list of scanned dirs 262 $this->aScannedDirs[] = $sSearchDir; 263 } 264 $sExtensionId = null; 265 $aSubDirectories = array(); 266 267 // First check if there is an extension.xml file in this directory 268 if (is_readable($sSearchDir.'/extension.xml')) 269 { 270 $oXml = new XMLParameters($sSearchDir.'/extension.xml'); 271 $oExtension = new iTopExtension(); 272 $oExtension->sCode = $oXml->Get('extension_code'); 273 $oExtension->sLabel = $oXml->Get('label'); 274 $oExtension->sDescription = $oXml->Get('description'); 275 $oExtension->sVersion = $oXml->Get('version'); 276 $oExtension->bMandatory = ($oXml->Get('mandatory') == 'true'); 277 $oExtension->sMoreInfoUrl = $oXml->Get('more_info_url'); 278 $oExtension->sVersion = $oXml->Get('version'); 279 $oExtension->sSource = $sSource; 280 $oExtension->sSourceDir = $sSearchDir; 281 282 $sParentExtensionId = $sExtensionId = $oExtension->sCode.'/'.$oExtension->sVersion; 283 $this->AddExtension($oExtension); 284 } 285 // Then scan the other files and subdirectories 286 while (($sFile = readdir($hDir)) !== false) 287 { 288 if (($sFile !== '.') && ($sFile !== '..')) 289 { 290 $aMatches = array(); 291 if (is_dir($sSearchDir.'/'.$sFile)) 292 { 293 // Recurse after parsing all the regular files 294 $aSubDirectories[] = $sSearchDir.'/'.$sFile; 295 } 296 else if (preg_match('/^module\.(.*).php$/i', $sFile, $aMatches)) 297 { 298 // Found a module 299 $aModuleInfo = $this->GetModuleInfo($sSearchDir.'/'.$sFile); 300 // If we are not already inside a formal extension, then the module itself is considered 301 // as an extension, otherwise, the module is just added to the list of modules belonging 302 // to this extension 303 $sModuleId = $aModuleInfo[1]; 304 list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); 305 if ($sModuleVersion == '') 306 { 307 // Provide a default module version since version is mandatory when recording ExtensionInstallation 308 $sModuleVersion = '0.0.1'; 309 } 310 311 if (($sParentExtensionId !== null) && (array_key_exists($sParentExtensionId, $this->aExtensions)) && ($this->aExtensions[$sParentExtensionId] instanceof iTopExtension)) 312 { 313 // Already inside an extension, let's add this module the list of modules belonging to this extension 314 $this->aExtensions[$sParentExtensionId]->aModules[] = $sModuleName; 315 $this->aExtensions[$sParentExtensionId]->aModuleVersion[$sModuleName] = $sModuleVersion; 316 } 317 else 318 { 319 // Not already inside an folder containing an 'extension.xml' file 320 321 // Ignore non-visible modules and auto-select ones, since these are never prompted 322 // as a choice to the end-user 323 $bVisible = true; 324 if (!$aModuleInfo[2]['visible'] || isset($aModuleInfo[2]['auto_select'])) 325 { 326 $bVisible = false; 327 } 328 329 // Let's create a "fake" extension from this module (containing just this module) for backwards compatibility 330 $sExtensionId = $sModuleId; 331 332 $oExtension = new iTopExtension(); 333 $oExtension->sCode = $sModuleName; 334 $oExtension->sLabel = $aModuleInfo[2]['label']; 335 $oExtension->sDescription = ''; 336 $oExtension->sVersion = $sModuleVersion; 337 $oExtension->sSource = $sSource; 338 $oExtension->bMandatory = $aModuleInfo[2]['mandatory']; 339 $oExtension->sMoreInfoUrl = $aModuleInfo[2]['doc.more_information']; 340 $oExtension->aModules = array($sModuleName); 341 $oExtension->aModuleVersion[$sModuleName] = $sModuleVersion; 342 $oExtension->sSourceDir = $sSearchDir; 343 $oExtension->bVisible = $bVisible; 344 $this->AddExtension($oExtension); 345 } 346 } 347 } 348 } 349 closedir($hDir); 350 foreach($aSubDirectories as $sDir) 351 { 352 // Recurse inside the subdirectories 353 $this->ReadDir($sDir, $sSource, $sExtensionId); 354 } 355 return true; 356 } 357 return false; 358 } 359 360 /** 361 * Check if some extension contains a module with missing dependencies... 362 * If so, populate the aMissingDepenencies array 363 * @param string $sFromEnvironment 364 * @return void 365 */ 366 protected function CheckDependencies($sFromEnvironment) 367 { 368 $aSearchDirs = array(); 369 370 if (is_dir(APPROOT.'/datamodels/2.x')) 371 { 372 $aSearchDirs[] = APPROOT.'/datamodels/2.x'; 373 } 374 else if (is_dir(APPROOT.'/datamodels/1.x')) 375 { 376 $aSearchDirs[] = APPROOT.'/datamodels/1.x'; 377 } 378 $aSearchDirs = array_merge($aSearchDirs, $this->aScannedDirs); 379 380 try 381 { 382 $aAllModules = ModuleDiscovery::GetAvailableModules($aSearchDirs, true); 383 } 384 catch(MissingDependencyException $e) 385 { 386 // Some modules have missing dependencies 387 // Let's check what is the impact at the "extensions" level 388 foreach($this->aExtensions as $sKey => $oExtension) 389 { 390 foreach($oExtension->aModules as $sModuleName) 391 { 392 if (array_key_exists($sModuleName, $oExtension->aModuleVersion)) 393 { 394 // This information is not available for pseudo modules defined in the installation wizard, but let's ignore them 395 $sVersion = $oExtension->aModuleVersion[$sModuleName]; 396 $sModuleId = $sModuleName.'/'.$sVersion; 397 398 if (array_key_exists($sModuleId, $e->aModulesInfo)) 399 { 400 // The extension actually contains a module which has unmet dependencies 401 $aModuleInfo = $e->aModulesInfo[$sModuleId]; 402 $this->aExtensions[$sKey]->aMissingDependencies = array_merge($oExtension->aMissingDependencies, $aModuleInfo['dependencies']); 403 } 404 } 405 } 406 } 407 } 408 } 409 410 /** 411 * Read the information from a module file (module.xxx.php) 412 * Closely inspired (almost copied/pasted !!) from ModuleDiscovery::ListModuleFiles 413 * @param string $sModuleFile 414 * @return array 415 */ 416 protected function GetModuleInfo($sModuleFile) 417 { 418 static $iDummyClassIndex = 0; 419 420 $aModuleInfo = array(); // will be filled by the "eval" line below... 421 try 422 { 423 $aMatches = array(); 424 $sModuleFileContents = file_get_contents($sModuleFile); 425 $sModuleFileContents = str_replace(array('<?php', '?>'), '', $sModuleFileContents); 426 $sModuleFileContents = str_replace('__FILE__', "'".addslashes($sModuleFile)."'", $sModuleFileContents); 427 preg_match_all('/class ([A-Za-z0-9_]+) extends ([A-Za-z0-9_]+)/', $sModuleFileContents, $aMatches); 428 //print_r($aMatches); 429 $idx = 0; 430 foreach($aMatches[1] as $sClassName) 431 { 432 if (class_exists($sClassName)) 433 { 434 // rename any class declaration inside the code to prevent a "duplicate class" declaration 435 // and change its parent class as well so that nobody will find it and try to execute it 436 // Note: don't use the same naming scheme as ModuleDiscovery otherwise you 'll have the duplicate class error again !! 437 $sModuleFileContents = str_replace($sClassName.' extends '.$aMatches[2][$idx], $sClassName.'_Ext_'.($iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents); 438 } 439 $idx++; 440 } 441 // Replace the main function call by an assignment to a variable, as an array... 442 $sModuleFileContents = str_replace(array('SetupWebPage::AddModule', 'ModuleDiscovery::AddModule'), '$aModuleInfo = array', $sModuleFileContents); 443 444 eval($sModuleFileContents); // Assigns $aModuleInfo 445 446 if (count($aModuleInfo) === 0) 447 { 448 SetupPage::log_warning("Eval of $sModuleFile did not return the expected information..."); 449 } 450 } 451 catch(ParseError $e) 452 { 453 // Continue... 454 SetupPage::log_warning("Eval of $sModuleFile caused a parse error: ".$e->getMessage()." at line ".$e->getLine()); 455 } 456 catch(Exception $e) 457 { 458 // Continue... 459 SetupPage::log_warning("Eval of $sModuleFile caused an exception: ".$e->getMessage()); 460 } 461 return $aModuleInfo; 462 } 463 464 /** 465 * Get all available extensions 466 * @return iTopExtension[] 467 */ 468 public function GetAllExtensions() 469 { 470 return $this->aExtensions; 471 } 472 473 /** 474 * Mark the given extension as chosen 475 * @param string $sExtensionCode The code of the extension (code without verison number) 476 * @param bool $bMark The value to set for the bmarkAschosen flag 477 * @return void 478 */ 479 public function MarkAsChosen($sExtensionCode, $bMark = true) 480 { 481 foreach($this->aExtensions as $oExtension) 482 { 483 if ($oExtension->sCode == $sExtensionCode) 484 { 485 $oExtension->bMarkedAsChosen = $bMark; 486 break; 487 } 488 } 489 } 490 491 /** 492 * Tells if a given extension(code) is marked as chosen 493 * @param string $sExtensionCode 494 * @return boolean 495 */ 496 public function IsMarkedAsChosen($sExtensionCode) 497 { 498 foreach($this->aExtensions as $oExtension) 499 { 500 if ($oExtension->sCode == $sExtensionCode) 501 { 502 return $oExtension->bMarkedAsChosen; 503 } 504 } 505 return false; 506 } 507 508 /** 509 * Set the 'installed_version' of the given extension(code) 510 * @param string $sExtensionCode 511 * @param string $sInstalledVersion 512 * @return void 513 */ 514 protected function SetInstalledVersion($sExtensionCode, $sInstalledVersion) 515 { 516 foreach($this->aExtensions as $oExtension) 517 { 518 if ($oExtension->sCode == $sExtensionCode) 519 { 520 $oExtension->sInstalledVersion = $sInstalledVersion; 521 break; 522 } 523 } 524 } 525 526 /** 527 * Get the list of the "chosen" extensions 528 * @return iTopExtension[] 529 */ 530 public function GetChoices() 531 { 532 $aResult = array(); 533 foreach($this->aExtensions as $oExtension) 534 { 535 if ($oExtension->bMarkedAsChosen) 536 { 537 $aResult[] = $oExtension; 538 } 539 } 540 return $aResult; 541 } 542 543 /** 544 * Load the choices (i.e. MarkedAsChosen) from the database defined in the supplied Config 545 * @param Config $oConfig 546 * @return bool 547 */ 548 public function LoadChoicesFromDatabase(Config $oConfig) 549 { 550 try 551 { 552 $aInstalledExtensions = array(); 553 if (CMDBSource::DBName() === null) 554 { 555 CMDBSource::InitFromConfig($oConfig); 556 } 557 $sLatestInstallationDate = CMDBSource::QueryToScalar("SELECT max(installed) FROM ".$oConfig->Get('db_subname')."priv_extension_install"); 558 $aInstalledExtensions = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_extension_install WHERE installed = '".$sLatestInstallationDate."'"); 559 } 560 catch (MySQLException $e) 561 { 562 // No database or erroneous information 563 return false; 564 } 565 566 foreach($aInstalledExtensions as $aDBInfo) 567 { 568 $this->MarkAsChosen($aDBInfo['code']); 569 $this->SetInstalledVersion($aDBInfo['code'], $aDBInfo['version']); 570 } 571 return true; 572 } 573 574 /** 575 * Find is a single-module extension is contained within another extension 576 * @param iTopExtension $oExtension 577 * @return NULL|iTopExtension 578 */ 579 public function IsExtensionObsoletedByAnother(iTopExtension $oExtension) 580 { 581 // Complex extensions (more than 1 module) are never considered as obsolete 582 if (count($oExtension->aModules) != 1) return null; 583 584 foreach($this->GetAllExtensions() as $oOtherExtension) 585 { 586 if (($oOtherExtension->sSourceDir != $oExtension->sSourceDir) && ($oOtherExtension->sSource != iTopExtension::SOURCE_WIZARD)) 587 { 588 if (array_key_exists($oExtension->sCode, $oOtherExtension->aModuleVersion) && 589 (version_compare($oOtherExtension->aModuleVersion[$oExtension->sCode], $oExtension->sVersion, '>=')) ) 590 { 591 // Found another extension containing a more recent version of the extension/module 592 return $oOtherExtension; 593 } 594 } 595 } 596 597 // No match at all 598 return null; 599 600 } 601 602 /** 603 * Search for multi-module extensions that are NOT deployed as an extension (i.e. shipped with an extension.xml file) 604 * but as a bunch of un-related modules based on the signature of some well-known extensions. If such an extension is found, 605 * replace the stand-alone modules by an "extension" with the appropriate label/description/version containing the same modules. 606 * @param string $sInSourceOnly The source directory to scan (datamodel|extensions|data) 607 */ 608 public function NormalizeOldExtensions($sInSourceOnly = iTopExtension::SOURCE_MANUAL) 609 { 610 $aSignatures = $this->GetOldExtensionsSignatures(); 611 foreach($aSignatures as $sExtensionCode => $aExtensionSignatures) 612 { 613 $bFound = false; 614 foreach($aExtensionSignatures['versions'] as $sVersion => $aModules) 615 { 616 $bInstalled = true; 617 foreach($aModules as $sModuleId) 618 { 619 if(!$this->ModuleIsPresent($sModuleId, $sInSourceOnly)) 620 { 621 $bFound = false; 622 break; // One missing module is enough to determine that the extension/version is not present 623 } 624 else 625 { 626 $bInstalled = $bInstalled && (!$this->ModuleIsInstalled($sModuleId, $sInSourceOnly)); 627 $bFound = true; 628 } 629 } 630 if ($bFound) break; // The current version matches the signature 631 } 632 633 if ($bFound) 634 { 635 $oExtension = new iTopExtension(); 636 $oExtension->sCode = $sExtensionCode; 637 $oExtension->sLabel = $aExtensionSignatures['label']; 638 $oExtension->sSource = $sInSourceOnly; 639 $oExtension->sDescription = $aExtensionSignatures['description']; 640 $oExtension->sVersion = $sVersion; 641 $oExtension->aModules = array(); 642 if ($bInstalled) 643 { 644 $oExtension->sInstalledVersion = $sVersion; 645 $oExtension->bMarkedAsChosen = true; 646 } 647 foreach($aModules as $sModuleId) 648 { 649 list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); 650 $oExtension->aModules[] = $sModuleName; 651 } 652 $this->ReplaceModulesByNormalizedExtension($aExtensionSignatures['versions'][$sVersion], $oExtension); 653 } 654 } 655 } 656 657 /** 658 * Check if the given module-code/version is present on the disk 659 * @param string $sModuleIdToFind The module ID (code/version) to search for 660 * @param string $sInSourceOnly The origin (=source) to search in (datamodel|extensions|data) 661 * @return boolean 662 */ 663 protected function ModuleIsPresent($sModuleIdToFind, $sInSourceOnly) 664 { 665 return (array_key_exists($sModuleIdToFind, $this->aExtensions) && ($this->aExtensions[$sModuleIdToFind]->sSource == $sInSourceOnly)); 666 } 667 668 /** 669 * Check if the given module-code/version is currently installed 670 * @param string $sModuleIdToFind The module ID (code/version) to search for 671 * @param string $sInSourceOnly The origin (=source) to search in (datamodel|extensions|data) 672 * @return boolean 673 */ 674 protected function ModuleIsInstalled($sModuleIdToFind, $sInSourceOnly) 675 { 676 return (array_key_exists($sModuleIdToFind, $this->aExtensions) && 677 ($this->aExtensions[$sModuleIdToFind]->sSource == $sInSourceOnly) && 678 ($this->aExtensions[$sModuleIdToFind]->sInstalledVersion !== '') ); 679 } 680 681 /** 682 * Tells if the given module name is "chosen" since it is part of a "chosen" extension (in the specified source dir) 683 * @param string $sModuleNameToFind 684 * @param string $sInSourceOnly 685 * @return boolean 686 */ 687 public function ModuleIsChosenAsPartOfAnExtension($sModuleNameToFind, $sInSourceOnly = iTopExtension::SOURCE_REMOTE) 688 { 689 $bChosen = false; 690 691 foreach($this->GetAllExtensions() as $oExtension) 692 { 693 if (($oExtension->sSource == $sInSourceOnly) && 694 ($oExtension->bMarkedAsChosen == true) && 695 (array_key_exists($sModuleNameToFind, $oExtension->aModuleVersion))) 696 { 697 return true; 698 } 699 } 700 return false; 701 } 702 703 /** 704 * Replace a given set of stand-alone modules by one single "extension" 705 * @param string[] $aModules 706 * @param iTopExtension $oNewExtension 707 */ 708 protected function ReplaceModulesByNormalizedExtension($aModules, iTopExtension $oNewExtension) 709 { 710 foreach($aModules as $sModuleId) 711 { 712 unset($this->aExtensions[$sModuleId]); 713 } 714 $this->AddExtension($oNewExtension); 715 } 716 717 /** 718 * Get the list of signatures of some well-known multi-module extensions without extension.xml file (should not exist anymore) 719 * 720 * @return string[][]|string[][][][] 721 */ 722 protected function GetOldExtensionsSignatures() 723 { 724 // Generated by the Factory using the page export_component_versions_for_normalisation.php 725 return array ( 726 'combodo-approval-process-light' => 727 array ( 728 'label' => 'Approval process light', 729 'description' => 'Approve a request via a simple email', 730 'versions' => 731 array ( 732 '1.0.1' => 733 array ( 734 0 => 'approval-base/2.1.0', 735 1 => 'combodo-approval-light/1.0.1', 736 ), 737 '1.0.2' => 738 array ( 739 0 => 'approval-base/2.1.1', 740 1 => 'combodo-approval-light/1.0.2', 741 ), 742 '1.0.3' => 743 array ( 744 0 => 'approval-base/2.1.2', 745 1 => 'combodo-approval-light/1.0.2', 746 ), 747 '1.1.0' => 748 array ( 749 0 => 'approval-base/2.2.2', 750 1 => 'combodo-approval-light/1.0.2', 751 ), 752 '1.1.1' => 753 array ( 754 0 => 'approval-base/2.2.3', 755 1 => 'combodo-approval-light/1.0.2', 756 ), 757 '1.1.2' => 758 array ( 759 0 => 'approval-base/2.2.6', 760 1 => 'combodo-approval-light/1.0.2', 761 ), 762 '1.1.3' => 763 array ( 764 0 => 'approval-base/2.2.6', 765 1 => 'combodo-approval-light/1.0.3', 766 ), 767 '1.2.0' => 768 array ( 769 0 => 'approval-base/2.3.0', 770 1 => 'combodo-approval-light/1.0.3', 771 ), 772 '1.2.1' => 773 array ( 774 0 => 'approval-base/2.4.0', 775 1 => 'combodo-approval-light/1.0.4', 776 ), 777 '1.3.0' => 778 array ( 779 0 => 'approval-base/2.4.2', 780 1 => 'combodo-approval-light/1.1.1', 781 ), 782 '1.3.1' => 783 array ( 784 0 => 'approval-base/2.5.0', 785 1 => 'combodo-approval-light/1.1.1', 786 ), 787 '1.3.2' => 788 array ( 789 0 => 'approval-base/2.5.0', 790 1 => 'combodo-approval-light/1.1.2', 791 ), 792 '1.2.2' => 793 array ( 794 0 => 'approval-base/2.4.2', 795 1 => 'combodo-approval-light/1.0.5', 796 ), 797 '1.3.3' => 798 array ( 799 0 => 'approval-base/2.5.1', 800 1 => 'combodo-approval-light/1.1.2', 801 ), 802 '1.3.4' => 803 array ( 804 0 => 'approval-base/2.5.2', 805 1 => 'combodo-approval-light/1.1.2', 806 ), 807 '1.3.5' => 808 array ( 809 0 => 'approval-base/2.5.3', 810 1 => 'combodo-approval-light/1.1.2', 811 ), 812 '1.4.0' => 813 array ( 814 0 => 'approval-base/2.5.3', 815 1 => 'combodo-approval-light/1.1.2', 816 2 => 'itop-approval-portal/1.0.0', 817 ), 818 ), 819 ), 820 'combodo-approval-process-automation' => 821 array ( 822 'label' => 'Approval process automation', 823 'description' => 'Control your approval process with predefined rules based on service catalog', 824 'versions' => 825 array ( 826 '1.0.1' => 827 array ( 828 0 => 'approval-base/2.1.0', 829 1 => 'combodo-approval-extended/1.0.2', 830 ), 831 '1.0.2' => 832 array ( 833 0 => 'approval-base/2.1.1', 834 1 => 'combodo-approval-extended/1.0.4', 835 ), 836 '1.0.3' => 837 array ( 838 0 => 'approval-base/2.1.2', 839 1 => 'combodo-approval-extended/1.0.4', 840 ), 841 '1.1.0' => 842 array ( 843 0 => 'approval-base/2.2.2', 844 1 => 'combodo-approval-extended/1.0.4', 845 ), 846 '1.1.1' => 847 array ( 848 0 => 'approval-base/2.2.3', 849 1 => 'combodo-approval-extended/1.0.4', 850 ), 851 '1.1.2' => 852 array ( 853 0 => 'approval-base/2.2.6', 854 1 => 'combodo-approval-extended/1.0.5', 855 ), 856 '1.1.3' => 857 array ( 858 0 => 'approval-base/2.2.6', 859 1 => 'combodo-approval-extended/1.0.6', 860 ), 861 '1.2.0' => 862 array ( 863 0 => 'approval-base/2.3.0', 864 1 => 'combodo-approval-extended/1.0.7', 865 ), 866 '1.2.1' => 867 array ( 868 0 => 'approval-base/2.4.0', 869 1 => 'combodo-approval-extended/1.0.8', 870 ), 871 '1.3.0' => 872 array ( 873 0 => 'approval-base/2.4.2', 874 1 => 'combodo-approval-extended/1.2.1', 875 ), 876 '1.3.1' => 877 array ( 878 0 => 'approval-base/2.5.0', 879 1 => 'combodo-approval-extended/1.2.1', 880 ), 881 '1.3.2' => 882 array ( 883 0 => 'approval-base/2.5.0', 884 1 => 'combodo-approval-extended/1.2.2', 885 ), 886 '1.2.2' => 887 array ( 888 0 => 'approval-base/2.4.2', 889 1 => 'combodo-approval-extended/1.0.9', 890 ), 891 '1.3.3' => 892 array ( 893 0 => 'approval-base/2.5.1', 894 1 => 'combodo-approval-extended/1.2.3', 895 ), 896 '1.3.4' => 897 array ( 898 0 => 'approval-base/2.5.2', 899 1 => 'combodo-approval-extended/1.2.3', 900 ), 901 '1.3.5' => 902 array ( 903 0 => 'approval-base/2.5.3', 904 1 => 'combodo-approval-extended/1.2.3', 905 ), 906 '1.4.0' => 907 array ( 908 0 => 'approval-base/2.5.3', 909 1 => 'combodo-approval-extended/1.2.3', 910 3 => 'itop-approval-portal/1.0.0', 911 ), 912 ), 913 ), 914 'combodo-predefined-response-models' => 915 array ( 916 'label' => 'Predefined response models', 917 'description' => 'Pick common answers from a list of predefined replies grouped by categories to update tickets log', 918 'versions' => 919 array ( 920 '1.0.0' => 921 array ( 922 0 => 'precanned-replies/1.0.0', 923 1 => 'precanned-replies-pro/1.0.0', 924 ), 925 '1.0.1' => 926 array ( 927 0 => 'precanned-replies/1.0.1', 928 1 => 'precanned-replies-pro/1.0.1', 929 ), 930 '1.0.2' => 931 array ( 932 0 => 'precanned-replies/1.0.2', 933 1 => 'precanned-replies-pro/1.0.1', 934 ), 935 '1.0.3' => 936 array ( 937 0 => 'precanned-replies/1.0.3', 938 1 => 'precanned-replies-pro/1.0.1', 939 ), 940 '1.0.4' => 941 array ( 942 0 => 'precanned-replies/1.0.3', 943 1 => 'precanned-replies-pro/1.0.2', 944 ), 945 '1.0.5' => 946 array ( 947 0 => 'precanned-replies/1.0.4', 948 1 => 'precanned-replies-pro/1.0.2', 949 ), 950 '1.1.0' => 951 array ( 952 0 => 'precanned-replies/1.1.0', 953 1 => 'precanned-replies-pro/1.0.2', 954 ), 955 '1.1.1' => 956 array ( 957 0 => 'precanned-replies/1.1.1', 958 1 => 'precanned-replies-pro/1.0.2', 959 ), 960 ), 961 ), 962 'combodo-customized-request-forms' => 963 array ( 964 'label' => 'Customized request forms', 965 'description' => 'Define personalized request forms based on the service catalog. Add extra fields for a given type of request.', 966 'versions' => 967 array ( 968 '1.0.1' => 969 array ( 970 0 => 'templates-base/2.1.1', 971 1 => 'itop-request-template/1.0.0', 972 ), 973 '1.0.2' => 974 array ( 975 0 => 'templates-base/2.1.2', 976 1 => 'itop-request-template/1.0.0', 977 ), 978 '1.0.3' => 979 array ( 980 0 => 'templates-base/2.1.2', 981 1 => 'itop-request-template/1.0.1', 982 ), 983 '1.0.4' => 984 array ( 985 0 => 'templates-base/2.1.3', 986 1 => 'itop-request-template/1.0.1', 987 ), 988 '1.0.5' => 989 array ( 990 0 => 'templates-base/2.1.4', 991 1 => 'itop-request-template/1.0.1', 992 ), 993 '2.0.0' => 994 array ( 995 0 => 'templates-base/3.0.0', 996 1 => 'itop-request-template/2.0.0', 997 2 => 'itop-request-template-portal/1.0.0', 998 ), 999 '2.0.1' => 1000 array ( 1001 0 => 'templates-base/3.0.1', 1002 1 => 'itop-request-template/2.0.0', 1003 2 => 'itop-request-template-portal/1.0.0', 1004 ), 1005 '2.0.2' => 1006 array ( 1007 0 => 'templates-base/3.0.2', 1008 1 => 'itop-request-template/2.0.0', 1009 2 => 'itop-request-template-portal/1.0.0', 1010 ), 1011 '2.0.3' => 1012 array ( 1013 0 => 'templates-base/3.0.4', 1014 1 => 'itop-request-template/2.0.0', 1015 2 => 'itop-request-template-portal/1.0.0', 1016 ), 1017 '2.0.4' => 1018 array ( 1019 0 => 'templates-base/3.0.5', 1020 1 => 'itop-request-template/2.0.0', 1021 2 => 'itop-request-template-portal/1.0.0', 1022 ), 1023 '2.0.5' => 1024 array ( 1025 0 => 'templates-base/3.0.6', 1026 1 => 'itop-request-template/2.0.0', 1027 2 => 'itop-request-template-portal/1.0.0', 1028 ), 1029 '2.0.6' => 1030 array ( 1031 0 => 'templates-base/3.0.8', 1032 1 => 'itop-request-template/2.0.0', 1033 2 => 'itop-request-template-portal/1.0.0', 1034 ), 1035 '2.0.7' => 1036 array ( 1037 0 => 'templates-base/3.0.9', 1038 1 => 'itop-request-template/2.0.0', 1039 2 => 'itop-request-template-portal/1.0.0', 1040 ), 1041 '2.0.8' => 1042 array ( 1043 0 => 'templates-base/3.0.12', 1044 1 => 'itop-request-template/2.0.0', 1045 2 => 'itop-request-template-portal/1.0.0', 1046 ), 1047 ), 1048 ), 1049 'combodo-sla-considering-business-hours' => 1050 array ( 1051 'label' => 'SLA considering business hours', 1052 'description' => 'Compute SLAs taking into account service coverage window and holidays', 1053 'versions' => 1054 array ( 1055 '2.0.1' => 1056 array ( 1057 0 => 'combodo-sla-computation/2.0.1', 1058 1 => 'combodo-coverage-windows-computation/2.0.0', 1059 ), 1060 '2.1.0' => 1061 array ( 1062 0 => 'combodo-sla-computation/2.1.0', 1063 1 => 'combodo-coverage-windows-computation/2.0.0', 1064 ), 1065 '2.1.1' => 1066 array ( 1067 0 => 'combodo-sla-computation/2.1.1', 1068 1 => 'combodo-coverage-windows-computation/2.0.0', 1069 ), 1070 '2.1.2' => 1071 array ( 1072 0 => 'combodo-sla-computation/2.1.2', 1073 1 => 'combodo-coverage-windows-computation/2.0.0', 1074 ), 1075 '2.1.3' => 1076 array ( 1077 0 => 'combodo-sla-computation/2.1.2', 1078 1 => 'combodo-coverage-windows-computation/2.0.1', 1079 ), 1080 '2.0.2' => 1081 array ( 1082 0 => 'combodo-sla-computation/2.0.1', 1083 1 => 'combodo-coverage-windows-computation/2.0.1', 1084 ), 1085 '2.1.4' => 1086 array ( 1087 0 => 'combodo-sla-computation/2.1.3', 1088 1 => 'combodo-coverage-windows-computation/2.0.1', 1089 ), 1090 '2.1.5' => 1091 array ( 1092 0 => 'combodo-sla-computation/2.1.5', 1093 1 => 'combodo-coverage-windows-computation/2.0.1', 1094 ), 1095 '2.1.6' => 1096 array ( 1097 0 => 'combodo-sla-computation/2.1.5', 1098 1 => 'combodo-coverage-windows-computation/2.0.2', 1099 ), 1100 '2.1.7' => 1101 array ( 1102 0 => 'combodo-sla-computation/2.1.6', 1103 1 => 'combodo-coverage-windows-computation/2.0.2', 1104 ), 1105 '2.1.8' => 1106 array ( 1107 0 => 'combodo-sla-computation/2.1.7', 1108 1 => 'combodo-coverage-windows-computation/2.0.2', 1109 ), 1110 '2.1.9' => 1111 array ( 1112 0 => 'combodo-sla-computation/2.1.8', 1113 1 => 'combodo-coverage-windows-computation/2.0.2', 1114 ), 1115 ), 1116 ), 1117 'combodo-mail-to-ticket-automation' => 1118 array ( 1119 'label' => 'Mail to ticket automation', 1120 'description' => 'Scan several mailboxes to create or update tickets.', 1121 'versions' => 1122 array ( 1123 '2.6.0' => 1124 array ( 1125 0 => 'combodo-email-synchro/2.6.0', 1126 1 => 'itop-standard-email-synchro/2.6.0', 1127 ), 1128 '2.6.1' => 1129 array ( 1130 0 => 'combodo-email-synchro/2.6.1', 1131 1 => 'itop-standard-email-synchro/2.6.0', 1132 ), 1133 '2.6.2' => 1134 array ( 1135 0 => 'combodo-email-synchro/2.6.2', 1136 1 => 'itop-standard-email-synchro/2.6.0', 1137 ), 1138 '2.6.3' => 1139 array ( 1140 0 => 'combodo-email-synchro/2.6.2', 1141 1 => 'itop-standard-email-synchro/2.6.1', 1142 ), 1143 '2.6.4' => 1144 array ( 1145 0 => 'combodo-email-synchro/2.6.3', 1146 1 => 'itop-standard-email-synchro/2.6.2', 1147 ), 1148 '2.6.5' => 1149 array ( 1150 0 => 'combodo-email-synchro/2.6.4', 1151 1 => 'itop-standard-email-synchro/2.6.2', 1152 ), 1153 '2.6.6' => 1154 array ( 1155 0 => 'combodo-email-synchro/2.6.5', 1156 1 => 'itop-standard-email-synchro/2.6.3', 1157 ), 1158 '2.6.7' => 1159 array ( 1160 0 => 'combodo-email-synchro/2.6.6', 1161 1 => 'itop-standard-email-synchro/2.6.4', 1162 ), 1163 '2.6.8' => 1164 array ( 1165 0 => 'combodo-email-synchro/2.6.7', 1166 1 => 'itop-standard-email-synchro/2.6.4', 1167 ), 1168 '2.6.9' => 1169 array ( 1170 0 => 'combodo-email-synchro/2.6.8', 1171 1 => 'itop-standard-email-synchro/2.6.5', 1172 ), 1173 '2.6.10' => 1174 array ( 1175 0 => 'combodo-email-synchro/2.6.9', 1176 1 => 'itop-standard-email-synchro/2.6.6', 1177 ), 1178 '2.6.11' => 1179 array ( 1180 0 => 'combodo-email-synchro/2.6.10', 1181 1 => 'itop-standard-email-synchro/2.6.6', 1182 ), 1183 '2.6.12' => 1184 array ( 1185 0 => 'combodo-email-synchro/2.6.11', 1186 1 => 'itop-standard-email-synchro/2.6.6', 1187 ), 1188 '3.0.0' => 1189 array ( 1190 0 => 'combodo-email-synchro/3.0.0', 1191 1 => 'itop-standard-email-synchro/3.0.0', 1192 ), 1193 '3.0.1' => 1194 array ( 1195 0 => 'combodo-email-synchro/3.0.1', 1196 1 => 'itop-standard-email-synchro/3.0.1', 1197 ), 1198 '3.0.2' => 1199 array ( 1200 0 => 'combodo-email-synchro/3.0.2', 1201 1 => 'itop-standard-email-synchro/3.0.1', 1202 ), 1203 '3.0.3' => 1204 array ( 1205 0 => 'combodo-email-synchro/3.0.3', 1206 1 => 'itop-standard-email-synchro/3.0.3', 1207 ), 1208 '3.0.4' => 1209 array ( 1210 0 => 'combodo-email-synchro/3.0.3', 1211 1 => 'itop-standard-email-synchro/3.0.4', 1212 ), 1213 '3.0.5' => 1214 array ( 1215 0 => 'combodo-email-synchro/3.0.4', 1216 1 => 'itop-standard-email-synchro/3.0.4', 1217 ), 1218 '3.0.6' => 1219 array ( 1220 0 => 'combodo-email-synchro/3.0.5', 1221 1 => 'itop-standard-email-synchro/3.0.4', 1222 ), 1223 '3.0.7' => 1224 array ( 1225 0 => 'combodo-email-synchro/3.0.5', 1226 1 => 'itop-standard-email-synchro/3.0.5', 1227 ), 1228 ), 1229 ), 1230 'combodo-configurator-for-automatic-object-creation' => 1231 array ( 1232 'label' => 'Configurator for automatic object creation', 1233 'description' => 'Templating based on existing objects.', 1234 'versions' => 1235 array ( 1236 '1.0.13' => 1237 array ( 1238 1 => 'itop-stencils/1.0.6', 1239 ), 1240 ), 1241 ), 1242 'combodo-user-actions-configurator' => 1243 array ( 1244 'label' => 'User actions configurator', 1245 'description' => 'Configure user actions to simplify and automate processes (e.g. create an incident from a CI).', 1246 'versions' => 1247 array ( 1248 '1.0.0' => 1249 array ( 1250 0 => 'itop-object-copier/1.0.0', 1251 ), 1252 '1.0.1' => 1253 array ( 1254 0 => 'itop-object-copier/1.0.1', 1255 ), 1256 '1.0.2' => 1257 array ( 1258 0 => 'itop-object-copier/1.0.2', 1259 ), 1260 '1.0.3' => 1261 array ( 1262 0 => 'itop-object-copier/1.0.3', 1263 ), 1264 '1.1.0' => 1265 array ( 1266 0 => 'itop-object-copier/1.1.0', 1267 ), 1268 '1.1.1' => 1269 array ( 1270 0 => 'itop-object-copier/1.1.1', 1271 ), 1272 '1.1.2' => 1273 array ( 1274 0 => 'itop-object-copier/1.1.2', 1275 ), 1276 '1.1.3' => 1277 array ( 1278 0 => 'itop-object-copier/1.1.3', 1279 ), 1280 '1.1.4' => 1281 array ( 1282 0 => 'itop-object-copier/1.1.4', 1283 ), 1284 '1.1.5' => 1285 array ( 1286 0 => 'itop-object-copier/1.1.5', 1287 ), 1288 '1.1.6' => 1289 array ( 1290 0 => 'itop-object-copier/1.1.6', 1291 ), 1292 '1.1.7' => 1293 array ( 1294 0 => 'itop-object-copier/1.1.7', 1295 ), 1296 '1.1.8' => 1297 array ( 1298 0 => 'itop-object-copier/1.1.8', 1299 ), 1300 ), 1301 ), 1302 'combodo-send-updates-by-email' => 1303 array ( 1304 'label' => 'Send updates by email', 1305 'description' => 'Send an email to pre-configured contacts when a ticket log is updated.', 1306 'versions' => 1307 array ( 1308 '1.0.1' => 1309 array ( 1310 0 => 'email-reply/1.0.1', 1311 ), 1312 '1.0.3' => 1313 array ( 1314 0 => 'email-reply/1.0.3', 1315 ), 1316 '1.1.1' => 1317 array ( 1318 0 => 'email-reply/1.1.1', 1319 ), 1320 '1.1.2' => 1321 array ( 1322 0 => 'email-reply/1.1.2', 1323 ), 1324 '1.1.3' => 1325 array ( 1326 0 => 'email-reply/1.1.3', 1327 ), 1328 '1.1.4' => 1329 array ( 1330 0 => 'email-reply/1.1.4', 1331 ), 1332 '1.1.5' => 1333 array ( 1334 0 => 'email-reply/1.1.5', 1335 ), 1336 '1.1.6' => 1337 array ( 1338 0 => 'email-reply/1.1.6', 1339 ), 1340 '1.1.7' => 1341 array ( 1342 0 => 'email-reply/1.1.7', 1343 ), 1344 // 1.1.8 was never released 1345 '1.1.9' => 1346 array ( 1347 0 => 'email-reply/1.1.9', 1348 ), 1349 '1.1.10' => 1350 array ( 1351 0 => 'email-reply/1.1.10', 1352 ), 1353 ), 1354 ), 1355 ); 1356 } 1357} 1358