diff --git a/Jenkinsfile b/Jenkinsfile index ca7548d2a5..7015aa8412 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -11,7 +11,7 @@ node(){ checkout scm - infra = load '/var/lib/jenkins/workspace/itop-test-infra_master/src/Infra.groovy' + infra = load '/var/lib/jenkins/workspace/itop-test-infra_6644-phpstan/src/Infra.groovy' } diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index 8a57472f58..b298a085b4 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -3,7 +3,7 @@ // // This file is part of iTop. // -// iTop is free software; you can redistribute it and/or modify +// iTop is free software; you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. @@ -25,7 +25,7 @@ * The installation process is split into a sequence of unitary steps * for performance reasons (i.e; timeout, memory usage) and also in order * to provide some feedback about the progress of the installation. - * + * * This class can be used for a step by step interactive installation * while displaying a progress bar, or in an unattended manner * (for example from the command line), to run all the steps @@ -157,7 +157,7 @@ public function ExecuteAllSteps($bVerbose = true, &$sMessage = null, $sInstallCo } } while(($aRes['status'] != self::ERROR) && ($aRes['next-step'] != '')); - + return ($iOverallStatus == self::OK); } @@ -225,7 +225,7 @@ public function ExecuteStep($sStep = '', $sInstallComment = null) case 'copy': $aPreinstall = $this->oParams->Get('preinstall'); - $aCopies = $aPreinstall['copies']; + $aCopies = $aPreinstall['copies'] ?? []; self::DoCopy($aCopies); $sReport = "Copying..."; @@ -472,7 +472,7 @@ protected static function DoCopy($aCopies) { $sSource = $aCopy['source']; $sDestination = APPROOT.$aCopy['destination']; - + SetupUtils::builddir($sDestination); SetupUtils::tidydir($sDestination); SetupUtils::copydir($sSource, $sDestination); @@ -513,7 +513,7 @@ protected static function DoBackup($oConfig, $sBackupFileFormat, $sSourceConfigF $oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile); } - + protected static function DoCompile($aSelectedModules, $sSourceDir, $sExtensionDir, $sTargetDir, $sEnvironment, $bUseSymbolicLinks = false) { SetupPage::log_info("Compiling data model."); @@ -525,7 +525,7 @@ protected static function DoCompile($aSelectedModules, $sSourceDir, $sExtensionD if (empty($sSourceDir) || empty($sTargetDir)) { throw new Exception("missing parameter source_dir and/or target_dir"); - } + } $sSourcePath = APPROOT.$sSourceDir; $aDirsToScan = array($sSourcePath); @@ -582,10 +582,10 @@ protected static function DoCompile($aSelectedModules, $sSourceDir, $sExtensionD } $oFactory = new ModelFactory($aDirsToScan); - + $oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries'); $oFactory->LoadModule($oDictModule); - + $sDeltaFile = APPROOT.'core/datamodel.core.xml'; if (file_exists($sDeltaFile)) { @@ -598,7 +598,7 @@ protected static function DoCompile($aSelectedModules, $sSourceDir, $sExtensionD $oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile); $oFactory->LoadModule($oApplicationModule); } - + $aModules = $oFactory->FindModules(); foreach($aModules as $oModule) @@ -611,7 +611,7 @@ protected static function DoCompile($aSelectedModules, $sSourceDir, $sExtensionD } // Dump the "reference" model, just before loading any actual delta $oFactory->SaveToFile(APPROOT.'data/datamodel-'.$sEnvironment.'.xml'); - + $sDeltaFile = APPROOT.'data/'.$sEnvironment.'.delta.xml'; if (file_exists($sDeltaFile)) { @@ -635,12 +635,12 @@ protected static function DoCompile($aSelectedModules, $sSourceDir, $sExtensionD if (file_exists($sFileToPatch)) { $sContent = file_get_contents($sFileToPatch); - + $sContent = str_replace("require_once(APPROOT.'modules/itop-welcome-itil/model.itop-welcome-itil.php');", "//\n// The line below is no longer needed in iTop 2.0 -- patched by the setup program\n// require_once(APPROOT.'modules/itop-welcome-itil/model.itop-welcome-itil.php');", $sContent); - + file_put_contents($sFileToPatch, $sContent); } - + // Set an "Instance UUID" identifying this machine based on a file located in the data directory $sInstanceUUIDFile = APPROOT.'data/instance.txt'; SetupUtils::builddir(APPROOT.'data'); @@ -699,7 +699,7 @@ protected static function DoUpdateDBSchema($aSelectedModules, $sModulesDir, $aPa // Starting 2.0, all table names must be lowercase if ($sMode != 'install') { - SetupPage::log_info("Renaming '{$sDBPrefix}priv_internalUser' into '{$sDBPrefix}priv_internaluser' (lowercase)"); + SetupPage::log_info("Renaming '{$sDBPrefix}priv_internalUser' into '{$sDBPrefix}priv_internaluser' (lowercase)"); // This command will have no effect under Windows... // and it has been written in two steps so as to make it work under windows! CMDBSource::SelectDB($sDBName); @@ -710,18 +710,18 @@ protected static function DoUpdateDBSchema($aSelectedModules, $sModulesDir, $aPa } catch (Exception $e) { - SetupPage::log_info("Renaming '{$sDBPrefix}priv_internalUser' failed (already done in a previous upgrade?)"); + SetupPage::log_info("Renaming '{$sDBPrefix}priv_internalUser' failed (already done in a previous upgrade?)"); } - + // let's remove the records in priv_change which have no counterpart in priv_changeop - SetupPage::log_info("Cleanup of '{$sDBPrefix}priv_change' to remove orphan records"); + SetupPage::log_info("Cleanup of '{$sDBPrefix}priv_change' to remove orphan records"); CMDBSource::SelectDB($sDBName); try { $sTotalCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_change`"; $iTotalCount = (int)CMDBSource::QueryToScalar($sTotalCount); SetupPage::log_info("There is a total of $iTotalCount records in {$sDBPrefix}priv_change."); - + $sOrphanCount = "SELECT COUNT(c.id) FROM `{$sDBPrefix}priv_change` AS c left join `{$sDBPrefix}priv_changeop` AS o ON c.id = o.changeid WHERE o.id IS NULL"; $iOrphanCount = (int)CMDBSource::QueryToScalar($sOrphanCount); SetupPage::log_info("There are $iOrphanCount useless records in {$sDBPrefix}priv_change (".sprintf('%.2f', ((100.0*$iOrphanCount)/$iTotalCount))."%)"); @@ -745,11 +745,11 @@ protected static function DoUpdateDBSchema($aSelectedModules, $sModulesDir, $aPa } catch (Exception $e) { - SetupPage::log_info("Cleanup of orphan records in `{$sDBPrefix}priv_change` failed: ".$e->getMessage()); + SetupPage::log_info("Cleanup of orphan records in `{$sDBPrefix}priv_change` failed: ".$e->getMessage()); } - + } - + // Module specific actions (migrate the data) // $aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir); @@ -757,9 +757,9 @@ protected static function DoUpdateDBSchema($aSelectedModules, $sModulesDir, $aPa if(!$oProductionEnv->CreateDatabaseStructure(MetaModel::GetConfig(), $sMode)) { - throw new Exception("Failed to create/upgrade the database structure for environment '$sTargetEnvironment'"); + throw new Exception("Failed to create/upgrade the database structure for environment '$sTargetEnvironment'"); } - + // Set a DBProperty with a unique ID to identify this instance of iTop $sUUID = DBProperty::GetProperty('database_uuid', ''); if ($sUUID === '') @@ -767,10 +767,10 @@ protected static function DoUpdateDBSchema($aSelectedModules, $sModulesDir, $aPa $sUUID = utils::CreateUUID('database'); DBProperty::SetProperty('database_uuid', $sUUID, 'Installation/upgrade of '.ITOP_APPLICATION, 'Unique ID of this '.ITOP_APPLICATION.' Database'); } - + // priv_change now has an 'origin' field to distinguish between the various input sources // Let's initialize the field with 'interactive' for all records were it's null - // Then check if some records should hold a different value, based on a pattern matching in the userinfo field + // Then check if some records should hold a different value, based on a pattern matching in the userinfo field CMDBSource::SelectDB($sDBName); try { @@ -778,21 +778,21 @@ protected static function DoUpdateDBSchema($aSelectedModules, $sModulesDir, $aPa $iCount = (int)CMDBSource::QueryToScalar($sCount); if ($iCount > 0) { - SetupPage::log_info("Initializing '{$sDBPrefix}priv_change.origin' ($iCount records to update)"); - + SetupPage::log_info("Initializing '{$sDBPrefix}priv_change.origin' ($iCount records to update)"); + // By default all uninitialized values are considered as 'interactive' $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'interactive' WHERE `origin` IS NULL"; CMDBSource::Query($sInit); - + // CSV Import was identified by the comment at the end $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'csv-import.php' WHERE `userinfo` LIKE '%Web Service (CSV)'"; CMDBSource::Query($sInit); - + // CSV Import was identified by the comment at the end $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'csv-interactive' WHERE `userinfo` LIKE '%(CSV)' AND origin = 'interactive'"; CMDBSource::Query($sInit); - - + + // Syncho data sources were identified by the comment at the end // Unfortunately the comment is localized, so we have to search for all possible patterns $sCurrentLanguage = Dict::GetUserLanguage(); @@ -806,19 +806,19 @@ protected static function DoUpdateDBSchema($aSelectedModules, $sModulesDir, $aPa Dict::SetUserLanguage($sCurrentLanguage); $sCondition = "`userinfo` LIKE ".implode(" OR `userinfo` LIKE ", array_keys($aSuffixes)); - $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'synchro-data-source' WHERE ($sCondition)"; + $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'synchro-data-source' WHERE ($sCondition)"; CMDBSource::Query($sInit); - - SetupPage::log_info("Initialization of '{$sDBPrefix}priv_change.origin' completed."); + + SetupPage::log_info("Initialization of '{$sDBPrefix}priv_change.origin' completed."); } else { - SetupPage::log_info("'{$sDBPrefix}priv_change.origin' already initialized, nothing to do."); + SetupPage::log_info("'{$sDBPrefix}priv_change.origin' already initialized, nothing to do."); } } catch (Exception $e) { - SetupPage::log_error("Initializing '{$sDBPrefix}priv_change.origin' failed: ".$e->getMessage()); + SetupPage::log_error("Initializing '{$sDBPrefix}priv_change.origin' failed: ".$e->getMessage()); } // priv_async_task now has a 'status' field to distinguish between the various statuses rather than just relying on the date columns @@ -830,24 +830,24 @@ protected static function DoUpdateDBSchema($aSelectedModules, $sModulesDir, $aPa $iCount = (int)CMDBSource::QueryToScalar($sCount); if ($iCount > 0) { - SetupPage::log_info("Initializing '{$sDBPrefix}priv_async_task.status' ($iCount records to update)"); - + SetupPage::log_info("Initializing '{$sDBPrefix}priv_async_task.status' ($iCount records to update)"); + $sInit = "UPDATE `{$sDBPrefix}priv_async_task` SET `status` = 'planned' WHERE (`status` IS NULL) AND (`started` IS NULL)"; CMDBSource::Query($sInit); $sInit = "UPDATE `{$sDBPrefix}priv_async_task` SET `status` = 'error' WHERE (`status` IS NULL) AND (`started` IS NOT NULL)"; CMDBSource::Query($sInit); - - SetupPage::log_info("Initialization of '{$sDBPrefix}priv_async_task.status' completed."); + + SetupPage::log_info("Initialization of '{$sDBPrefix}priv_async_task.status' completed."); } else { - SetupPage::log_info("'{$sDBPrefix}priv_async_task.status' already initialized, nothing to do."); + SetupPage::log_info("'{$sDBPrefix}priv_async_task.status' already initialized, nothing to do."); } } catch (Exception $e) { - SetupPage::log_error("Initializing '{$sDBPrefix}priv_async_task.status' failed: ".$e->getMessage()); + SetupPage::log_error("Initializing '{$sDBPrefix}priv_async_task.status' failed: ".$e->getMessage()); } SetupPage::log_info("Database Schema Successfully Updated for environment '$sTargetEnvironment'."); @@ -887,15 +887,15 @@ protected static function AfterDBCreate( $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment); $oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database $oContextTag = new ContextTag(ContextTag::TAG_SETUP); - self::$bMetaModelStarted = true; // No need to reload the final MetaModel in case the installer runs synchronously - + self::$bMetaModelStarted = true; // No need to reload the final MetaModel in case the installer runs synchronously + // Perform here additional DB setup... profiles, etc... // $aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir); $oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation'); $oProductionEnv->UpdatePredefinedObjects(); - + if($sMode == 'install') { if (!self::CreateAdminAccount(MetaModel::GetConfig(), $sAdminUser, $sAdminPwd, $sAdminLanguage)) @@ -907,20 +907,20 @@ protected static function AfterDBCreate( SetupPage::log_info("Administrator account '$sAdminUser' created."); } } - + // Perform final setup tasks here // $oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup'); } - + /** * Helper function to create and administrator account for iTop - * @return boolean true on success, false otherwise + * @return boolean true on success, false otherwise */ protected static function CreateAdminAccount(Config $oConfig, $sAdminUser, $sAdminPwd, $sLanguage) { SetupPage::log_info('CreateAdminAccount'); - + if (UserRights::CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage)) { return true; @@ -946,9 +946,9 @@ protected static function DoLoadFiles( 'user rights' => 'addons/userrights/userrightsprofile.db.class.inc.php', )); } - + $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment); - + //Load the MetaModel if needed (asynchronous mode) if (!self::$bMetaModelStarted) { @@ -956,8 +956,8 @@ protected static function DoLoadFiles( $oContextTag = new ContextTag(ContextTag::TAG_SETUP); self::$bMetaModelStarted = true; // No need to reload the final MetaModel in case the installer runs synchronously - } - + } + $aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, APPROOT.$sModulesDir); $oProductionEnv->LoadData($aAvailableModules, $aSelectedModules, $bSampleData); @@ -992,7 +992,7 @@ protected static function DoCreateConfig( $bPreserveModuleSettings = false; if ($sMode == 'upgrade') { - try + try { $oOldConfig = new Config($sPreviousConfigFile); $oConfig = clone($oOldConfig); @@ -1038,7 +1038,7 @@ protected static function DoCreateConfig( { mkdir(APPCONF); chmod(APPCONF, 0770); // RWX for owner and group, nothing for others - SetupPage::log_info("Created configuration directory: ".APPCONF); + SetupPage::log_info("Created configuration directory: ".APPCONF); } // Write the final configuration file @@ -1048,7 +1048,7 @@ protected static function DoCreateConfig( @chmod($sConfigDir, 0770); // RWX for owner and group, nothing for others $oConfig->WriteToFile($sConfigFile); - + // try to make the final config file read-only @chmod($sConfigFile, 0440); // Read-only for owner and group, nothing for others diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 4dd2e281a1..4a24f0bcc7 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -76,7 +76,7 @@ class ModuleDiscovery 'doc.manual_setup' => 'url', 'doc.more_information' => 'url', ); - + // Cache the results and the source directories protected static $m_aSearchDirs = null; @@ -148,7 +148,7 @@ public static function AddModule($sFilePath, $sId, $aArgs) self::$m_aModuleVersionByName[$sModuleName]['version'] = $sModuleVersion; self::$m_aModuleVersionByName[$sModuleName]['id'] = $sId; } - + self::$m_aModules[$sId] = $aArgs; // Now keep the relative paths, as provided @@ -250,7 +250,7 @@ public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDep if ($bAbortOnMissingDependency && count($aDependencies) > 0) { $aModulesInfo = array(); - $aModuleDeps = array(); + $aModuleDeps = array(); foreach($aDependencies as $sId => $aDeps) { $aModule = $aModules[$sId]; @@ -282,7 +282,7 @@ public static function RemoveDuplicateModules($aModules) // The de-duplication is now done directly by the AddModule method return $aModules; } - + protected static function DependencyIsResolved($sDepString, $aOrderedModules, $aSelectedModules) { $bResult = false; @@ -329,12 +329,12 @@ protected static function DependencyIsResolved($sDepString, $aOrderedModules, $a if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) { $aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing - // a function call that results in a runtime fatal error + // a function call that results in a runtime fatal error } else { $aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing - // a function call that results in a runtime fatal error + // a function call that results in a runtime fatal error } } else @@ -393,20 +393,20 @@ public static function GetAvailableModules($aSearchDirs, $bAbortOnMissingDepende { self::ResetCache(); } - + if (is_null(self::$m_aSearchDirs)) { self::$m_aSearchDirs = $aSearchDirs; - + // Not in cache, let's scan the disk foreach($aSearchDirs as $sSearchDir) { - $sLookupDir = realpath($sSearchDir); + $sLookupDir = realpath($sSearchDir); if ($sLookupDir == '') { throw new Exception("Invalid directory '$sSearchDir'"); } - + clearstatcache(); self::ListModuleFiles(basename($sSearchDir), dirname($sSearchDir)); } @@ -418,7 +418,7 @@ public static function GetAvailableModules($aSearchDirs, $bAbortOnMissingDepende return self::GetModules($bAbortOnMissingDependency, $aModulesToLoad); } } - + public static function ResetCache() { self::$m_aSearchDirs = null; @@ -430,7 +430,7 @@ public static function ResetCache() * Helper function to interpret the name of a module * @param $sModuleId string Identifier of the module, in the form 'name/version' * @return array(name, version) - */ + */ public static function GetModuleName($sModuleId) { $aMatches = array(); @@ -459,7 +459,7 @@ protected static function ListModuleFiles($sRelDir, $sRootDir) { static $iDummyClassIndex = 0; $sDirectory = $sRootDir.'/'.$sRelDir; - + if ($hDir = opendir($sDirectory)) { // This is the correct way to loop over the directory. (according to the documentation) @@ -495,12 +495,12 @@ protected static function ListModuleFiles($sRelDir, $sRootDir) $idx++; } $bRet = eval($sModuleFileContents); - + if ($bRet === false) { SetupPage::log_warning("Eval of $sRelDir/$sFile returned false"); } - + //echo "

