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