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