Done.

\n"; } catch(ParseError $e) @@ -528,7 +528,7 @@ protected static function ListModuleFiles($sRelDir, $sRootDir) /** Alias for backward compatibility with old module files in which * the declaration of a module invokes SetupWebPage::AddModule() * whereas the new form is ModuleDiscovery::AddModule() - */ + */ class SetupWebPage extends ModuleDiscovery { // For backward compatibility with old modules... @@ -555,9 +555,9 @@ public static function log_ok($sText) public static function log($sText) { SetupPage::log($sText); - } + } } - + /** Ugly patch !!! * In order to be able to analyse / load several times * the same module file, we rename the class (to avoid duplicate class definitions) diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 3b92fa460d..0d46c5ad3e 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -3,7 +3,7 @@ // // This file is part of iTop. // -// iTop is free software; you can redistribute it and/or modify +// iTop is free software; you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. @@ -100,14 +100,14 @@ public function LogQueryCallback($sQuery, $fDuration) $this->log_info(sprintf('%.3fs - query: %s ', $fDuration, $sQuery)); $this->log_db_query($sQuery); } - + /** * Helper function to initialize the ORM and load the data model * from the given file * @param $oConfig object The configuration (volatile, not necessarily already on disk) - * @param $bModelOnly boolean Whether or not to allow loading a data model with no corresponding DB + * @param $bModelOnly boolean Whether or not to allow loading a data model with no corresponding DB * @return none - */ + */ public function InitDataModel($oConfig, $bModelOnly = true, $bUseCache = false) { require_once APPROOT.'/setup/moduleinstallation.class.inc.php'; @@ -121,15 +121,15 @@ public function InitDataModel($oConfig, $bModelOnly = true, $bUseCache = false) { $this->log_info("MetaModel::Startup (ModelOnly = $bModelOnly)"); } - + if (!$bUseCache) { // Reset the cache for the first use ! MetaModel::ResetCache(md5(APPROOT).'-'.$this->sTargetEnv); } - + MetaModel::Startup($oConfig, $bModelOnly, $bUseCache, false /* $bTraceSourceFiles */, $this->sTargetEnv); - + if ($this->oExtensionsMap === null) { $this->oExtensionsMap = new iTopExtensionsMap($this->sTargetEnv); @@ -139,7 +139,7 @@ public function InitDataModel($oConfig, $bModelOnly = true, $bUseCache = false) /** * Analyzes the current installation and the possibilities * - * @param Config $oConfig Defines the target environment (DB) + * @param null|Config $oConfig Defines the target environment (DB) * @param mixed $modulesPath Either a single string or an array of absolute paths * @param bool $bAbortOnMissingDependency ... * @param array $aModulesToLoad List of modules to search for, defaults to all if omitted @@ -178,7 +178,7 @@ public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDepe 'name_code' => ITOP_APPLICATION, ) ); - + $aDirs = is_array($modulesPath) ? $modulesPath : array($modulesPath); $aModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad); foreach($aModules as $sModuleId => $aModuleInfo) @@ -194,11 +194,11 @@ public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDepe //throw new Exception("Missing version for the module: '$sModuleId'"); $sModuleVersion = '1.0.0'; } - + $sModuleAppVersion = $aModuleInfo['itop_version']; $aModuleInfo['version_db'] = ''; $aModuleInfo['version_code'] = $sModuleVersion; - + if (!in_array($sModuleAppVersion, array('1.0.0', '1.0.1', '1.0.2'))) { // This module is NOT compatible with the current version @@ -223,18 +223,20 @@ public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDepe } $aRes[$sModuleName] = $aModuleInfo; } - + try { - CMDBSource::InitFromConfig($oConfig); - $aSelectInstall = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install"); + $aSelectInstall = array(); + if (! is_null($oConfig)) { + CMDBSource::InitFromConfig($oConfig); + $aSelectInstall = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install"); + } } catch (MySQLException $e) { // No database or erroneous information - $aSelectInstall = array(); } - + // Build the list of installed module (get the latest installation) // $aInstallByModule = array(); // array of => array ('installed' => timestamp, 'version' => ) @@ -251,7 +253,7 @@ public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDepe } } } - + foreach ($aSelectInstall as $aInstall) { //$aInstall['comment']; // unsused @@ -265,7 +267,7 @@ public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDepe // as being installed $sModuleVersion = '0.0.0'; } - + if ($aInstall['parent_id'] == 0) { $sModuleName = ROOT_MODULE; @@ -275,7 +277,7 @@ public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDepe // Skip all modules belonging to previous installations continue; } - + if (array_key_exists($sModuleName, $aInstallByModule)) { if ($iInstalled < $aInstallByModule[$sModuleName]['installed']) @@ -283,30 +285,30 @@ public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDepe continue; } } - + if ($aInstall['parent_id'] == 0) { $aRes[$sModuleName]['version_db'] = $sModuleVersion; $aRes[$sModuleName]['name_db'] = $aInstall['name']; } - + $aInstallByModule[$sModuleName]['installed'] = $iInstalled; $aInstallByModule[$sModuleName]['version'] = $sModuleVersion; } - + // Adjust the list of proposed modules // foreach ($aInstallByModule as $sModuleName => $aModuleDB) { if ($sModuleName == ROOT_MODULE) continue; // Skip the main module - + if (!array_key_exists($sModuleName, $aRes)) { - // A module was installed, it is not proposed in the new build... skip + // A module was installed, it is not proposed in the new build... skip continue; } $aRes[$sModuleName]['version_db'] = $aModuleDB['version']; - + if ($aRes[$sModuleName]['install']['flag'] == MODULE_ACTION_MANDATORY) { $aRes[$sModuleName]['uninstall'] = array( @@ -322,7 +324,7 @@ public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDepe ); } } - + return $aRes; } @@ -336,9 +338,9 @@ public function WriteConfigFileSafe($oConfig) { self::MakeDirSafe(APPCONF); self::MakeDirSafe(APPCONF.$this->sTargetEnv); - + $sTargetConfigFile = APPCONF.$this->sTargetEnv.'/'.ITOP_CONFIG_FILE; - + // Write the config file @chmod($sTargetConfigFile, 0770); // In case it exists: RWX for owner and group, nothing for others $oConfig->WriteToFile($sTargetConfigFile); @@ -354,7 +356,7 @@ protected function GetExtraDirsToScan($aDirs = array()) // Do nothing, overload this method if needed return array(); } - + /** * Decide whether or not the given extension is selected for installation * @param iTopExtension $oExtension @@ -364,10 +366,10 @@ protected function IsExtensionSelected(iTopExtension $oExtension) { return ($oExtension->sSource == iTopExtension::SOURCE_REMOTE); } - + /** - * Get the installed modules (only the installed ones) - */ + * Get the installed modules (only the installed ones) + */ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) { $sSourceDirFull = APPROOT.$sSourceDir; @@ -388,7 +390,7 @@ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) $aExtraDirs = $this->GetExtraDirsToScan($aDirsToCompile); $aDirsToCompile = array_merge($aDirsToCompile, $aExtraDirs); - + $aRet = array(); // Determine the installed modules and extensions @@ -396,7 +398,7 @@ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) $oSourceConfig = new Config(APPCONF.$sSourceEnv.'/'.ITOP_CONFIG_FILE); $oSourceEnv = new RunTimeEnvironment($sSourceEnv); $aAvailableModules = $oSourceEnv->AnalyzeInstallation($oSourceConfig, $aDirsToCompile); - + // Actually read the modules available for the target environment, // but get the selection from the source environment and finally // mark as (automatically) chosen alll the "remote" modules present in the @@ -416,7 +418,7 @@ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) // $oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries'); $aRet[$oDictModule->GetName()] = $oDictModule; - + $oFactory = new ModelFactory($aDirsToCompile); $sDeltaFile = APPROOT.'core/datamodel.core.xml'; if (file_exists($sDeltaFile)) @@ -430,14 +432,14 @@ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) $oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile); $aRet[$oApplicationModule->GetName()] = $oApplicationModule; } - + $aModules = $oFactory->FindModules(); foreach($aModules as $oModule) { $sModule = $oModule->GetName(); $sModuleRootDir = $oModule->GetRootDir(); $bIsExtra = $this->oExtensionsMap->ModuleIsChosenAsPartOfAnExtension($sModule, iTopExtension::SOURCE_REMOTE); - if (array_key_exists($sModule, $aAvailableModules)) + if (array_key_exists($sModule, $aAvailableModules)) { if (($aAvailableModules[$sModule]['version_db'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) //Extra modules are always unless they are 'AutoSelect' { @@ -445,7 +447,7 @@ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) } } } - + // Now process the 'AutoSelect' modules do { @@ -514,7 +516,7 @@ public function CompileFrom($sSourceEnv, $bUseSymLinks = false) } $oFactory->LoadModule($oModule); } - + if ($oModule instanceof MFDeltaModule) { @@ -565,7 +567,7 @@ public function CreateDatabaseStructure(Config $oConfig, $sMode) { $this->log_info("Creating the structure in '".$oConfig->Get('db_name')."'."); } - + //MetaModel::CheckDefinitions(); if ($sMode == 'install') { @@ -597,7 +599,7 @@ public function CreateDatabaseStructure(Config $oConfig, $sMode) MetaModel::DBCreate(array($this, 'LogQueryCallback')); $this->log_ok("Database structure successfully updated."); - + // Check (and update only if it seems needed) the hierarchical keys if (MFCompiler::SkipRebuildHKeys()) { $this->log_ok("Hierchical keys are NOT rebuilt due to the presence of the \"data/.setup-rebuild-hkeys-never\" file"); @@ -657,7 +659,7 @@ public function UpdatePredefinedObjects() if ($aPredefinedObjects != null) { $this->log_info("$sClass::GetPredefinedObjects() returned " . count($aPredefinedObjects) . " elements."); - + // Create/Delete/Update objects of this class, // according to the given constant values // @@ -699,7 +701,7 @@ public function UpdatePredefinedObjects() // Restore the previous access mode $oConfig->Set('access_mode', $iPrevAccessMode); } - + public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelectedModuleCodes, $aSelectedExtensionCodes, $sShortComment = null) { // Have it work fine even if the DB has been set in read-only mode for the users @@ -708,7 +710,7 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect //$oConfig->Set('access_mode', ACCESS_FULL); if (CMDBSource::DBName() == '') - { + { // In case this has not yet been done CMDBSource::InitFromConfig($oConfig); } @@ -718,7 +720,7 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect $sShortComment = 'Done by the setup program'; } $sMainComment = $sShortComment."\nBuilt on ".ITOP_BUILD_DATE; - + // Record datamodel version $aData = array( 'source_dir' => $oConfig->Get('source_dir'), @@ -731,7 +733,7 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect $oInstallRec->Set('parent_id', 0); // root module $oInstallRec->Set('installed', $iInstallationTime); $iMainItopRecord = $oInstallRec->DBInsertNoReload(); - + // Record main installation $oInstallRec = new ModuleInstallation(); $oInstallRec->Set('name', ITOP_APPLICATION); @@ -740,8 +742,8 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect $oInstallRec->Set('parent_id', 0); // root module $oInstallRec->Set('installed', $iInstallationTime); $iMainItopRecord = $oInstallRec->DBInsertNoReload(); - - + + // Record installed modules and extensions // $aAvailableExtensions = array(); @@ -775,7 +777,7 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect $aComments[] = "Depends on module: $sDependOn"; } $sComment = implode("\n", $aComments); - + $oInstallRec = new ModuleInstallation(); $oInstallRec->Set('name', $sName); $oInstallRec->Set('version', $sVersion); @@ -784,7 +786,7 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect $oInstallRec->Set('installed', $iInstallationTime); $oInstallRec->DBInsertNoReload(); } - + if ($this->oExtensionsMap) { // Mark as chosen the selected extensions code passed to us @@ -796,7 +798,7 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect $this->oExtensionsMap->MarkAsChosen($oExtension->sCode); } } - + foreach($this->oExtensionsMap->GetChoices() as $oExtension) { $oInstallRec = new ExtensionInstallation(); @@ -813,9 +815,9 @@ public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelect MetaModel::GetConfig()->Set('access_mode', $iPrevAccessMode); // Database is created, installation has been tracked into it - return true; + return true; } - + public function GetApplicationVersion(Config $oConfig) { $aResult = false; @@ -832,7 +834,7 @@ public function GetApplicationVersion(Config $oConfig) $this->log_error('Exception '.$e->getMessage()); return false; } - + // Scan the list of installed modules to get the version of the 'ROOT' module which holds the main application version foreach ($aSelectInstall as $aInstall) { @@ -844,7 +846,7 @@ public function GetApplicationVersion(Config $oConfig) // as being installed $sModuleVersion = '0.0.0'; } - + if ($aInstall['parent_id'] == 0) { if ($aInstall['name'] == DATAMODEL_MODULE) @@ -870,7 +872,7 @@ public function GetApplicationVersion(Config $oConfig) $aResult['datamodel_version'] = $aResult['product_version']; } $this->log_info("GetApplicationVersion returns: product_name: ".$aResult['product_name'].', product_version: '.$aResult['product_version']); - return $aResult; + return $aResult; } public static function MakeDirSafe($sDir) @@ -886,8 +888,8 @@ public static function MakeDirSafe($sDir) } /** - * Wrappers for logging into the setup log files - */ + * Wrappers for logging into the setup log files + */ protected function log_error($sText) { SetupPage::log_error($sText); @@ -923,7 +925,7 @@ protected function log_db_query($sQuery) fclose($hSetupQueriesFile); } } - + public function GetCurrentDataModelVersion() { $oSearch = DBObjectSearch::FromOQL("SELECT ModuleInstallation WHERE name='".DATAMODEL_MODULE."'"); @@ -1116,7 +1118,7 @@ public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sH } } } - + /** * Load data from XML files for the selected modules (structural data and/or sample data) * @param array[] $aAvailableModules All available modules and their definition @@ -1126,13 +1128,13 @@ public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sH public function LoadData($aAvailableModules, $aSelectedModules, $bSampleData) { $oDataLoader = new XMLDataLoader(); - + CMDBObject::SetTrackInfo("Initialization"); $oMyChange = CMDBObject::GetCurrentChange(); - + SetupPage::log_info("starting data load session"); $oDataLoader->StartSession($oMyChange); - + $aFiles = array(); $aPreviouslyLoadedFiles = array(); foreach($aAvailableModules as $sModuleId => $aModule) @@ -1173,7 +1175,7 @@ public function LoadData($aAvailableModules, $aSelectedModules, $bSampleData) } } } - + // Simulate the load of the previously loaded files, in order to initialize // the mapping between the identifiers in the XML and the actual identifiers // in the current database @@ -1185,12 +1187,12 @@ public function LoadData($aAvailableModules, $aSelectedModules, $bSampleData) { throw(new Exception("File $sFileName does not exist")); } - + $oDataLoader->LoadFile($sFileName, true); $sResult = sprintf("loading of %s done.", basename($sFileName)); SetupPage::log_info($sResult); } - + foreach($aFiles as $sFileRelativePath) { $sFileName = APPROOT.$sFileRelativePath; @@ -1199,16 +1201,16 @@ public function LoadData($aAvailableModules, $aSelectedModules, $bSampleData) { throw(new Exception("File $sFileName does not exist")); } - + $oDataLoader->LoadFile($sFileName); $sResult = sprintf("loading of %s done.", basename($sFileName)); SetupPage::log_info($sResult); } - + $oDataLoader->EndSession(); SetupPage::log_info("ending data load session"); } - + /** * Merge two arrays of file names, adding the relative path to the files provided in the array to merge * @param string[] $aSourceArray @@ -1225,7 +1227,7 @@ protected static function MergeWithRelativeDir($aSourceArray, $sBaseDir, $aFiles } return array_merge($aSourceArray, $aToMerge); } - + /** * Check the MetaModel for some common pitfall (class name too long, classes requiring too many joins...) * The check takes about 900 ms for 200 classes @@ -1265,7 +1267,7 @@ public function CheckMetaModel() $iCount++; } $fDuration = microtime(true) - $fStart; - + return sprintf("Checked %d classes in %.1f ms. No error found.\n", $iCount, $fDuration*1000.0); } } // End of class diff --git a/setup/unattended-install/InstallationFileService.php b/setup/unattended-install/InstallationFileService.php new file mode 100644 index 0000000000..103080e59d --- /dev/null +++ b/setup/unattended-install/InstallationFileService.php @@ -0,0 +1,252 @@ +sInstallationPath = $sInstallationPath; + $this->aSelectedModules = []; + $this->aUnSelectedModules = []; + $this->sTargetEnvironment = $sTargetEnvironment; + $this->aSelectedExtensions = $aSelectedExtensions; + $this->bInstallationOptionalChoicesChecked = $bInstallationOptionalChoicesChecked; + } + + public function GetSelectedModules(): array { + return $this->aSelectedModules; + } + + public function GetUnSelectedModules(): array { + return $this->aUnSelectedModules; + } + + public function Init(): void { + clearstatcache(); + + $this->ProcessDefaultModules(); + $this->ProcessInstallationChoices(); + $this->ProcessAutoSelectModules(); + } + + public function ProcessInstallationChoices(): void { + $oXMLParameters = new XMLParameters($this->sInstallationPath); + $aSteps = $oXMLParameters->Get('steps', []); + if (! is_array($aSteps)) { + return; + } + + foreach ($aSteps as $aStepInfo) { + $aOptions = $aStepInfo["options"] ?? null; + if (! is_null($aOptions) && is_array($aOptions)) { + foreach ($aOptions as $aChoiceInfo) { + $this->ProcessSelectedChoice($aChoiceInfo, $this->bInstallationOptionalChoicesChecked); + } + } + $aOptions = $aStepInfo["alternatives"] ?? null; + if (! is_null($aOptions) && is_array($aOptions)) { + foreach ($aOptions as $aChoiceInfo) { + $this->ProcessSelectedChoice($aChoiceInfo, false); + } + } + } + + foreach ($this->aSelectedModules as $sModuleId => $sVal){ + if (array_key_exists($sModuleId, $this->aUnSelectedModules)){ + unset($this->aUnSelectedModules[$sModuleId]); + } + } + } + + private function ProcessUnSelectedChoice($aChoiceInfo) { + if (!is_array($aChoiceInfo)) { + return; + } + + $aCurrentModules = $aChoiceInfo["modules"] ?? []; + foreach ($aCurrentModules as $sModuleId){ + $this->aUnSelectedModules[$sModuleId] = true; + } + + $aAlternatives = $aChoiceInfo["alternatives"] ?? null; + if (!is_null($aAlternatives) && is_array($aAlternatives)) { + foreach ($aAlternatives as $aSubChoiceInfo) { + $this->ProcessUnSelectedChoice($aSubChoiceInfo); + } + } + + if (array_key_exists('sub_options', $aChoiceInfo)) { + if (array_key_exists('options', $aChoiceInfo['sub_options'])) { + $aSubOptions = $aChoiceInfo['sub_options']['options']; + if (!is_null($aSubOptions) && is_array($aSubOptions)) { + foreach ($aSubOptions as $aSubChoiceInfo) { + $this->ProcessUnSelectedChoice($aSubChoiceInfo); + } + } + } + if (array_key_exists('alternatives', $aChoiceInfo['sub_options'])) { + $aSubAlternatives = $aChoiceInfo['sub_options']['alternatives']; + if (!is_null($aSubAlternatives) && is_array($aSubAlternatives)) { + foreach ($aSubAlternatives as $aSubChoiceInfo) { + $this->ProcessUnSelectedChoice($aSubChoiceInfo); + } + } + } + } + } + + private function ProcessSelectedChoice($aChoiceInfo, bool $bAllChecked) { + if (!is_array($aChoiceInfo)) { + return; + } + + $sDefault = $aChoiceInfo["default"] ?? "false"; + $sMandatory = $aChoiceInfo["mandatory"] ?? "false"; + + $aCurrentModules = $aChoiceInfo["modules"] ?? []; + if (0 === count($this->aSelectedExtensions)){ + $bSelected = $bAllChecked || $sDefault === "true" || $sMandatory === "true"; + } else { + $sExtensionCode = $aChoiceInfo["extension_code"] ?? null; + $bSelected = $sMandatory === "true" || + (null !== $sExtensionCode && in_array($sExtensionCode, $this->aSelectedExtensions)); + } + + foreach ($aCurrentModules as $sModuleId){ + if ($bSelected) { + $this->aSelectedModules[$sModuleId] = true; + } else { + $this->aUnSelectedModules[$sModuleId] = true; + } + } + + $aAlternatives = $aChoiceInfo["alternatives"] ?? null; + if (!is_null($aAlternatives) && is_array($aAlternatives)) { + foreach ($aAlternatives as $aSubChoiceInfo) { + if ($bSelected) { + $this->ProcessSelectedChoice($aSubChoiceInfo, $bAllChecked); + } else { + $this->ProcessUnSelectedChoice($aSubChoiceInfo); + } + } + } + + if (array_key_exists('sub_options', $aChoiceInfo)) { + if (array_key_exists('options', $aChoiceInfo['sub_options'])) { + $aSubOptions = $aChoiceInfo['sub_options']['options']; + if (!is_null($aSubOptions) && is_array($aSubOptions)) { + foreach ($aSubOptions as $aSubChoiceInfo) { + if ($bSelected) { + $this->ProcessSelectedChoice($aSubChoiceInfo, $bAllChecked); + } else { + $this->ProcessUnSelectedChoice($aSubChoiceInfo); + } + } + } + } + if (array_key_exists('alternatives', $aChoiceInfo['sub_options'])) { + $aSubAlternatives = $aChoiceInfo['sub_options']['alternatives']; + if (!is_null($aSubAlternatives) && is_array($aSubAlternatives)) { + foreach ($aSubAlternatives as $aSubChoiceInfo) { + if ($bSelected) { + $this->ProcessSelectedChoice($aSubChoiceInfo, false); + } else { + $this->ProcessUnSelectedChoice($aSubChoiceInfo); + } + } + } + } + } + } + + private function GetExtraDirs() : array { + $aSearchDirs = []; + + $aDirs = [ + '/datamodels/1.x', + '/datamodels/2.x', + 'data/' . $this->sTargetEnvironment . '-modules', + 'extensions', + ]; + foreach ($aDirs as $sRelativeDir){ + $sDirPath = APPROOT.$sRelativeDir; + if (is_dir($sDirPath)) + { + $aSearchDirs[] = $sDirPath; + } + } + + return $aSearchDirs; + } + + public function ProcessDefaultModules() : void { + $sProductionModuleDir = APPROOT.'data/' . $this->sTargetEnvironment . '-modules/'; + + $oProductionEnv = new RunTimeEnvironment(); + $aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), $this->GetExtraDirs(), false, null); + + $this->aAutoSelectModules = []; + foreach ($aAvailableModules as $sModuleId => $aModule) { + if (($sModuleId != ROOT_MODULE)) { + if (isset($aModule['auto_select'])) { + $this->aAutoSelectModules[$sModuleId] = $aModule; + continue; + } + + if (($aModule['category'] == 'authentication') || (!$aModule['visible'])) { + $this->aSelectedModules[$sModuleId] = true; + continue; + } + + $bIsExtra = (array_key_exists('root_dir', $aModule) && (strpos($aModule['root_dir'], + $sProductionModuleDir) !== false)); // Some modules (root, datamodel) have no 'root_dir' + if ($bIsExtra) { + // Modules in data/production-modules/ are considered as mandatory and always installed + $this->aSelectedModules[$sModuleId] = true; + } + } + } + } + + public function ProcessAutoSelectModules() : void { + foreach($this->aAutoSelectModules as $sModuleId => $aModule) + { + try { + $bSelected = false; + SetupInfo::SetSelectedModules($this->aSelectedModules); + eval('$bSelected = ('.$aModule['auto_select'].');'); + if ($bSelected) + { + // Modules in data/production-modules/ are considered as mandatory and always installed + $this->aSelectedModules[$sModuleId] = true; + } + } + catch (Exception $e) { + } + } + } +} diff --git a/setup/unattended-install/README.md b/setup/unattended-install/README.md index d4ca4249b3..bb1a19ebca 100644 --- a/setup/unattended-install/README.md +++ b/setup/unattended-install/README.md @@ -3,3 +3,23 @@ This script allows to install and update iTop via CLI. For more information, see the official Wiki : [Automated installation [iTop Documentation]](https://www.itophub.io/wiki/page?id=latest:advancedtopics:automatic_install) + + +#install-itop.sh +You can install your iTop by only using config-itop.php settings and run either + +- a non-ITIL iTop fresh installation (use itil-fresh-install.xml to have ITIL modules instead) +``` +./install-itop.sh ./xml_setup/fresh-install.xml +``` + +- a non-ITIL iTop upgrade (use itil-upgrade.xml to have ITIL modules instead) +``` +./install-itop.sh ./xml_setup/upgrade.xml +``` + +- a specific iTop installation by providing both xml setup file +in below example file provided is the one generated by iTop during last setup. +``` +./install-itop.sh ../../log/install-2024-04-03.xml +``` diff --git a/setup/unattended-install/install-itop.sh b/setup/unattended-install/install-itop.sh new file mode 100644 index 0000000000..dc165c57f9 --- /dev/null +++ b/setup/unattended-install/install-itop.sh @@ -0,0 +1,50 @@ +#! /bin/bash + +CLI_NAME=$(basename $0) +DIR=$(dirname $0) +ITOP_DIR="$DIR/../.." + +HELP="Syntax: $CLI_NAME XML_SETUP [INSTALLATION_XML]" + +function HELP { + echo $HELP + exit 1 +} + +if [ $# -lt 1 ] +then + echo "Missing parameters passed." + HELP +fi + +if [ $# -gt 2 ] +then + echo "Too much parameters passed ($#) : $*." + HELP +fi + +XML_SETUP=$1 +if [ ! -f $XML_SETUP ] +then + echo "XML_SETUP file ($XML_SETUP) not found." + HELP +fi + +if [ $# -eq 2 ] +then + INSTALLATION_XML=$2 + if [ ! -f $INSTALLATION_XML ] + then + echo "INSTALLATION_XML file ($INSTALLATION_XML) not found." + HELP + fi +else + INSTALLATION_XML="$ITOP_DIR/datamodels/2.x/installation.xml" +fi + +echo "$CLI_NAME: Using XML_SETUP ($XML_SETUP) and INSTALLATION_XML ($INSTALLATION_XML) files during unattended itop installation." + +rm -rf $ITOP_DIR/data/.maintenance; +echo php $DIR/unattended-install.php --use_itop_config --installation_xml="$INSTALLATION_XML" --param-file="$XML_SETUP" + +php $DIR/unattended-install.php --use_itop_config --installation_xml="$INSTALLATION_XML" --param-file="$XML_SETUP" diff --git a/setup/unattended-install/unattended-install.php b/setup/unattended-install/unattended-install.php index 09af751ece..10dc03fc03 100644 --- a/setup/unattended-install/unattended-install.php +++ b/setup/unattended-install/unattended-install.php @@ -1,17 +1,29 @@ [--installation_xml=] [--use_itop_config] + +Options: + --param-file= Path to the file (XML) to use for the unattended installation. That file (generated by the setup into log directory) must contain the following sections: + - target_env: the target environment (production, test, dev) + - database: the database settings (server, user, pwd, name, prefix) + - selected_modules: the list of modules to install + --response_file DEPRECATED: use `--param-file` instead + --installation_xml= Use an installation.xml file to compute the modules to install depending on the selected extensions listed in the param file + --use_itop_config Use the iTop configuration file to get the database settings, otherwise use the database settings from the parameters file +Advanced options: + --check-consistency=1 Check the data model consistency after the installation (default: 0) + --clean=1 In case of a first installation, cleanup the environment before proceeding: delete the configuration file, the cache directory, the target directory, the database (default: 0) + --install=0 Set to 0 to perform a dry-run (default: 1) +EOF; + exit(-1); +} ///////////////////////////////////////////////// if (! utils::IsModeCLI()) { @@ -19,17 +31,22 @@ exit(-1); } -$sParamFile = utils::ReadParam('response_file', 'null', true /* CLI allowed */, 'raw_data'); -if ($sParamFile === 'null') { - echo "No `--response_file` param specified, using default value !\n"; - $sParamFile = 'default-params.xml'; +if (in_array('--help', $argv)) { + PrintUsageAndExit(); +} + +$sParamFile = utils::ReadParam('param-file', null, true /* CLI allowed */, 'raw_data') ?? utils::ReadParam('response_file', null, true /* CLI allowed */, 'raw_data'); +if (is_null($sParamFile)) { + echo "Missing mandatory argument `--param-file`.\n"; + PrintUsageAndExit(); } -$bCheckConsistency = (utils::ReadParam('check_consistency', '0', true /* CLI allowed */) == '1'); +$bCheckConsistency = (utils::ReadParam('check-consistency', '0', true /* CLI allowed */) == '1'); if (false === file_exists($sParamFile)) { - echo "Param file `$sParamFile` doesn't exist ! Exiting..."; + echo "Param file `$sParamFile` doesn't exist! Exiting...\n"; exit(-1); } + $oParams = new XMLParameters($sParamFile); $sMode = $oParams->Get('mode'); @@ -40,8 +57,73 @@ $sTargetEnvironment = 'production'; } -//unattended run based on db settings coming from response_file (XML file) -$aDBXmlSettings = $oParams->Get('database', array()); +$sXmlSetupBaseName = basename($sParamFile); +$sInstallationXmlPath = utils::ReadParam('installation_xml', null, true /* CLI allowed */, 'raw_data'); +if (! is_null($sInstallationXmlPath) && is_file($sInstallationXmlPath)) { + $sInstallationBaseName = basename($sInstallationXmlPath); + + $aSelectedExtensionsFromXmlSetup = $oParams->Get('selected_extensions', []); + if (count($aSelectedExtensionsFromXmlSetup) !== 0) { + $sMsg = "Modules to install computed based on $sInstallationBaseName file and installation choices (listed in section `selected_extensions` of $sXmlSetupBaseName file)"; + echo "$sMsg:\n".implode(',', $aSelectedExtensionsFromXmlSetup)."\n\n"; + SetupLog::Info($sMsg, null, $aSelectedExtensionsFromXmlSetup); + } else { + $sMsg = "Modules to install computed based on default installation choices inside $sInstallationBaseName (no choice specified in section `selected_extensions` of $sXmlSetupBaseName file)."; + echo "$sMsg\n\n"; + SetupLog::Info($sMsg); + } + + $oInstallationFileService = new InstallationFileService($sInstallationXmlPath, $sTargetEnvironment, $aSelectedExtensionsFromXmlSetup); + $oInstallationFileService->Init(); + $aComputedModules = $oInstallationFileService->GetSelectedModules(); + $aSelectedModules = array_keys($aComputedModules); + $oParams->Set('selected_modules', $aSelectedModules); + + $sMsg = "Modules to install computed"; +} else { + $aSelectedModules = $oParams->Get('selected_modules', []); + $sMsg = "Modules to install listed in $sXmlSetupBaseName (selected_modules section)"; +} + +sort($aSelectedModules); +echo "$sMsg:\n".implode(',', $aSelectedModules)."\n\n"; +SetupLog::Info($sMsg, null, $aSelectedModules); + +// Configuration file +$sConfigFile = APPCONF.$sTargetEnvironment.'/'.ITOP_CONFIG_FILE; +$bUseItopConfig = in_array('--use_itop_config', $argv); +if ($bUseItopConfig && file_exists($sConfigFile)){ + //unattended run based on db settings coming from itop configuration + copy($sConfigFile, "$sConfigFile.backup"); + + $oConfig = new Config($sConfigFile); + $aDBXmlSettings = $oParams->Get('database', array()); + $aDBXmlSettings ['server'] = $oConfig->Get('db_host'); + $aDBXmlSettings ['user'] = $oConfig->Get('db_user'); + $aDBXmlSettings ['pwd'] = $oConfig->Get('db_pwd'); + $aDBXmlSettings ['name'] = $oConfig->Get('db_name'); + $aDBXmlSettings ['prefix'] = $oConfig->Get('db_subname'); + $aDBXmlSettings ['db_tls_enabled'] = $oConfig->Get('db_tls.enabled'); + //cannot be null or infinite loop triggered! + $aDBXmlSettings ['db_tls_ca'] = $oConfig->Get('db_tls.ca') ?? ""; + $oParams->Set('database', $aDBXmlSettings); + + $aFields = [ + 'url' => 'app_root_url', + 'source_dir' => 'source_dir', + 'graphviz_path' => 'graphviz_path', + ]; + foreach($aFields as $sSetupField => $sConfField){ + $oParams->Set($sSetupField, $oConfig->Get($sConfField)); + } + + $oParams->Set('mysql_bindir', $oConfig->GetModuleSetting('itop-backup', 'mysql_bindir', "")); + $oParams->Set('language', $oConfig->GetDefaultLanguage()); +} else { + //unattended run based on db settings coming from response_file (XML file) + $aDBXmlSettings = $oParams->Get('database', array()); +} + $sDBServer = $aDBXmlSettings['server']; $sDBUser = $aDBXmlSettings['user']; $sDBPwd = $aDBXmlSettings['pwd']; @@ -57,8 +139,6 @@ { echo "Cleanup mode detected.\n"; - // Configuration file - $sConfigFile = APPCONF.$sTargetEnvironment.'/'.ITOP_CONFIG_FILE; if (file_exists($sConfigFile)) { echo "Trying to delete the configuration file: '$sConfigFile'.\n"; @@ -200,7 +280,7 @@ $sLogMsg = "Encountered stopper issues. Aborting..."; echo "$sLogMsg\n"; SetupLog::Error($sLogMsg); - die; + exit(-1); } $bFoundIssues = false; @@ -291,11 +371,18 @@ } } -if (!$bFoundIssues) +if (! $bFoundIssues) { // last line: used to check the install // the only way to track issues in case of Fatal error or even parsing error! $sLogMsg = "installed!"; + + if ($bUseItopConfig && is_file("$sConfigFile.backup")) + { + echo "\nuse config file provided by backup in $sConfigFile."; + copy("$sConfigFile.backup", $sConfigFile); + } + SetupLog::Info($sLogMsg); echo "\n$sLogMsg"; exit(0); @@ -303,5 +390,5 @@ $sLogMsg = "installation failed!"; SetupLog::Error($sLogMsg); -echo "\n$sLogMsg"; +echo "\n$sLogMsg\n"; exit(-1); diff --git a/setup/unattended-install/xml_setup/fresh-install.xml b/setup/unattended-install/xml_setup/fresh-install.xml new file mode 100644 index 0000000000..db1311cf55 --- /dev/null +++ b/setup/unattended-install/xml_setup/fresh-install.xml @@ -0,0 +1,41 @@ + + + install + + + datamodels/2.x/ + 2.7.0 + /var/www/html/iTop/conf/production/config-itop.php + extensions + production + + + + + + + + + + + + /usr/bin/dot + + + + + + + + + + + 1 + + + + + + + + diff --git a/setup/unattended-install/xml_setup/itil-fresh-install.xml b/setup/unattended-install/xml_setup/itil-fresh-install.xml new file mode 100644 index 0000000000..e1b1e594f0 --- /dev/null +++ b/setup/unattended-install/xml_setup/itil-fresh-install.xml @@ -0,0 +1,53 @@ + + + install + + + datamodels/2.x/ + 2.7.0 + /var/www/html/iTop/conf/production/config-itop.php + extensions + production + + + + + + + + + + + + /usr/bin/dot + + + + + + + + + + + 1 + + + + + + + itop-config-mgmt-datacenter + itop-config-mgmt-end-user + itop-config-mgmt-storage + itop-config-mgmt-virtualization + itop-service-mgmt-enterprise + itop-ticket-mgmt-itil + itop-ticket-mgmt-itil-user-request + itop-ticket-mgmt-itil-incident + itop-ticket-mgmt-itil-enhanced-portal + itop-change-mgmt-itil + itop-config-mgmt-core + itop-kown-error-mgmt + + diff --git a/setup/unattended-install/xml_setup/itil-upgrade.xml b/setup/unattended-install/xml_setup/itil-upgrade.xml new file mode 100644 index 0000000000..eb0a63208a --- /dev/null +++ b/setup/unattended-install/xml_setup/itil-upgrade.xml @@ -0,0 +1,53 @@ + + + upgrade + + + datamodels/2.x/ + 2.7.0 + /var/www/html/iTop/conf/production/config-itop.php + extensions + production + + + + + + + + + + + + /usr/bin/dot + + + + + + + + + + + 1 + + + + + + + itop-config-mgmt-datacenter + itop-config-mgmt-end-user + itop-config-mgmt-storage + itop-config-mgmt-virtualization + itop-service-mgmt-enterprise + itop-ticket-mgmt-itil + itop-ticket-mgmt-itil-user-request + itop-ticket-mgmt-itil-incident + itop-ticket-mgmt-itil-enhanced-portal + itop-change-mgmt-itil + itop-config-mgmt-core + itop-kown-error-mgmt + + diff --git a/setup/unattended-install/xml_setup/upgrade.xml b/setup/unattended-install/xml_setup/upgrade.xml new file mode 100644 index 0000000000..ff0d153f1e --- /dev/null +++ b/setup/unattended-install/xml_setup/upgrade.xml @@ -0,0 +1,41 @@ + + + upgrade + + + datamodels/2.x/ + 2.7.0 + /var/www/html/iTop/conf/production/config-itop.php + extensions + production + + + + + + + + + + + + /usr/bin/dot + + + + + + + + + + + 1 + + + + + + + + \ No newline at end of file diff --git a/tests/ci_description.ini b/tests/ci_description.ini index b9f1cdd4cb..d840cd5ee2 100644 --- a/tests/ci_description.ini +++ b/tests/ci_description.ini @@ -5,10 +5,10 @@ php_version=7.2-apache db_version=5.7 [itop] -itop_setup=tests/setup_params/default-params.xml +;itop_setup=tests/setup_params/default-params.xml itop_backup=tests/backups/backup-itop.tar.gz [phpunit] ; when empty phpunit_xml => no phpunit test performed ; phpunit xml file description. required for phpunit testing -phpunit_xml=tests/php-unit-tests/phpunit.xml.dist +;phpunit_xml=tests/php-unit-tests/phpunit.xml.dist diff --git a/tests/php-static-analysis/README.md b/tests/php-static-analysis/README.md new file mode 100644 index 0000000000..8c365b124b --- /dev/null +++ b/tests/php-static-analysis/README.md @@ -0,0 +1,129 @@ +# PHP static analysis + +- [Installation](#installation) +- [Usages](#usages) + - [Analysing a package](#analysing-a-package) + - [Analysing a module](#analysing-a-module) +- [Configuration](#configuration) + - [Adjust local configuration to your needs](#adjust-local-configuration-to-your-needs) + - [Adjust configuration for a particular CI repository / job](#adjust-configuration-for-a-particular-ci-repository--job) + +## Installation +- Install dependencies by running `composer install` in this folder +- You should be all set! 🚀 + +## Usages +### Analysing a package +_Do this if you want to analyse the whole iTop package (iTop core, extensions, third-party libs, ...)_ + +- Make sure you ran a setup on your iTop as it will analyse the `env-production` folder +- Open a prompt in your iTop folder +- Run the following command + ```bash + tests/php-static-analysis/vendor/bin/phpstan analyse \ + --configuration ./tests/php-static-analysis/config/for-package.dist.neon \ + --error-format raw + ``` + +You will then have an output like this listing all errors: +```bash +tests/php-static-analysis/vendor/bin/phpstan analyse \ + --configuration ./tests/php-static-analysis/config/for-package.dist.neon \ + --error-format raw + + 1049/1049 [============================] 100% + +\addons\userrights\userrightsprofile.class.inc.php:552:Call to static method InitSharedClassProperties() on an unknown class SharedObject. +\addons\userrights\userrightsprofile.db.class.inc.php:927:Call to static method GetSharedClassProperties() on an unknown class SharedObject. +\addons\userrights\userrightsprojection.class.inc.php:722:Access to an undefined property UserRightsProjection::$m_aClassProjs. +\application\applicationextension.inc.php:295:Method AbstractPreferencesExtension::ApplyPreferences() should return bool but return statement is missing. +\application\cmdbabstract.class.inc.php:1010:Class utils referenced with incorrect case: Utils. +[...] +``` + +### Analysing a module +_Do this if you only want to analyse one or more modules within this iTop but not the whole package_ + +- Make sure you ran a setup on your iTop as it will analyse the `env-production` folder +- Open a prompt in your iTop folder +- Run the following command + ``` + tests/php-static-analysis/vendor/bin/phpstan analyse \ + --configuration ./tests/php-static-analysis/config/for-package.dist.neon \ + --error-format raw \ + env-production/ [env-production/ ...] + ``` + +You will then have an output like this listing all errors: +``` + tests/php-static-analysis/vendor/bin/phpstan analyse \ + --configuration ./tests/php-static-analysis/config/for-module.dist.neon \ + --error-format raw \ + env-production/authent-ldap env-production/itop-oauth-client + + 49/49 [============================] 100% + +\env-production\authent-ldap\model.authent-ldap.php:79:Undefined variable: $hDS +\env-production\authent-ldap\model.authent-ldap.php:80:Undefined variable: $name +\env-production\authent-ldap\model.authent-ldap.php:80:Undefined variable: $value +\env-production\itop-oauth-client\vendor\composer\InstalledVersions.php:105:Parameter $parser of method Composer\InstalledVersions::satisfies() has invalid type Composer\Semver\VersionParser. +[...] +``` + +## Configuration +### Adjust local configuration to your needs +#### Define which PHP version to run the analysis for +The way we configured PHPStan in this project changes how it will find the PHP version to run the analysis for. \ +By default PHPStan check the information from the composer.json file, but we changed that (via the `config/php-includes/set-php-version-from-process.php` include) so it used the PHP +version currently ran by the CLI. + +So all you have to do is either: +- Prepend your command line with the path of the executable of the desired PHP version +- Change the default PHP interpreter in your IDE settings + +#### Change some parameters for a local run +If you want to change some particular settings (eg. the memory limit, the rules level, ...) for a local run of the analysis you have 2 choices. + +##### Method 1: CLI parameter +For most parameters there is a good chance you can just add the parameter and its value in your command, which will override the one defined in the configuration file. \ +Below are some example, but your can find the complete reference [here](https://phpstan.org/user-guide/command-line-usage). + +```bash +--memory-limit 1G +--level 5 +--error-format raw +[...] +``` + +**Pros** Quick and easy to try different parameters \ +**Cons** Parameters aren't saved, so you'll have to remember them and put them again next time + +##### Method 2: Configuration file +Crafting your own configuration file gives you the ability to fine tune any parameters, it's way more powerful but can also quickly lead to crashes if you mess with the symbols discovery (classes, ...). \ +But mostly it can be saved, shared, re-used; which is it's main purpose. + +It is recommended that you create your configuration file from scratch and that you include the `base.dist.neon` so you are bootstrapped for the symbols discovery. Then you can override any parameter. \ +Check [the documentation](https://phpstan.org/config-reference#multiple-files) for more information. + +```neon +includes: + - base.dist.neon + +parameters: + # Override parameters here +``` + +#### Analyse only one (or some) folder(s) quicker +It's pretty easy and good news you don't need to create a new configuration file or change an existing one. \ +Just adapt and use command lines from the [usages section](#usages) and add the folders you want to analyse at the end of the command, exactly like when analysing modules. + +For example if you want to analyse just `/setup` and `/sources`, use something like: +``` +tests/php-static-analysis/vendor/bin/phpstan analyse \ + --configuration ./tests/php-static-analysis/config/for-package.dist.neon \ + --error-format raw \ + setup sources +``` + +### Adjust configuration for a particular CI repository / job +TODO \ No newline at end of file diff --git a/tests/php-static-analysis/composer.json b/tests/php-static-analysis/composer.json new file mode 100644 index 0000000000..79097c0b3e --- /dev/null +++ b/tests/php-static-analysis/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "phpstan/phpstan": "^1.10" + } +} diff --git a/tests/php-static-analysis/composer.lock b/tests/php-static-analysis/composer.lock new file mode 100644 index 0000000000..4e52e3fb3e --- /dev/null +++ b/tests/php-static-analysis/composer.lock @@ -0,0 +1,81 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "14812c2a05a5972f00f9d67abbd710a9", + "packages": [ + { + "name": "phpstan/phpstan", + "version": "1.10.26", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "5d660cbb7e1b89253a47147ae44044f49832351f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5d660cbb7e1b89253a47147ae44044f49832351f", + "reference": "5d660cbb7e1b89253a47147ae44044f49832351f", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2023-07-19T12:44:37+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/tests/php-static-analysis/config/README.md b/tests/php-static-analysis/config/README.md new file mode 100644 index 0000000000..2ef0858892 --- /dev/null +++ b/tests/php-static-analysis/config/README.md @@ -0,0 +1,29 @@ +## Disclaimer +DON'T modify the following files without knowledge and discussing with the team: +- base.dist.neon +- for-package.dist.neon +- for-module.dist.neon + +## Purpose of these files +### base.dist.neon +This configuration file contains the common parameters for all analysis, whereas it is a package, a module or something specific. Among others: +- Rules level for analysis +- PHP version to compare +- Necessary files for autoloaders discovery and such +- ... + +This file should not be modified for your specific needs, you should always include it and override the desired parameters. \ +See how it is done in `for-package.dist.neon` and `for-module.dist.neon` or on the documentation [here](https://phpstan.org/config-reference#multiple-files). + +### for-package.dist.neon +This configuration file contains the parameters to analyse a package (iTop core, modules, third-party libs). + +### for-module.dist.neon +This configuration file contains the parameters to analyse one or more modules only. + +## How / when can I modify these files? +**You CAN'T!** \ +Well, unless there is a good reason and you talked about it with the team. But you should never modify them for a specific need on your local environment. + +- If you have a particular need for your local environment (eg. increase memory limit, change rules levels, analyse only a specific folder), check the [Configuration section](../#configuration) of the main README.md. +- If you feel like there is need for an adjustment in the default configurations, discuss it with th team and make a PR. \ No newline at end of file diff --git a/tests/php-static-analysis/config/base.dist.neon b/tests/php-static-analysis/config/base.dist.neon new file mode 100644 index 0000000000..915500880d --- /dev/null +++ b/tests/php-static-analysis/config/base.dist.neon @@ -0,0 +1,34 @@ +includes: + - php-includes/set-php-version-from-process.php # Workaround to set PHP version to the on running the CLI + # for an explanation of the baseline concept, see: https://phpstan.org/user-guide/baseline + #baseline HERE DO NOT REMOVE FOR CI + +parameters: + level: 0 + #phpVersion: null # Explicitly commented as we rather use the detected version from the above include (`php-includes/target-php-version.php`) + editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%' # Open in PHPStorm asit is Combodo's default IDE + bootstrapFiles: + - ../../../approot.inc.php + - ../../../bootstrap.inc.php + scanFiles: + # Files necessary as they contain some declarations (constants, classes, functions, ...) + - ../../../approot.inc.php + - ../../../bootstrap.inc.php + excludePaths: + analyse: + # For third-party libs we should analyse them in a dedicated configuration as we can't improve / clean them which would + # prevent us from raising the rules level as we improve / clean our codebase + - ../../../lib # Irrelevant as we only want to analyze our codebase + - ../../../node_modules # Irrelevant as we only want to analyze our codebase + # TMP WIP for CI debug + - ../../../core/apc-emulation.php + - ../../../core/oql/build + analyseAndScan: + #- ../../../data # Left and commented on purpose to show that we want to analyse the generated cache files + # Note 1: We can analyse these folders as if a PHP file requires another PHP element declared in an XML file, it won't find it. So we rely only on `env-production` + # Note 2: Only the options selected during the setup will be analysed correctly in `env-production`. For unselected options, we still want to ignore them during the analysis as they would only give a false sentiment of security as their XML PHP classes / snippets / etc would not be tested. + - ../../../data/production-modules # Irrelevent as it will already be in `env-production` (for local run only, not useful in the CI) + - ../../../datamodels # Irrelevent as it will already be in `env-production` + - ../../../extensions # Irrelevent as it will already be in `env-production` (for local run only, not useful in the CI) + - ../../../tests # Exclude tests for now + - ../../../toolkit # Exlclude toolkit for now diff --git a/tests/php-static-analysis/config/for-module.dist.neon b/tests/php-static-analysis/config/for-module.dist.neon new file mode 100644 index 0000000000..586be18012 --- /dev/null +++ b/tests/php-static-analysis/config/for-module.dist.neon @@ -0,0 +1,15 @@ +includes: + - base.dist.neon + +parameters: + paths: + # We just want to analyse the module folder(s), either: + # - Create your own `for-module.neon` file, include this one and override this parameter (see https://phpstan.org/config-reference#multiple-files) + # - Pass the module folder(s) in the commande line (see https://phpstan.org/config-reference#analysed-files) + scanDirectories: + # Unlike for `for-package.dist.neon`, here we need to scan all the folders to discover symbols, but we only want to analyse the module folder. + # We initially thought of doing it through the `excludePaths` param. by excluding everything but the module folder, but it doesn't seem to be possible, because it uses the `fnmatch()` function. + # As a workaround, we list here all the folders to scan. + # + # Scan the whole project and rely on the `excludePaths` param. to filter the unnecessary + - ../../.. diff --git a/tests/php-static-analysis/config/for-package.dist.neon b/tests/php-static-analysis/config/for-package.dist.neon new file mode 100644 index 0000000000..ff0d190ebf --- /dev/null +++ b/tests/php-static-analysis/config/for-package.dist.neon @@ -0,0 +1,7 @@ +includes: + - base.dist.neon + +parameters: + paths: + # We want to analyse almost the whole project, so we do a negative selection between the `paths` and `excludePaths` (see base.dist.neon) parameters + - ../../../ diff --git a/tests/php-static-analysis/config/php-includes/set-php-version-from-process.php b/tests/php-static-analysis/config/php-includes/set-php-version-from-process.php new file mode 100644 index 0000000000..4a618628a5 --- /dev/null +++ b/tests/php-static-analysis/config/php-includes/set-php-version-from-process.php @@ -0,0 +1,24 @@ +sFolderToCleanup = null; + \ModuleDiscovery::ResetCache(); + } + + protected function tearDown(): void { + parent::tearDown(); + + $sModuleId = "itop-problem-mgmt"; + $this->RecurseMoveDir(APPROOT."data/production-modules/$sModuleId", APPROOT . "datamodels/2.x/$sModuleId"); + } + + public function GetDefaultModulesProvider() { + return [ + 'all checked' => [ true ], + 'only defaut + mandatory' => [ false ], + ]; + } + + /** + * @dataProvider GetDefaultModulesProvider + */ + public function testProcessInstallationChoices($bInstallationOptionalChoicesChecked=false) { + $sPath = realpath(dirname(__FILE__, 6)."/datamodels/2.x/installation.xml"); + $this->assertTrue(is_file($sPath)); + $oInstallationFileService = new \InstallationFileService($sPath, 'production', [], $bInstallationOptionalChoicesChecked); + $oInstallationFileService->ProcessInstallationChoices(); + $aExpectedModules = [ + "itop-config-mgmt", + "itop-attachments", + "itop-profiles-itil", + "itop-welcome-itil", + "itop-tickets", + "itop-files-information", + "combodo-db-tools", + "itop-core-update", + "itop-hub-connector", + "itop-oauth-client", + "itop-datacenter-mgmt", + "itop-endusers-devices", + "itop-storage-mgmt", + "itop-virtualization-mgmt", + "itop-service-mgmt", + "itop-request-mgmt", + "itop-portal", + "itop-portal-base", + "itop-change-mgmt", + ]; + + $aExpectedUnselectedModules = [ + 'itop-change-mgmt-itil', + 'itop-incident-mgmt-itil', + 'itop-request-mgmt-itil', + 'itop-service-mgmt-provider', + ]; + + if ($bInstallationOptionalChoicesChecked){ + $aExpectedModules []= "itop-problem-mgmt"; + $aExpectedModules []= "itop-knownerror-mgmt"; + } else { + $aExpectedUnselectedModules []= "itop-problem-mgmt"; + $aExpectedUnselectedModules []= "itop-knownerror-mgmt"; + } + + sort($aExpectedModules); + $aModules = array_keys($oInstallationFileService->GetSelectedModules()); + sort($aModules); + + $this->assertEquals($aExpectedModules, $aModules); + + $aUnselectedModules = array_keys($oInstallationFileService->GetUnSelectedModules()); + sort($aExpectedUnselectedModules); + sort($aUnselectedModules); + $this->assertEquals($aExpectedUnselectedModules, $aUnselectedModules); + } + + /** + * @dataProvider GetDefaultModulesProvider + */ + public function testGetAllSelectedModules($bInstallationOptionalChoicesChecked=false) { + $sPath = realpath(dirname(__FILE__, 6)."/datamodels/2.x/installation.xml"); + $oInstallationFileService = new \InstallationFileService($sPath, 'production', [], $bInstallationOptionalChoicesChecked); + $oInstallationFileService->Init(); + + $aSelectedModules = $oInstallationFileService->GetSelectedModules(); + $aExpectedInstallationModules = [ + "itop-config-mgmt", + "itop-attachments", + "itop-profiles-itil", + "itop-welcome-itil", + "itop-tickets", + "itop-files-information", + "combodo-db-tools", + "itop-core-update", + "itop-hub-connector", + "itop-oauth-client", + "itop-datacenter-mgmt", + "itop-endusers-devices", + "itop-storage-mgmt", + "itop-virtualization-mgmt", + "itop-service-mgmt", + "itop-request-mgmt", + "itop-portal", + "itop-portal-base", + "itop-change-mgmt", + ]; + if ($bInstallationOptionalChoicesChecked){ + $aExpectedInstallationModules []= "itop-problem-mgmt"; + $aExpectedInstallationModules []= "itop-knownerror-mgmt"; + } + + $aExpectedAuthenticationModules = [ + 'authent-cas', + 'authent-external', + 'authent-ldap', + 'authent-local', + ]; + + $aUnvisibleModules = [ + 'itop-backup', + 'itop-config', + 'itop-sla-computation', + ]; + + $aAutoSelectedModules = [ + 'itop-bridge-virtualization-storage', + ]; + + $this->checkModuleList("installation.xml choices", $aExpectedInstallationModules, $aSelectedModules); + $this->checkModuleList("authentication category", $aExpectedAuthenticationModules, $aSelectedModules); + $this->checkModuleList("unvisible", $aUnvisibleModules, $aSelectedModules); + $this->checkModuleList("auto-select", $aAutoSelectedModules, $aSelectedModules); + $this->assertEquals([], $aSelectedModules, "there should be no more modules remaining apart from below lists"); + } + + private function GetSelectedItilExtensions(bool $coreExtensionIncluded, bool $bKnownMgtIncluded) : array { + $aExtensions = [ + 'itop-config-mgmt-datacenter', + 'itop-config-mgmt-end-user', + 'itop-config-mgmt-storage', + 'itop-config-mgmt-virtualization', + 'itop-service-mgmt-enterprise', + 'itop-ticket-mgmt-itil', + 'itop-ticket-mgmt-itil-user-request', + 'itop-ticket-mgmt-itil-incident', + 'itop-ticket-mgmt-itil-enhanced-portal', + 'itop-change-mgmt-itil', + ]; + + if ($coreExtensionIncluded){ + $aExtensions[]= 'itop-config-mgmt-core'; + } + + if ($bKnownMgtIncluded){ + $aExtensions[]= 'itop-kown-error-mgmt'; + } + + return $aExtensions; + + } + + public function ItilExtensionProvider() { + return [ + 'all itil extensions + INCLUDING known-error-mgt' => [ + 'aSelectedExtensions' => $this->GetSelectedItilExtensions(true, true), + 'bKnownMgtSelected' => true, + ], + 'all itil extensions WITHOUT known-error-mgt' => [ + 'aSelectedExtensions' => $this->GetSelectedItilExtensions(true, false), + 'bKnownMgtSelected' => false, + ], + 'all itil extensions WITHOUT core mandatory ones + INCLUDING known-error-mgt' => [ + 'aSelectedExtensions' => $this->GetSelectedItilExtensions(false, true), + 'bKnownMgtSelected' => true, + ], + 'all itil extensions WITHOUT core mandatory ones and WITHOUT known-error-mgt' => [ + 'aSelectedExtensions' => $this->GetSelectedItilExtensions(false, false), + 'bKnownMgtSelected' => false, + ], + ]; + } + + /** + * @dataProvider ItilExtensionProvider + */ + public function testGetAllSelectedModules_withItilExtensions(array $aSelectedExtensions, bool $bKnownMgtSelected) { + $sPath = realpath(dirname(__FILE__, 6)."/datamodels/2.x/installation.xml"); + $oInstallationFileService = new \InstallationFileService($sPath, 'production', $aSelectedExtensions); + $oInstallationFileService->Init(); + + $aSelectedModules = $oInstallationFileService->GetSelectedModules(); + $aExpectedInstallationModules = [ + "itop-config-mgmt", + "itop-attachments", + "itop-profiles-itil", + "itop-welcome-itil", + "itop-tickets", + "itop-files-information", + "combodo-db-tools", + "itop-core-update", + "itop-hub-connector", + "itop-oauth-client", + "itop-datacenter-mgmt", + "itop-endusers-devices", + "itop-storage-mgmt", + "itop-virtualization-mgmt", + "itop-service-mgmt", + "itop-request-mgmt-itil", + "itop-incident-mgmt-itil", + "itop-portal", + "itop-portal-base", + "itop-change-mgmt-itil", + "itop-full-itil", + ]; + if ($bKnownMgtSelected){ + $aExpectedInstallationModules []= "itop-knownerror-mgmt"; + } + + $aExpectedAuthenticationModules = [ + 'authent-cas', + 'authent-external', + 'authent-ldap', + 'authent-local', + ]; + + $aUnvisibleModules = [ + 'itop-backup', + 'itop-config', + 'itop-sla-computation', + ]; + + $aAutoSelectedModules = [ + 'itop-bridge-virtualization-storage', + ]; + + $this->checkModuleList("installation.xml choices", $aExpectedInstallationModules, $aSelectedModules); + $this->checkModuleList("authentication category", $aExpectedAuthenticationModules, $aSelectedModules); + $this->checkModuleList("unvisible", $aUnvisibleModules, $aSelectedModules); + $this->checkModuleList("auto-select", $aAutoSelectedModules, $aSelectedModules); + $this->assertEquals([], $aSelectedModules, "there should be no more modules remaining apart from below lists"); + } + + private function checkModuleList(string $sModuleCategory, array $aExpectedModuleList, array &$aSelectedModules) { + $aMissingModules = []; + + foreach ($aExpectedModuleList as $sModuleId){ + if (! array_key_exists($sModuleId, $aSelectedModules)){ + $aMissingModules[]=$sModuleId; + } else { + unset($aSelectedModules[$sModuleId]); + } + } + + $this->assertEquals([], $aMissingModules, "$sModuleCategory modules are missing"); + + } + + public function ProductionModulesProvider() { + return [ + 'module autoload as located in production-modules' => [ true ], + 'module not loaded' => [ false ], + ]; + } + + /** + * @dataProvider ProductionModulesProvider + */ + public function testGetAllSelectedModules_ProductionModules(bool $bModuleInProductionModulesFolder) { + $sModuleId = "itop-problem-mgmt"; + if ($bModuleInProductionModulesFolder){ + if (! is_dir(APPROOT."data/production-modules")){ + @mkdir(APPROOT."data/production-modules"); + } + + $this->RecurseMoveDir(APPROOT . "datamodels/2.x/$sModuleId", APPROOT."data/production-modules/$sModuleId"); + } + + $sPath = realpath(dirname(__FILE__, 6)."/datamodels/2.x/installation.xml"); + $oInstallationFileService = new \InstallationFileService($sPath, 'production', [], false); + $oInstallationFileService->Init(); + + $aSelectedModules = $oInstallationFileService->GetSelectedModules(); + $this->assertEquals($bModuleInProductionModulesFolder, array_key_exists($sModuleId, $aSelectedModules)); + } + + private function RecurseMoveDir($sFromDir, $sToDir) { + if (! is_dir($sFromDir)){ + return; + } + + if (! is_dir($sToDir)){ + @mkdir($sToDir); + } + + foreach (glob("$sFromDir/*") as $sPath){ + $sToPath = $sToDir.'/'.basename($sPath); + if (is_file($sPath)){ + @rename($sPath, $sToPath); + } else { + $this->RecurseMoveDir($sPath, $sToPath); + } + } + + @rmdir($sFromDir); + } +} diff --git a/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php b/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php index ee4bdd1d3c..cde7af01a4 100644 --- a/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php @@ -63,9 +63,12 @@ public function testCallUnattendedInstallFromHttp(){ } public function testCallUnattendedInstallFromCLI() { - $cliPath = realpath(APPROOT."/setup/unattended-install/unattended-install.php"); - $res = exec("php ".$cliPath); + $sCliPath = realpath(APPROOT."/setup/unattended-install/unattended-install.php"); + exec(sprintf("%s %s", PHP_BINARY, $sCliPath), $aOutput, $iCode); - $this->assertEquals("Param file `default-params.xml` doesn't exist ! Exiting...", $res); + $sOutput = implode('\n', $aOutput); + var_dump($sOutput); + $this->assertStringContainsString("Missing mandatory argument `--param-file`", $sOutput); + $this->assertEquals(255, $iCode); } } diff --git a/tests/setup_params/default-params.xml b/tests/setup_params/default-params.xml index 071916b9c7..dcaf309866 100644 --- a/tests/setup_params/default-params.xml +++ b/tests/setup_params/default-params.xml @@ -29,40 +29,8 @@ EN US - authent-cas - authent-external - authent-local - itop-backup - itop-config - itop-profiles-itil - itop-sla-computation - itop-tickets - itop-welcome-itil - itop-config-mgmt - itop-attachments - itop-datacenter-mgmt - itop-endusers-devices - itop-storage-mgmt - itop-virtualization-mgmt - itop-bridge-virtualization-storage - itop-service-mgmt - itop-request-mgmt - itop-portal - itop-portal-base - itop-change-mgmt - itop-knownerror-mgmt - itop-config-mgmt-core - itop-config-mgmt-datacenter - itop-config-mgmt-end-user - itop-config-mgmt-storage - itop-config-mgmt-virtualization - itop-service-mgmt-enterprise - itop-ticket-mgmt-simple-ticket - itop-ticket-mgmt-simple-ticket-enhanced-portal - itop-change-mgmt-simple - itop-kown-error-mgmt 1