1<?php
2// Copyright (C) 2010-2018 Combodo SARL
3//
4//   This file is part of iTop.
5//
6//   iTop is free software; you can redistribute it and/or modify
7//   it under the terms of the GNU Affero General Public License as published by
8//   the Free Software Foundation, either version 3 of the License, or
9//   (at your option) any later version.
10//
11//   iTop is distributed in the hope that it will be useful,
12//   but WITHOUT ANY WARRANTY; without even the implied warranty of
13//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14//   GNU Affero General Public License for more details.
15//
16//   You should have received a copy of the GNU Affero General Public License
17//   along with iTop. If not, see <http://www.gnu.org/licenses/>
18
19
20/**
21 * Manage a runtime environment
22 *
23 * @copyright   Copyright (C) 2010-2018 Combodo SARL
24 * @license     http://opensource.org/licenses/AGPL-3.0
25 */
26
27require_once(APPROOT."setup/modulediscovery.class.inc.php");
28require_once(APPROOT.'setup/modelfactory.class.inc.php');
29require_once(APPROOT.'setup/compiler.class.inc.php');
30require_once(APPROOT.'setup/extensionsmap.class.inc.php');
31require_once(APPROOT.'core/metamodel.class.php');
32
33define ('MODULE_ACTION_OPTIONAL', 1);
34define ('MODULE_ACTION_MANDATORY', 2);
35define ('MODULE_ACTION_IMPOSSIBLE', 3);
36define ('ROOT_MODULE', '_Root_'); // Convention to store IN MEMORY the name/version of the root module i.e. application
37define ('DATAMODEL_MODULE', 'datamodel'); // Convention to store the version of the datamodel
38
39
40
41class RunTimeEnvironment
42{
43	/**
44	 * The name of the environment that the caller wants to build
45	 * @var string sFinalEnv
46	 */
47	protected $sFinalEnv;
48
49	/**
50	 * Environment into which the build will be performed
51	 * @var string sTargetEnv
52	 */
53	protected $sTargetEnv;
54
55	/**
56	 * Extensions map of the source environment
57	 * @var iTopExtensionsMap
58	 */
59	protected $oExtensionsMap;
60
61	/**
62	 * Toolset for building a run-time environment
63	 *
64	 * @param string $sEnvironment (e.g. 'test')
65	 * @param bool $bAutoCommit (make the target environment directly, or build a temporary one)
66	 */
67	public function __construct($sEnvironment = 'production', $bAutoCommit = true)
68	{
69		$this->sFinalEnv = $sEnvironment;
70		if ($bAutoCommit)
71		{
72			// Build directly onto the requested environment
73			$this->sTargetEnv = $sEnvironment;
74		}
75		else
76		{
77			// Build into a temporary target
78			$this->sTargetEnv = $sEnvironment.'-build';
79		}
80		$this->oExtensionsMap = null;
81	}
82
83	/**
84	 * Return the full path to the compiled code (do not use after commit)
85	 * @return string
86	 */
87	public function GetBuildDir()
88	{
89		return APPROOT.'env-'.$this->sTargetEnv;
90	}
91
92	/**
93	 * Callback function for logging the queries run by the setup.
94	 * According to the documentation the function must be defined before passing it to call_user_func...
95	 * @param string $sQuery
96	 * @param float $fDuration
97	 * @return void
98	 */
99	public function LogQueryCallback($sQuery, $fDuration)
100	{
101		$this->log_info(sprintf('%.3fs - query: %s ', $fDuration, $sQuery));
102		$this->log_db_query($sQuery);
103	}
104
105	/**
106	 * Helper function to initialize the ORM and load the data model
107	 * from the given file
108	 * @param $oConfig object The configuration (volatile, not necessarily already on disk)
109	 * @param $bModelOnly boolean Whether or not to allow loading a data model with no corresponding DB
110	 * @return none
111	 */
112	public function InitDataModel($oConfig, $bModelOnly = true, $bUseCache = false)
113	{
114		require_once(APPROOT.'/core/log.class.inc.php');
115		require_once(APPROOT.'/core/kpi.class.inc.php');
116		require_once(APPROOT.'/core/coreexception.class.inc.php');
117		require_once(APPROOT.'/core/dict.class.inc.php');
118		require_once(APPROOT.'/core/attributedef.class.inc.php');
119		require_once(APPROOT.'/core/filterdef.class.inc.php');
120		require_once(APPROOT.'/core/stimulus.class.inc.php');
121		require_once(APPROOT.'/core/MyHelpers.class.inc.php');
122		require_once(APPROOT.'/core/oql/expression.class.inc.php');
123		require_once(APPROOT.'/core/cmdbsource.class.inc.php');
124		require_once(APPROOT.'/core/sqlquery.class.inc.php');
125		require_once(APPROOT.'/core/sqlobjectquery.class.inc.php');
126		require_once(APPROOT.'/core/sqlunionquery.class.inc.php');
127		require_once(APPROOT.'/core/dbobject.class.php');
128		require_once(APPROOT.'/core/dbsearch.class.php');
129		require_once(APPROOT.'/core/dbobjectset.class.php');
130		require_once(APPROOT.'/application/cmdbabstract.class.inc.php');
131		require_once(APPROOT.'/core/userrights.class.inc.php');
132		require_once(APPROOT.'/setup/moduleinstallation.class.inc.php');
133
134		$sConfigFile = $oConfig->GetLoadedFile();
135		if (strlen($sConfigFile) > 0)
136		{
137			$this->log_info("MetaModel::Startup from $sConfigFile (ModelOnly = $bModelOnly)");
138		}
139		else
140		{
141			$this->log_info("MetaModel::Startup (ModelOnly = $bModelOnly)");
142		}
143
144		if (!$bUseCache)
145		{
146			// Reset the cache for the first use !
147			MetaModel::ResetCache(md5(APPROOT).'-'.$this->sTargetEnv);
148		}
149
150		MetaModel::Startup($oConfig, $bModelOnly, $bUseCache, false /* $bTraceSourceFiles */, $this->sTargetEnv);
151
152		if ($this->oExtensionsMap === null)
153		{
154			$this->oExtensionsMap = new iTopExtensionsMap($this->sTargetEnv);
155		}
156	}
157
158	/**
159	 * Analyzes the current installation and the possibilities
160	 *
161	 * @param Config $oConfig Defines the target environment (DB)
162	 * @param mixed $modulesPath Either a single string or an array of absolute paths
163	 * @param bool  $bAbortOnMissingDependency ...
164	 * @param hash $aModulesToLoad List of modules to search for, defaults to all if ommitted
165	 * @return hash Array with the following format:
166	 * array =>
167	 *     'iTop' => array(
168	 *         'version_db' => ... (could be empty in case of a fresh install)
169	 *         'version_code => ...
170	 *     )
171	 *     <module_name> => array(
172	 *         'version_db' => ...
173	 *         'version_code' => ...
174	 *         'install' => array(
175	 *             'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
176	 *             'message' => ...
177	 *         )
178	 *         'uninstall' => array(
179	 *             'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
180	 *             'message' => ...
181	 *         )
182	 *         'label' => ...
183	 *         'dependencies' => array(<module1>, <module2>, ...)
184	 *         'visible' => true | false
185	 *     )
186	 * )
187	 */
188	public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
189	{
190		$aRes = array(
191			ROOT_MODULE => array(
192				'version_db' => '',
193				'name_db' => '',
194				'version_code' => ITOP_VERSION.'.'.ITOP_REVISION,
195				'name_code' => ITOP_APPLICATION,
196			)
197		);
198
199		$aDirs = is_array($modulesPath) ? $modulesPath : array($modulesPath);
200		$aModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
201		foreach($aModules as $sModuleId => $aModuleInfo)
202		{
203			list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
204			if ($sModuleName == '')
205			{
206				throw new Exception("Missing name for the module: '$sModuleId'");
207			}
208			if ($sModuleVersion == '')
209			{
210				// The version must not be empty (it will be used as a criteria to determine wether a module has been installed or not)
211				//throw new Exception("Missing version for the module: '$sModuleId'");
212				$sModuleVersion  = '1.0.0';
213			}
214
215			$sModuleAppVersion = $aModuleInfo['itop_version'];
216			$aModuleInfo['version_db'] = '';
217			$aModuleInfo['version_code'] = $sModuleVersion;
218
219			if (!in_array($sModuleAppVersion, array('1.0.0', '1.0.1', '1.0.2')))
220			{
221				// This module is NOT compatible with the current version
222					$aModuleInfo['install'] = array(
223						'flag' => MODULE_ACTION_IMPOSSIBLE,
224						'message' => 'the module is not compatible with the current version of the application'
225					);
226			}
227			elseif ($aModuleInfo['mandatory'])
228			{
229				$aModuleInfo['install'] = array(
230					'flag' => MODULE_ACTION_MANDATORY,
231					'message' => 'the module is part of the application'
232				);
233			}
234			else
235			{
236				$aModuleInfo['install'] = array(
237					'flag' => MODULE_ACTION_OPTIONAL,
238					'message' => ''
239				);
240			}
241			$aRes[$sModuleName] = $aModuleInfo;
242		}
243
244		try
245		{
246			require_once(APPROOT.'/core/cmdbsource.class.inc.php');
247			CMDBSource::InitFromConfig($oConfig);
248			$aSelectInstall = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install");
249		}
250		catch (MySQLException $e)
251		{
252			// No database or erroneous information
253			$aSelectInstall = array();
254		}
255
256		// Build the list of installed module (get the latest installation)
257		//
258		$aInstallByModule = array(); // array of <module> => array ('installed' => timestamp, 'version' => <version>)
259		$iRootId = 0;
260		foreach ($aSelectInstall as $aInstall)
261		{
262			if (($aInstall['parent_id'] == 0) && ($aInstall['name'] != 'datamodel'))
263			{
264				// Root module, what is its ID ?
265				$iId = (int) $aInstall['id'];
266				if ($iId > $iRootId)
267				{
268					$iRootId = $iId;
269				}
270			}
271		}
272
273		foreach ($aSelectInstall as $aInstall)
274		{
275			//$aInstall['comment']; // unsused
276			$iInstalled = strtotime($aInstall['installed']);
277			$sModuleName = $aInstall['name'];
278			$sModuleVersion = $aInstall['version'];
279			if ($sModuleVersion == '')
280			{
281				// Though the version cannot be empty in iTop 2.0, it used to be possible
282				// therefore we have to put something here or the module will not be considered
283				// as being installed
284				$sModuleVersion = '0.0.0';
285			}
286
287			if ($aInstall['parent_id'] == 0)
288			{
289				$sModuleName = ROOT_MODULE;
290			}
291			else if($aInstall['parent_id'] != $iRootId)
292			{
293				// Skip all modules belonging to previous installations
294				continue;
295			}
296
297			if (array_key_exists($sModuleName, $aInstallByModule))
298			{
299				if ($iInstalled < $aInstallByModule[$sModuleName]['installed'])
300				{
301					continue;
302				}
303			}
304
305			if ($aInstall['parent_id'] == 0)
306			{
307				$aRes[$sModuleName]['version_db'] = $sModuleVersion;
308				$aRes[$sModuleName]['name_db'] = $aInstall['name'];
309			}
310
311			$aInstallByModule[$sModuleName]['installed'] = $iInstalled;
312			$aInstallByModule[$sModuleName]['version'] = $sModuleVersion;
313		}
314
315		// Adjust the list of proposed modules
316		//
317		foreach ($aInstallByModule as $sModuleName => $aModuleDB)
318		{
319			if ($sModuleName == ROOT_MODULE) continue; // Skip the main module
320
321			if (!array_key_exists($sModuleName, $aRes))
322			{
323				// A module was installed, it is not proposed in the new build... skip
324				continue;
325			}
326			$aRes[$sModuleName]['version_db'] = $aModuleDB['version'];
327
328			if ($aRes[$sModuleName]['install']['flag'] == MODULE_ACTION_MANDATORY)
329			{
330				$aRes[$sModuleName]['uninstall'] = array(
331					'flag' => MODULE_ACTION_IMPOSSIBLE,
332					'message' => 'the module is part of the application'
333				);
334			}
335			else
336			{
337				$aRes[$sModuleName]['uninstall'] = array(
338					'flag' => MODULE_ACTION_OPTIONAL,
339					'message' => ''
340				);
341			}
342		}
343
344		return $aRes;
345	}
346
347
348	public function WriteConfigFileSafe($oConfig)
349	{
350		self::MakeDirSafe(APPCONF);
351		self::MakeDirSafe(APPCONF.$this->sTargetEnv);
352
353		$sTargetConfigFile = APPCONF.$this->sTargetEnv.'/'.ITOP_CONFIG_FILE;
354
355		// Write the config file
356		@chmod($sTargetConfigFile, 0770); // In case it exists: RWX for owner and group, nothing for others
357		$oConfig->WriteToFile($sTargetConfigFile);
358		@chmod($sTargetConfigFile, 0440); // Read-only for owner and group, nothing for others
359	}
360
361	/**
362	 * Return an array with extra directories to scan for extensions/modules to install
363	 * @return string[]
364	 */
365	protected function GetExtraDirsToScan()
366	{
367		// Do nothing, overload this method if needed
368		return array();
369	}
370
371	/**
372	 * Decide whether or not the given extension is selected for installation
373	 * @param iTopExtension $oExtension
374	 * @return boolean
375	 */
376	protected function IsExtensionSelected(iTopExtension $oExtension)
377	{
378		return ($oExtension->sSource == iTopExtension::SOURCE_REMOTE);
379	}
380
381	/**
382	 * Get the installed modules (only the installed ones)
383	 */
384	protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir)
385	{
386		$sSourceDirFull = APPROOT.$sSourceDir;
387		if (!is_dir($sSourceDirFull))
388		{
389			throw new Exception("The source directory '$sSourceDirFull' does not exist (or could not be read)");
390		}
391		$aDirsToCompile = array($sSourceDirFull);
392		if (is_dir(APPROOT.'extensions'))
393		{
394			$aDirsToCompile[] = APPROOT.'extensions';
395		}
396		$sExtraDir = APPROOT.'data/'.$this->sTargetEnv.'-modules/';
397		if (is_dir($sExtraDir))
398		{
399			$aDirsToCompile[] = $sExtraDir;
400		}
401
402		$aExtraDirs = $this->GetExtraDirsToScan($aDirsToCompile);
403		$aDirsToCompile = array_merge($aDirsToCompile, $aExtraDirs);
404
405		$aRet = array();
406
407		// Determine the installed modules and extensions
408		//
409		$oSourceConfig = new Config(APPCONF.$sSourceEnv.'/'.ITOP_CONFIG_FILE);
410		$oSourceEnv = new RunTimeEnvironment($sSourceEnv);
411		$aAvailableModules = $oSourceEnv->AnalyzeInstallation($oSourceConfig, $aDirsToCompile);
412
413		// Actually read the modules available for the target environment,
414		// but get the selection from the source environment and finally
415		// mark as (automatically) chosen alll the "remote" modules present in the
416		// target environment (data/<target-env>-modules)
417		// The actual choices will be recorded by RecordInstallation below
418		$this->oExtensionsMap = new iTopExtensionsMap($this->sTargetEnv, true, $aExtraDirs);
419		$this->oExtensionsMap->LoadChoicesFromDatabase($oSourceConfig);
420		foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension)
421		{
422			if($this->IsExtensionSelected($oExtension))
423			{
424				$this->oExtensionsMap->MarkAsChosen($oExtension->sCode);
425			}
426		}
427
428		// Do load the required modules
429		//
430		$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
431		$aRet[$oDictModule->GetName()] = $oDictModule;
432
433		$oFactory = new ModelFactory($aDirsToCompile);
434		$sDeltaFile = APPROOT.'core/datamodel.core.xml';
435		if (file_exists($sDeltaFile))
436		{
437			$oCoreModule = new MFCoreModule('core', 'Core Module', $sDeltaFile);
438			$aRet[$oCoreModule->GetName()] = $oCoreModule;
439		}
440		$sDeltaFile = APPROOT.'application/datamodel.application.xml';
441		if (file_exists($sDeltaFile))
442		{
443			$oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile);
444			$aRet[$oApplicationModule->GetName()] = $oApplicationModule;
445		}
446
447		$aModules = $oFactory->FindModules();
448		foreach($aModules as $oModule)
449		{
450			$sModule = $oModule->GetName();
451			$sModuleRootDir = $oModule->GetRootDir();
452			$bIsExtra = $this->oExtensionsMap->ModuleIsChosenAsPartOfAnExtension($sModule, iTopExtension::SOURCE_REMOTE);
453			if (array_key_exists($sModule, $aAvailableModules))
454			{
455				if (($aAvailableModules[$sModule]['version_db'] != '') ||  $bIsExtra && !$oModule->IsAutoSelect()) //Extra modules are always unless they are 'AutoSelect'
456				{
457					$aRet[$oModule->GetName()] = $oModule;
458				}
459			}
460		}
461
462		// Now process the 'AutoSelect' modules
463		do
464		{
465			// Loop while new modules are added...
466			$bModuleAdded = false;
467			foreach($aModules as $oModule)
468			{
469				if (!array_key_exists($oModule->GetName(), $aRet) && $oModule->IsAutoSelect())
470				{
471					try
472					{
473						$bSelected = false;
474						SetupInfo::SetSelectedModules($aRet);
475						eval('$bSelected = ('.$oModule->GetAutoSelect().');');
476					}
477					catch(Exception $e)
478					{
479						$bSelected = false;
480					}
481					if ($bSelected)
482					{
483						$aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module
484						$bModuleAdded  = true;
485					}
486				}
487			}
488		}
489		while($bModuleAdded);
490
491		$sDeltaFile = APPROOT.'data/'.$this->sTargetEnv.'.delta.xml';
492		if (file_exists($sDeltaFile))
493		{
494			$oDelta = new MFDeltaModule($sDeltaFile);
495			$aRet[$oDelta->GetName()] = $oDelta;
496		}
497
498		return $aRet;
499	}
500
501	/**
502	 * Compile the data model by imitating the given environment
503	 * The list of modules to be installed in the target environment is:
504	 *  - the list of modules present in the "source_dir" (defined by the source environment) which are marked as "installed" in the source environment's database
505	 *  - plus the list of modules present in the "extra" directory of the target environment: data/<target_environment>-modules/
506	 * @param string $sSourceEnv The name of the source environment to 'imitate'
507	 * @param bool $bUseSymLinks Whether to create symbolic links instead of copies
508	 * @return string[]
509	 */
510	public function CompileFrom($sSourceEnv, $bUseSymLinks = false)
511	{
512		$oSourceConfig = new Config(utils::GetConfigFilePath($sSourceEnv));
513		$sSourceDir = $oSourceConfig->Get('source_dir');
514
515		$sSourceDirFull = APPROOT.$sSourceDir;
516		// Do load the required modules
517		//
518		$oFactory = new ModelFactory($sSourceDirFull);
519		$aModulesToCompile = $this->GetMFModulesToCompile($sSourceEnv, $sSourceDir);
520		foreach($aModulesToCompile as $oModule)
521		{
522			if ($oModule instanceof MFDeltaModule)
523			{
524				// Just before loading the delta, let's save an image of the datamodel
525				// in case there is no delta the operation will be done after the end of the loop
526				$oFactory->SaveToFile(APPROOT.'data/datamodel-'.$this->sTargetEnv.'.xml');
527			}
528			$oFactory->LoadModule($oModule);
529		}
530
531
532		if ($oModule instanceof MFDeltaModule)
533		{
534			// A delta was loaded, let's save a second copy of the datamodel
535			$oFactory->SaveToFile(APPROOT.'data/datamodel-'.$this->sTargetEnv.'-with-delta.xml');
536		}
537		else
538		{
539			// No delta was loaded, let's save the datamodel now
540			$oFactory->SaveToFile(APPROOT.'data/datamodel-'.$this->sTargetEnv.'.xml');
541		}
542
543		$sTargetDir = APPROOT.'env-'.$this->sTargetEnv;
544		self::MakeDirSafe($sTargetDir);
545		$bSkipTempDir = ($this->sFinalEnv != $this->sTargetEnv); // No need for a temporary directory if sTargetEnv is already a temporary directory
546		$oMFCompiler = new MFCompiler($oFactory);
547		$oMFCompiler->Compile($sTargetDir, null, $bUseSymLinks, $bSkipTempDir);
548
549		$sCacheDir = APPROOT.'data/cache-'.$this->sTargetEnv;
550		SetupUtils::builddir($sCacheDir);
551		SetupUtils::tidydir($sCacheDir);
552
553		MetaModel::ResetCache(md5(APPROOT).'-'.$this->sTargetEnv);
554
555		return array_keys($aModulesToCompile);
556	}
557
558	/**
559	 * Helper function to create the database structure
560	 * @return boolean true on success, false otherwise
561	 */
562	public function CreateDatabaseStructure(Config $oConfig, $sMode)
563	{
564		if (strlen($oConfig->Get('db_subname')) > 0)
565		{
566			$this->log_info("Creating the structure in '".$oConfig->Get('db_name')."' (table names prefixed by '".$oConfig->Get('db_subname')."').");
567		}
568		else
569		{
570			$this->log_info("Creating the structure in '".$oConfig->Get('db_subname')."'.");
571		}
572
573		//MetaModel::CheckDefinitions();
574		if ($sMode == 'install')
575		{
576			if (!MetaModel::DBExists(/* bMustBeComplete */ false))
577			{
578				MetaModel::DBCreate(array($this, 'LogQueryCallback'));
579				$this->log_ok("Database structure successfully created.");
580			}
581			else
582			{
583				if (strlen($oConfig->Get('db_subname')) > 0)
584				{
585					throw new Exception("Error: found iTop tables into the database '".$oConfig->Get('db_name')."' (prefix: '".$oConfig->Get('db_subname')."'). Please, try selecting another database instance or specify another prefix to prevent conflicting table names.");
586				}
587				else
588				{
589					throw new Exception("Error: found iTop tables into the database '".$oConfig->Get('db_name')."'. Please, try selecting another database instance or specify a prefix to prevent conflicting table names.");
590				}
591			}
592		}
593		else
594		{
595			if (MetaModel::DBExists(/* bMustBeComplete */ false))
596			{
597				// Have it work fine even if the DB has been set in read-only mode for the users
598				// (fix copied from RunTimeEnvironment::RecordInstallation)
599				$iPrevAccessMode = $oConfig->Get('access_mode');
600				$oConfig->Set('access_mode', ACCESS_FULL);
601
602				MetaModel::DBCreate(array($this, 'LogQueryCallback'));
603				$this->log_ok("Database structure successfully updated.");
604
605				// Check (and update only if it seems needed) the hierarchical keys
606				ob_start();
607				MetaModel::CheckHKeys(false /* bDiagnosticsOnly */, true /* bVerbose*/, true /* bForceUpdate */); // Since in 1.2-beta the detection was buggy, let's force the rebuilding of HKeys
608				$sFeedback = ob_get_clean();
609				$this->log_ok("Hierchical keys rebuilt: $sFeedback");
610
611				// Check (and fix) data sync configuration
612				ob_start();
613				MetaModel::CheckDataSources(false /*$bDiagnostics*/, true/*$bVerbose*/);
614				$sFeedback = ob_get_clean();
615				$this->log_ok("Data sources checked: $sFeedback");
616
617				// Fix meta enums
618				ob_start();
619				MetaModel::RebuildMetaEnums(true /*bVerbose*/);
620				$sFeedback = ob_get_clean();
621				$this->log_ok("Meta enums rebuilt: $sFeedback");
622
623				// Restore the previous access mode
624				$oConfig->Set('access_mode', $iPrevAccessMode);
625			}
626			else
627			{
628				if (strlen($oConfig->Get('db_subname')) > 0)
629				{
630					throw new Exception("Error: No previous instance of iTop found into the database '".$oConfig->Get('db_name')."' (prefix: '".$oConfig->Get('db_subname')."'). Please, try selecting another database instance.");
631				}
632				else
633				{
634					throw new Exception("Error: No previous instance of iTop found into the database '".$oConfig->Get('db_name')."'. Please, try selecting another database instance.");
635				}
636			}
637		}
638		return true;
639	}
640
641	public function UpdatePredefinedObjects()
642	{
643		// Have it work fine even if the DB has been set in read-only mode for the users
644		$oConfig = MetaModel::GetConfig();
645		$iPrevAccessMode = $oConfig->Get('access_mode');
646		$oConfig->Set('access_mode', ACCESS_FULL);
647
648		// Constant classes (e.g. User profiles)
649		//
650		foreach (MetaModel::GetClasses() as $sClass)
651		{
652			$aPredefinedObjects = call_user_func(array(
653				$sClass,
654				'GetPredefinedObjects'
655			));
656			if ($aPredefinedObjects != null)
657			{
658				$this->log_info("$sClass::GetPredefinedObjects() returned " . count($aPredefinedObjects) . " elements.");
659
660				// Create/Delete/Update objects of this class,
661				// according to the given constant values
662				//
663				$aDBIds = array();
664				$oAll = new DBObjectSet(new DBObjectSearch($sClass));
665				while ($oObj = $oAll->Fetch())
666				{
667					if (array_key_exists($oObj->GetKey(), $aPredefinedObjects))
668					{
669						$aObjValues = $aPredefinedObjects[$oObj->GetKey()];
670						foreach ($aObjValues as $sAttCode => $value)
671						{
672							$oObj->Set($sAttCode, $value);
673						}
674						$oObj->DBUpdate();
675						$aDBIds[$oObj->GetKey()] = true;
676					}
677					else
678					{
679						$oObj->DBDelete();
680					}
681				}
682				foreach ($aPredefinedObjects as $iRefId => $aObjValues)
683				{
684					if (! array_key_exists($iRefId, $aDBIds))
685					{
686						$oNewObj = MetaModel::NewObject($sClass);
687						$oNewObj->SetKey($iRefId);
688						foreach ($aObjValues as $sAttCode => $value)
689						{
690							$oNewObj->Set($sAttCode, $value);
691						}
692						$oNewObj->DBInsert();
693					}
694				}
695			}
696		}
697
698		// Restore the previous access mode
699		$oConfig->Set('access_mode', $iPrevAccessMode);
700	}
701
702	public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelectedModuleCodes, $aSelectedExtensionCodes, $sShortComment = null)
703	{
704		// Have it work fine even if the DB has been set in read-only mode for the users
705		$iPrevAccessMode = $oConfig->Get('access_mode');
706		$oConfig->Set('access_mode', ACCESS_FULL);
707
708		if (CMDBSource::DBName() == '')
709		{
710			// In case this has not yet been done
711			CMDBSource::InitFromConfig($oConfig);
712		}
713
714		if ($sShortComment === null)
715		{
716			$sShortComment = 'Done by the setup program';
717		}
718		$sMainComment = $sShortComment."\nBuilt on ".ITOP_BUILD_DATE;
719
720		// Record datamodel version
721		$aData = array(
722			'source_dir' => $oConfig->Get('source_dir'),
723		);
724		$iInstallationTime = time(); // Make sure that all modules record the same installation time
725		$oInstallRec = new ModuleInstallation();
726		$oInstallRec->Set('name', DATAMODEL_MODULE);
727		$oInstallRec->Set('version', $sDataModelVersion);
728		$oInstallRec->Set('comment', json_encode($aData));
729		$oInstallRec->Set('parent_id', 0); // root module
730		$oInstallRec->Set('installed', $iInstallationTime);
731		$iMainItopRecord = $oInstallRec->DBInsertNoReload();
732
733		// Record main installation
734		$oInstallRec = new ModuleInstallation();
735		$oInstallRec->Set('name', ITOP_APPLICATION);
736		$oInstallRec->Set('version', ITOP_VERSION.'.'.ITOP_REVISION);
737		$oInstallRec->Set('comment', $sMainComment);
738		$oInstallRec->Set('parent_id', 0); // root module
739		$oInstallRec->Set('installed', $iInstallationTime);
740		$iMainItopRecord = $oInstallRec->DBInsertNoReload();
741
742
743		// Record installed modules and extensions
744		//
745		$aAvailableExtensions = array();
746		$aAvailableModules = $this->AnalyzeInstallation($oConfig, $this->GetBuildDir());
747		foreach($aSelectedModuleCodes as $sModuleId)
748		{
749			$aModuleData = $aAvailableModules[$sModuleId];
750			$sName = $sModuleId;
751			$sVersion = $aModuleData['version_code'];
752			$aComments = array();
753			$aComments[] = $sShortComment;
754			if ($aModuleData['mandatory'])
755			{
756				$aComments[] = 'Mandatory';
757			}
758			else
759			{
760				$aComments[] = 'Optional';
761			}
762			if ($aModuleData['visible'])
763			{
764				$aComments[] = 'Visible (during the setup)';
765			}
766			else
767			{
768				$aComments[] = 'Hidden (selected automatically)';
769			}
770			foreach ($aModuleData['dependencies'] as $sDependOn)
771			{
772				$aComments[] = "Depends on module: $sDependOn";
773			}
774			$sComment = implode("\n", $aComments);
775
776			$oInstallRec = new ModuleInstallation();
777			$oInstallRec->Set('name', $sName);
778			$oInstallRec->Set('version', $sVersion);
779			$oInstallRec->Set('comment', $sComment);
780			$oInstallRec->Set('parent_id', $iMainItopRecord);
781			$oInstallRec->Set('installed', $iInstallationTime);
782			$oInstallRec->DBInsertNoReload();
783		}
784
785		if ($this->oExtensionsMap)
786		{
787			// Mark as chosen the selected extensions code passed to us
788			// Note: some other extensions may already be marked as chosen
789			foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension)
790			{
791				if (in_array($oExtension->sCode, $aSelectedExtensionCodes))
792				{
793					$this->oExtensionsMap->MarkAsChosen($oExtension->sCode);
794				}
795			}
796
797			foreach($this->oExtensionsMap->GetChoices() as $oExtension)
798			{
799				$oInstallRec = new ExtensionInstallation();
800				$oInstallRec->Set('code', $oExtension->sCode);
801				$oInstallRec->Set('label', $oExtension->sLabel);
802				$oInstallRec->Set('version', $oExtension->sVersion);
803				$oInstallRec->Set('source',  $oExtension->sSource);
804				$oInstallRec->Set('installed', $iInstallationTime);
805				$oInstallRec->DBInsertNoReload();
806			}
807		}
808
809		// Restore the previous access mode
810		$oConfig->Set('access_mode', $iPrevAccessMode);
811
812		// Database is created, installation has been tracked into it
813		return true;
814	}
815
816	public function GetApplicationVersion(Config $oConfig)
817	{
818		$aResult = false;
819		try
820		{
821			require_once(APPROOT.'/core/cmdbsource.class.inc.php');
822			CMDBSource::InitFromConfig($oConfig);
823			$sSQLQuery = "SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install";
824			$aSelectInstall = CMDBSource::QueryToArray($sSQLQuery);
825		}
826		catch (MySQLException $e)
827		{
828			// No database or erroneous information
829			$this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name'));
830			$this->log_error('Exception '.$e->getMessage());
831			return false;
832		}
833
834		// Scan the list of installed modules to get the version of the 'ROOT' module which holds the main application version
835		foreach ($aSelectInstall as $aInstall)
836		{
837			$sModuleVersion = $aInstall['version'];
838			if ($sModuleVersion == '')
839			{
840				// Though the version cannot be empty in iTop 2.0, it used to be possible
841				// therefore we have to put something here or the module will not be considered
842				// as being installed
843				$sModuleVersion = '0.0.0';
844			}
845
846			if ($aInstall['parent_id'] == 0)
847			{
848				if ($aInstall['name'] == DATAMODEL_MODULE)
849				{
850					$aResult['datamodel_version'] = $sModuleVersion;
851					$aComments = json_decode($aInstall['comment'], true);
852					if (is_array($aComments))
853					{
854						$aResult = array_merge($aResult, $aComments);
855					}
856				}
857				else
858				{
859					$aResult['product_name'] = $aInstall['name'];
860					$aResult['product_version'] = $sModuleVersion;
861				}
862			}
863		}
864		if (!array_key_exists('datamodel_version', $aResult))
865		{
866			// Versions prior to 2.0 did not record the version of the datamodel
867			// so assume that the datamodel version is equal to the application version
868			$aResult['datamodel_version'] = $aResult['product_version'];
869		}
870		$this->log_info("GetApplicationVersion returns: product_name: ".$aResult['product_name'].', product_version: '.$aResult['product_version']);
871		return $aResult;
872	}
873
874	public static function MakeDirSafe($sDir)
875	{
876		if (!is_dir($sDir))
877		{
878			if (!@mkdir($sDir))
879			{
880				throw new Exception("Failed to create directory '$sDir', please check that the web server process has enough rights to create the directory.");
881			}
882			@chmod($sDir, 0770); // RWX for owner and group, nothing for others
883		}
884	}
885
886	/**
887	 * Wrappers for logging into the setup log files
888	 */
889	protected function log_error($sText)
890	{
891		SetupPage::log_error($sText);
892	}
893	protected function log_warning($sText)
894	{
895		SetupPage::log_warning($sText);
896	}
897	protected function log_info($sText)
898	{
899		SetupPage::log_info($sText);
900	}
901	protected function log_ok($sText)
902	{
903		SetupPage::log_ok($sText);
904	}
905
906	/**
907	 * Writes queries run by the setup in a SQL file
908	 *
909	 * @param string $sQuery
910	 *
911	 * @since 2.5 N°1001 utf8mb4 switch
912	 * @uses \SetupUtils::GetSetupQueriesFilePath()
913	 */
914	protected function log_db_query($sQuery)
915	{
916		$sSetupQueriesFilePath = SetupUtils::GetSetupQueriesFilePath();
917		$hSetupQueriesFile = @fopen($sSetupQueriesFilePath, 'a');
918		if ($hSetupQueriesFile !== false)
919		{
920			fwrite($hSetupQueriesFile, "$sQuery\n");
921			fclose($hSetupQueriesFile);
922		}
923	}
924
925	public function GetCurrentDataModelVersion()
926	{
927		$oSearch = DBObjectSearch::FromOQL("SELECT ModuleInstallation WHERE name='".DATAMODEL_MODULE."'");
928		$oSet = new DBObjectSet($oSearch, array('installed' => false));
929		$oLatestDM = $oSet->Fetch();
930		if ($oLatestDM == null)
931		{
932			return '0.0.0';
933		}
934		return $oLatestDM->Get('version');
935	}
936
937	public function Commit()
938	{
939		if ($this->sFinalEnv != $this->sTargetEnv)
940		{
941			if (file_exists(APPROOT.'data/'.$this->sTargetEnv.'.delta.xml'))
942			{
943				if (file_exists(APPROOT.'data/'.$this->sFinalEnv.'.delta.xml'))
944				{
945					// Make a "previous" file
946					copy(
947						APPROOT.'data/'.$this->sTargetEnv.'.delta.xml',
948						APPROOT.'data/'.$this->sFinalEnv.'.delta.prev.xml'
949					);
950				}
951				$this->CommitFile(
952					APPROOT.'data/'.$this->sTargetEnv.'.delta.xml',
953					APPROOT.'data/'.$this->sFinalEnv.'.delta.xml'
954				);
955			}
956			$this->CommitFile(
957				APPROOT.'data/datamodel-'.$this->sTargetEnv.'.xml',
958				APPROOT.'data/datamodel-'.$this->sFinalEnv.'.xml'
959			);
960			$this->CommitFile(
961				APPROOT.'data/datamodel-'.$this->sTargetEnv.'-with-delta.xml',
962				APPROOT.'data/datamodel-'.$this->sFinalEnv.'-with-delta.xml',
963				false
964			);
965			$this->CommitDir(
966				APPROOT.'data/'.$this->sTargetEnv.'-modules/',
967				APPROOT.'data/'.$this->sFinalEnv.'-modules/',
968				false
969			);
970			$this->CommitDir(
971				APPROOT.'data/cache-'.$this->sTargetEnv,
972				APPROOT.'data/cache-'.$this->sFinalEnv,
973				false
974			);
975			$this->CommitDir(
976				APPROOT.'env-'.$this->sTargetEnv,
977				APPROOT.'env-'.$this->sFinalEnv,
978                true,
979                false
980			);
981
982			// Move the config file
983			//
984			$sTargetConfig = APPCONF.$this->sTargetEnv.'/config-itop.php';
985			$sFinalConfig = APPCONF.$this->sFinalEnv.'/config-itop.php';
986			@chmod($sFinalConfig, 0770); // In case it exists: RWX for owner and group, nothing for others
987			$this->CommitFile($sTargetConfig, $sFinalConfig);
988			@chmod($sFinalConfig, 0440); // Read-only for owner and group, nothing for others
989			@rmdir(dirname($sTargetConfig)); // Cleanup the temporary build dir if empty
990
991			MetaModel::ResetCache(md5(APPROOT).'-'.$this->sFinalEnv);
992		}
993	}
994
995	/**
996	 * Overwrite or create the destination file
997	 *
998	 * @param $sSource
999	 * @param $sDest
1000	 * @param bool $bSourceMustExist
1001	 * @throws Exception
1002	 */
1003	protected function CommitFile($sSource, $sDest, $bSourceMustExist = true)
1004	{
1005		if (file_exists($sSource))
1006		{
1007			SetupUtils::builddir(dirname($sDest));
1008			if (file_exists($sDest))
1009			{
1010				$bRes = @unlink($sDest);
1011				if (!$bRes)
1012				{
1013					throw new Exception('Commit - Failed to cleanup destination file: '.$sDest);
1014				}
1015			}
1016			rename($sSource, $sDest);
1017		}
1018		else
1019		{
1020			// The file does not exist
1021			if ($bSourceMustExist)
1022			{
1023				throw new Exception('Commit - Missing file: '.$sSource);
1024			}
1025			else
1026			{
1027				// Align the destination with the source... make sure there is NO file
1028				if (file_exists($sDest))
1029				{
1030					$bRes = @unlink($sDest);
1031					if (!$bRes)
1032					{
1033						throw new Exception('Commit - Failed to cleanup destination file: '.$sDest);
1034					}
1035				}
1036			}
1037		}
1038	}
1039
1040	/**
1041	 * Overwrite or create the destination directory
1042	 *
1043	 * @param $sSource
1044	 * @param $sDest
1045	 * @param boolean $bSourceMustExist
1046     * @param boolean $bRemoveSource If true $sSource will be removed, otherwise $sSource will just be emptied
1047	 * @throws Exception
1048	 */
1049	protected function CommitDir($sSource, $sDest, $bSourceMustExist = true, $bRemoveSource = true)
1050	{
1051		if (file_exists($sSource))
1052		{
1053			SetupUtils::movedir($sSource, $sDest, $bRemoveSource);
1054		}
1055		else
1056		{
1057			// The file does not exist
1058			if ($bSourceMustExist)
1059			{
1060				throw new Exception('Commit - Missing directory: '.$sSource);
1061			}
1062			else
1063			{
1064				// Align the destination with the source... make sure there is NO file
1065				if (file_exists($sDest))
1066				{
1067					SetupUtils::rrmdir($sDest);
1068				}
1069			}
1070		}
1071	}
1072
1073	/**
1074	 * Call the given handler method for all selected modules having an installation handler
1075	 * @param array[] $aAvailableModules
1076	 * @param string[] $aSelectedModules
1077	 * @param string $sHandlerName
1078	 */
1079	public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sHandlerName)
1080	{
1081	    foreach($aAvailableModules as $sModuleId => $aModule)
1082	    {
1083	        if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules) &&
1084	            isset($aAvailableModules[$sModuleId]['installer']) )
1085	        {
1086	            $sModuleInstallerClass = $aAvailableModules[$sModuleId]['installer'];
1087	            SetupPage::log_info("Calling Module Handler: $sModuleInstallerClass::$sHandlerName(oConfig, {$aModule['version_db']}, {$aModule['version_code']})");
1088	            $aCallSpec = array($sModuleInstallerClass, $sHandlerName);
1089	            if (is_callable($aCallSpec))
1090	            {
1091	               call_user_func_array($aCallSpec, array(MetaModel::GetConfig(), $aModule['version_db'], $aModule['version_code']));
1092	            }
1093	        }
1094	    }
1095	}
1096
1097	/**
1098	 * Load data from XML files for the selected modules (structural data and/or sample data)
1099	 * @param array[] $aAvailableModules All available modules and their definition
1100	 * @param string[] $aSelectedModules List of selected modules
1101	 * @param bool $bSampleData Wether or not to load sample data
1102	 */
1103	public function LoadData($aAvailableModules, $aSelectedModules, $bSampleData)
1104	{
1105	    $oDataLoader = new XMLDataLoader();
1106
1107	    CMDBObject::SetTrackInfo("Initialization");
1108	    $oMyChange = CMDBObject::GetCurrentChange();
1109
1110	    SetupPage::log_info("starting data load session");
1111	    $oDataLoader->StartSession($oMyChange);
1112
1113	    $aFiles = array();
1114	    $aPreviouslyLoadedFiles = array();
1115	    foreach($aAvailableModules as $sModuleId => $aModule)
1116	    {
1117	        if (($sModuleId != ROOT_MODULE))
1118	        {
1119	            $sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']);
1120	            // Load data only for selected AND newly installed modules
1121	            if (in_array($sModuleId, $aSelectedModules))
1122	            {
1123	                if ($aModule['version_db'] != '')
1124	                {
1125	                    // Simulate the load of the previously loaded XML files to get the mapping of the keys
1126	                    if ($bSampleData)
1127	                    {
1128	                        $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
1129	                        $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
1130	                    }
1131	                    else
1132	                    {
1133	                        // Load only structural data
1134	                        $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
1135	                    }
1136	                }
1137	                else
1138	                {
1139	                    if ($bSampleData)
1140	                    {
1141	                        $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
1142	                        $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
1143	                    }
1144	                    else
1145	                    {
1146	                        // Load only structural data
1147	                        $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
1148	                    }
1149	                }
1150	            }
1151	        }
1152	    }
1153
1154	    // Simulate the load of the previously loaded files, in order to initialize
1155	    // the mapping between the identifiers in the XML and the actual identifiers
1156	    // in the current database
1157	    foreach($aPreviouslyLoadedFiles as $sFileRelativePath)
1158	    {
1159	        $sFileName = APPROOT.$sFileRelativePath;
1160	        SetupPage::log_info("Loading file: $sFileName (just to get the keys mapping)");
1161	        if (empty($sFileName) || !file_exists($sFileName))
1162	        {
1163	            throw(new Exception("File $sFileName does not exist"));
1164	        }
1165
1166	        $oDataLoader->LoadFile($sFileName, true);
1167	        $sResult = sprintf("loading of %s done.", basename($sFileName));
1168	        SetupPage::log_info($sResult);
1169	    }
1170
1171	    foreach($aFiles as $sFileRelativePath)
1172	    {
1173	        $sFileName = APPROOT.$sFileRelativePath;
1174	        SetupPage::log_info("Loading file: $sFileName");
1175	        if (empty($sFileName) || !file_exists($sFileName))
1176	        {
1177	            throw(new Exception("File $sFileName does not exist"));
1178	        }
1179
1180	        $oDataLoader->LoadFile($sFileName);
1181	        $sResult = sprintf("loading of %s done.", basename($sFileName));
1182	        SetupPage::log_info($sResult);
1183	    }
1184
1185	    $oDataLoader->EndSession();
1186	    SetupPage::log_info("ending data load session");
1187	}
1188
1189	/**
1190	 * Merge two arrays of file names, adding the relative path to the files provided in the array to merge
1191	 * @param string[] $aSourceArray
1192	 * @param string $sBaseDir
1193	 * @param string[] $aFilesToMerge
1194	 * @return string[]
1195	 */
1196	protected static function MergeWithRelativeDir($aSourceArray, $sBaseDir, $aFilesToMerge)
1197	{
1198	    $aToMerge = array();
1199	    foreach($aFilesToMerge as $sFile)
1200	    {
1201	        $aToMerge[] = $sBaseDir.'/'.$sFile;
1202	    }
1203	    return array_merge($aSourceArray, $aToMerge);
1204	}
1205
1206	/**
1207	 * Check the MetaModel for some common pitfall (class name too long, classes requiring too many joins...)
1208	 * The check takes about 900 ms for 200 classes
1209	 * @throws Exception
1210	 * @return string
1211	 */
1212    public function CheckMetaModel()
1213    {
1214        $iCount = 0;
1215        $fStart = microtime(true);
1216        foreach(MetaModel::GetClasses() as $sClass)
1217        {
1218            if (false == MetaModel::HasTable($sClass) && MetaModel::IsAbstract($sClass))
1219            {
1220                //if a class is not persisted and is abstract, the code below would crash. Needed by the class AbstractRessource. This is tolerable to skip this because we check the setup process integrity, not the datamodel integrity.
1221                continue;
1222            }
1223
1224            $oSearch = new DBObjectSearch($sClass);
1225            $oSearch->SetShowObsoleteData(false);
1226            $oSQLQuery = $oSearch->GetSQLQueryStructure(null, false);
1227            $sViewName = MetaModel::DBGetView($sClass);
1228            if (strlen($sViewName) > 64)
1229            {
1230                throw new Exception("Class name too long for class: '$sClass'. The name of the corresponding view ($sViewName) would exceed MySQL's limit for the name of a table (64 characters).");
1231            }
1232            $sTableName = MetaModel::DBGetTable($sClass);
1233            if (strlen($sTableName) > 64)
1234            {
1235                throw new Exception("Table name too long for class: '$sClass'. The name of the corresponding MySQL table ($sTableName) would exceed MySQL's limit for the name of a table (64 characters).");
1236            }
1237            $iTableCount = $oSQLQuery->CountTables();
1238            if ($iTableCount > 61)
1239            {
1240                throw new Exception("Class requiring too many tables: '$sClass'. The structure of the class ($sClass) would require a query with more than 61 JOINS (MySQL's limitation).");
1241            }
1242            $iCount++;
1243        }
1244        $fDuration = microtime(true) - $fStart;
1245
1246        return sprintf("Checked %d classes in %.1f ms. No error found.\n", $iCount, $fDuration*1000.0);
1247    }
1248} // End of class
1249