/** * The standardized result of any pass/fail check performed by the setup * * @copyright Copyright (C) 2010-2018 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ class CheckResult { // Severity levels const ERROR = 0; const WARNING = 1; const INFO = 2; public $iSeverity; public $sLabel; public $sDescription; public function __construct($iSeverity, $sLabel, $sDescription = '') { $this->iSeverity = $iSeverity; $this->sLabel = $sLabel; $this->sDescription = $sDescription; } } /** * All of the functions/utilities needed by both the setup wizard and the installation process * * @copyright Copyright (C) 2010-2012 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ class SetupUtils { // -- Minimum versions (requirements : forbids installation if not met) const PHP_MIN_VERSION = '5.6.0'; // 5.6 will be supported until the end of 2018 (see http://php.net/supported-versions.php) const MYSQL_MIN_VERSION = '5.6.0'; // 5.6 to have fulltext on InnoDB for Tags fields (N°931) const MYSQL_NOT_VALIDATED_VERSION = ''; // MySQL 8 is now OK (N°2010 in 2.7.0) // -- versions that will be the minimum in next iTop major release (warning if not met) const PHP_NEXT_MIN_VERSION = ''; // no new PHP requirement for next iTop version const MYSQL_NEXT_MIN_VERSION = ''; // no new MySQL requirement for next iTop version // -- First recent version that is not yet validated by Combodo (warning) const PHP_NOT_VALIDATED_VERSION = '7.4.0'; const MIN_MEMORY_LIMIT = 33554432; // 32 * 1024 * 1024 - we can use expressions in const since PHP 5.6 but we are in the setup ! const SUHOSIN_GET_MAX_VALUE_LENGTH = 2048; /** * Check configuration parameters, for example : *
');
return false;
}
}
else if (bMandatory)
{
$("#v_"+sFieldId).html('
');
return false;
}
else
{
$("#v_"+sFieldId).html("");
return true;
}
}
}
EOF
);
$oPage->add_ready_script(
<<
On Windows, the backup won\'t work because database password contains %, ! or " character");');
}
else
{
$sTlsEnabled = (isset($aParameters['db_tls_enabled'])) ? $aParameters['db_tls_enabled'] : null;
$sTlsCA = (isset($aParameters['db_tls_ca'])) ? $aParameters['db_tls_ca'] : null;
$oPage->add_ready_script('oXHRCheckDB = null;');
$checks = SetupUtils::CheckDbServer($sDBServer, $sDBUser, $sDBPwd, $sTlsEnabled, $sTlsCA);
if ($checks === false)
{
// Connection failed, disable the "Next" button
$oPage->add_ready_script('$("#wiz_form").data("db_connection", "error");');
$oPage->add_ready_script('$("#db_info").html("
No connection to the database...");');
}
else
{
$aErrors = array();
$aWarnings = array();
foreach ($checks['checks'] as $oCheck)
{
if ($oCheck->iSeverity == CheckResult::ERROR)
{
$aErrors[] = $oCheck->sLabel;
}
else
{
if ($oCheck->iSeverity == CheckResult::WARNING)
{
$aWarnings[] = $oCheck->sLabel;
}
}
}
if (count($aErrors) > 0)
{
$oPage->add_ready_script('$("#wiz_form").data("db_connection", "error");');
$oPage->add_ready_script('$("#db_info").html(\'
Error: '.htmlentities(implode('
Warning: '.htmlentities(implode('
Database server connection Ok.\');');
}
}
if ($checks['databases'] == null)
{
$sDBNameInput = '';
$oPage->add_ready_script('$("#table_info").html(\'
Not enough rights to enumerate the databases\');');
}
else
{
$sDBNameInput = '';
}
$oPage->add_ready_script('$("#db_name_container").html("'.addslashes($sDBNameInput).'");');
$oPage->add_ready_script('$("#db_name").bind("click keyup change", function() { $("#existing_db").prop("checked", true); WizardUpdateButtons(); });');
}
}
$oPage->add_ready_script('WizardUpdateButtons();');
}
/**
* Helper function to get the available languages from the given directory
* @param $sDir String Path to the dictionary
* @return array of language code => description
*/
static public function GetAvailableLanguages($sDir)
{
require_once(APPROOT.'/core/coreexception.class.inc.php');
require_once(APPROOT.'/core/dict.class.inc.php');
$aFiles = scandir($sDir);
foreach($aFiles as $sFile)
{
if ($sFile == '.' || $sFile == '..' || $sFile == '.svn' || $sFile == '.git')
{
// Skip
continue;
}
$sFilePath = $sDir.'/'.$sFile;
if (is_file($sFilePath) && preg_match('/^.*dict.*\.php$/i', $sFilePath, $aMatches))
{
require_once($sFilePath);
}
}
return Dict::GetLanguages();
}
static public function GetLanguageSelect($sSourceDir, $sInputName, $sDefaultLanguageCode)
{
$sHtml = '';
return $sHtml;
}
/**
*
* @param $oWizard
* @param bool $bAbortOnMissingDependency ...
* @param array $aModulesToLoad List of modules to search for, defaults to all if ommitted
* @return hash
* @throws Exception
*/
public static function AnalyzeInstallation($oWizard, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
$oConfig = new Config();
$sSourceDir = $oWizard->GetParameter('source_dir', '');
if (strpos($sSourceDir, APPROOT) !== false)
{
$sRelativeSourceDir = str_replace(APPROOT, '', $sSourceDir);
}
else if (strpos($sSourceDir, $oWizard->GetParameter('previous_version_dir')) !== false)
{
$sRelativeSourceDir = str_replace($oWizard->GetParameter('previous_version_dir'), '', $sSourceDir);
}
else
{
throw(new Exception('Internal error: AnalyzeInstallation: source_dir is neither under APPROOT nor under previous_installation_dir ???'));
}
$aParamValues = array(
'db_server' => $oWizard->GetParameter('db_server', ''),
'db_user' => $oWizard->GetParameter('db_user', ''),
'db_pwd' => $oWizard->GetParameter('db_pwd', ''),
'db_name' => $oWizard->GetParameter('db_name', ''),
'db_prefix' => $oWizard->GetParameter('db_prefix', ''),
'db_tls_enabled' => $oWizard->GetParameter('db_tls_enabled', false),
'db_tls_ca' => $oWizard->GetParameter('db_tls_ca', ''),
'source_dir' => $sRelativeSourceDir,
);
$oConfig->UpdateFromParams($aParamValues, null);
$aDirsToScan = array($sSourceDir);
if (is_dir(APPROOT.'extensions'))
{
$aDirsToScan[] = APPROOT.'extensions';
}
if (is_dir($oWizard->GetParameter('copy_extensions_from')))
{
$aDirsToScan[] = $oWizard->GetParameter('copy_extensions_from');
}
$sExtraDir = APPROOT.'data/production-modules/';
if (is_dir($sExtraDir))
{
$aDirsToScan[] = $sExtraDir;
}
$oProductionEnv = new RunTimeEnvironment();
$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, $aDirsToScan, $bAbortOnMissingDependency, $aModulesToLoad);
foreach($aAvailableModules as $key => $aModule)
{
$bIsExtra = (array_key_exists('root_dir', $aModule) && (strpos($aModule['root_dir'], $sExtraDir) !== false)); // Some modules (root, datamodel) have no 'root_dir'
if ($bIsExtra)
{
// Modules in data/production-modules/ are considered as mandatory and always installed
$aAvailableModules[$key]['visible'] = false;
}
}
return $aAvailableModules;
}
/**
* @param WizardController $oWizard
*
* @return array|bool
*/
public static function GetApplicationVersion($oWizard)
{
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
$oConfig = new Config();
$aParamValues = array(
'db_server' => $oWizard->GetParameter('db_server', ''),
'db_user' => $oWizard->GetParameter('db_user', ''),
'db_pwd' => $oWizard->GetParameter('db_pwd', ''),
'db_name' => $oWizard->GetParameter('db_name', ''),
'db_prefix' => $oWizard->GetParameter('db_prefix', ''),
'db_tls_enabled' => $oWizard->GetParameter('db_tls_enabled', false),
'db_tls_ca' => $oWizard->GetParameter('db_tls_ca', ''),
'source_dir' => '',
);
$oConfig->UpdateFromParams($aParamValues, null);
$oProductionEnv = new RunTimeEnvironment();
return $oProductionEnv->GetApplicationVersion($oConfig);
}
/**
* Checks if the content of a directory matches the given manifest
* @param string $sBaseDir Path to the root directory of iTop
* @param string $sSourceDir Relative path to the directory to check under $sBaseDir
* @param $aManifest
* @param array $aExcludeNames
* @param Hash $aResult Used for recursion
* @return hash Hash array ('added' => array(), 'removed' => array(), 'modified' => array())
* @internal param array $aDOMManifest Array of array('path' => relative_path 'size'=> iSize, 'md5' => sHexMD5)
*/
public static function CheckDirAgainstManifest($sBaseDir, $sSourceDir, $aManifest, $aExcludeNames = array('.svn', '.git'), $aResult = null)
{
//echo "CheckDirAgainstManifest($sBaseDir, $sSourceDir ...)\n";
if ($aResult === null)
{
$aResult = array('added' => array(), 'removed' => array(), 'modified' => array());
}
if (substr($sSourceDir, 0, 1) == '/')
{
$sSourceDir = substr($sSourceDir, 1);
}
// Manifest limited to all the files supposed to be located in this directory
$aDirManifest = array();
foreach($aManifest as $aFileInfo)
{
$sDir = dirname($aFileInfo['path']);
if ($sDir == '.')
{
// Hmm... the file seems located at the root of iTop
$sDir = '';
}
if ($sDir == $sSourceDir)
{
$aDirManifest[basename($aFileInfo['path'])] = $aFileInfo;
}
}
//echo "The manifest contains ".count($aDirManifest)." files for the directory '$sSourceDir' (and below)\n";
// Read the content of the directory
foreach(glob($sBaseDir.'/'.$sSourceDir .'/*') as $sFilePath)
{
$sFile = basename($sFilePath);
//echo "Checking $sFile ($sFilePath)\n";
if (in_array(basename($sFile), $aExcludeNames)) continue;
if(is_dir($sFilePath))
{
$aResult = self::CheckDirAgainstManifest($sBaseDir, $sSourceDir.'/'.$sFile, $aManifest, $aExcludeNames, $aResult);
}
else
{
if (!array_key_exists($sFile, $aDirManifest))
{
//echo "New file ".$sFile." in $sSourceDir\n";
$aResult['added'][$sSourceDir.'/'.$sFile] = true;
}
else
{
$aStats = stat($sFilePath);
if ($aStats['size'] != $aDirManifest[$sFile]['size'])
{
// Different sizes
$aResult['modified'][$sSourceDir.'/'.$sFile] = 'Different sizes. Original size: '.$aDirManifest[$sFile]['size'].' bytes, actual file size on disk: '.$aStats['size'].' bytes.';
}
else
{
// Same size, compare the md5 signature
$sMD5 = md5_file($sFilePath);
if ($sMD5 != $aDirManifest[$sFile]['md5'])
{
$aResult['modified'][$sSourceDir.'/'.$sFile] = 'Content modified (MD5 checksums differ).';
//echo $sSourceDir.'/'.$sFile." modified ($sMD5 == {$aDirManifest[$sFile]['md5']})\n";
}
//else
//{
// echo $sSourceDir.'/'.$sFile." unmodified ($sMD5 == {$aDirManifest[$sFile]['md5']})\n";
//}
}
//echo "Removing ".$sFile." from aDirManifest\n";
unset($aDirManifest[$sFile]);
}
}
}
// What remains in the array are files that were deleted
foreach($aDirManifest as $sDeletedFile => $void)
{
$aResult['removed'][$sSourceDir.'/'.$sDeletedFile] = true;
}
return $aResult;
}
public static function CheckDataModelFiles($sManifestFile, $sBaseDir)
{
$oXML = simplexml_load_file($sManifestFile);
$aManifest = array();
foreach($oXML as $oFileInfo)
{
$aManifest[] = array('path' => (string)$oFileInfo->path, 'size' => (int)$oFileInfo->size, 'md5' => (string)$oFileInfo->md5);
}
$sBaseDir = preg_replace('|modules/?$|', '', $sBaseDir);
$aResults = self::CheckDirAgainstManifest($sBaseDir, 'modules', $aManifest);
// echo "Comparison of ".dirname($sBaseDir)."/modules against $sManifestFile:\n".print_r($aResults, true).""; return $aResults; } public static function CheckPortalFiles($sManifestFile, $sBaseDir) { $oXML = simplexml_load_file($sManifestFile); $aManifest = array(); foreach($oXML as $oFileInfo) { $aManifest[] = array('path' => (string)$oFileInfo->path, 'size' => (int)$oFileInfo->size, 'md5' => (string)$oFileInfo->md5); } $aResults = self::CheckDirAgainstManifest($sBaseDir, 'portal', $aManifest); // echo "
Comparison of ".dirname($sBaseDir)."/portal:\n".print_r($aResults, true).""; return $aResults; } public static function CheckApplicationFiles($sManifestFile, $sBaseDir) { $oXML = simplexml_load_file($sManifestFile); $aManifest = array(); foreach($oXML as $oFileInfo) { $aManifest[] = array('path' => (string)$oFileInfo->path, 'size' => (int)$oFileInfo->size, 'md5' => (string)$oFileInfo->md5); } $aResults = array('added' => array(), 'removed' => array(), 'modified' => array()); foreach(array('addons', 'core', 'dictionaries', 'js', 'application', 'css', 'pages', 'synchro', 'webservices') as $sDir) { $aTmp = self::CheckDirAgainstManifest($sBaseDir, $sDir, $aManifest); $aResults['added'] = array_merge($aResults['added'], $aTmp['added']); $aResults['modified'] = array_merge($aResults['modified'], $aTmp['modified']); $aResults['removed'] = array_merge($aResults['removed'], $aTmp['removed']); } // echo "
Comparison of ".dirname($sBaseDir)."/portal:\n".print_r($aResults, true).""; return $aResults; } /** * @param string $sInstalledVersion * @param string $sSourceDir * @return bool|hash * @throws Exception */ public static function CheckVersion($sInstalledVersion, $sSourceDir) { $sManifestFilePath = self::GetVersionManifest($sInstalledVersion); if ($sSourceDir != '') { if (file_exists($sManifestFilePath)) { $aDMchanges = self::CheckDataModelFiles($sManifestFilePath, $sSourceDir); //$aPortalChanges = self::CheckPortalFiles($sManifestFilePath, $sSourceDir); //$aCodeChanges = self::CheckApplicationFiles($sManifestFilePath, $sSourceDir); //echo("Changes detected compared to $sInstalledVersion:
".print_r($aDMchanges, true).""); //echo("Changes detected compared to $sInstalledVersion:
".print_r($aDMchanges, true)."
".print_r($aPortalChanges, true)."
".print_r($aCodeChanges, true).""); return $aDMchanges; } else { return false; } } else { throw(new Exception("Cannot check version '$sInstalledVersion', no source directory provided to check the files.")); } } public static function GetVersionManifest($sInstalledVersion) { if (preg_match('/^([0-9]+)\./', $sInstalledVersion, $aMatches)) { return APPROOT.'datamodels/'.$aMatches[1].'.x/manifest-'.$sInstalledVersion.'.xml'; } return false; } public static function CheckWritableDirs($aWritableDirs) { $aNonWritableDirs = array(); foreach($aWritableDirs as $sDir) { $sFullPath = APPROOT.$sDir; if (is_dir($sFullPath) && !is_writable($sFullPath)) { $aNonWritableDirs[APPROOT.$sDir] = new CheckResult(CheckResult::ERROR, "The directory '".APPROOT.$sDir."' exists but is not writable for the application."); } else if (file_exists($sFullPath) && !is_dir($sFullPath)) { $aNonWritableDirs[APPROOT.$sDir] = new CheckResult(CheckResult::ERROR, ITOP_APPLICATION." needs the directory '".APPROOT.$sDir."' to be writable. However file named '".APPROOT.$sDir."' already exists."); } else if (!is_dir($sFullPath) && !is_writable(APPROOT)) { $aNonWritableDirs[APPROOT.$sDir] = new CheckResult(CheckResult::ERROR, ITOP_APPLICATION." needs the directory '".APPROOT.$sDir."' to be writable. The directory '".APPROOT.$sDir."' does not exist and '".APPROOT."' is not writable, the application cannot create the directory '$sDir' inside it."); } } return $aNonWritableDirs; } public static function GetLatestDataModelDir() { $sBaseDir = APPROOT.'datamodels'; $aDirs = glob($sBaseDir.'/*', GLOB_MARK | GLOB_ONLYDIR); if ($aDirs !== false) { sort($aDirs); // Windows: there is a backslash at the end (though the path is made of slashes!!!) $sDir = basename(array_pop($aDirs)); $sRes = $sBaseDir.'/'.$sDir.'/'; return $sRes; } return false; } public static function GetCompatibleDataModelDir($sInstalledVersion) { if (preg_match('/^([0-9]+)\./', $sInstalledVersion, $aMatches)) { $sMajorVersion = $aMatches[1]; $sDir = APPROOT.'datamodels/'.$sMajorVersion.'.x/'; if (is_dir($sDir)) { return $sDir; } } return false; } static public function GetDataModelVersion($sDatamodelDir) { $sVersionFile = $sDatamodelDir.'version.xml'; if (file_exists($sVersionFile)) { $oParams = new XMLParameters($sVersionFile); return $oParams->Get('version'); } return false; } /** * Returns an array of xml nodes describing the licences. * @param $sEnv string|null Execution environment. If present loads licenses only for installed modules else loads all licenses available. * @return array Licenses list. */ static public function GetLicenses($sEnv = null) { $aLicenses = array(); $aLicenceFiles = glob(APPROOT.'setup/licenses/*.xml'); if (empty($sEnv)) { $aLicenceFiles = array_merge($aLicenceFiles, glob(APPROOT.'datamodels/*/*/license.*.xml')); $aLicenceFiles = array_merge($aLicenceFiles, glob(APPROOT.'extensions/*/license.*.xml')); $aLicenceFiles = array_merge($aLicenceFiles, glob(APPROOT.'data/*-modules/*/license.*.xml')); } else { $aLicenceFiles = array_merge($aLicenceFiles, glob(APPROOT.'env-'.$sEnv.'/*/license.*.xml')); } foreach ($aLicenceFiles as $sFile) { $oXml = simplexml_load_file($sFile); if (!empty($oXml->license)) { foreach ($oXml->license as $oLicense) { $aLicenses[(string)$oLicense->product] = $oLicense; } } } return $aLicenses; } /** * @return string path to the log file where the create and/or alter queries are written */ static public function GetSetupQueriesFilePath() { return APPROOT.'log/setup-queries-'.strftime('%Y-%m-%d_%H_%M').'.sql'; } public final static function EnterMaintenanceMode($oConfig) { @touch(MAINTENANCE_MODE_FILE); self::Log("----> Entering maintenance mode"); try { // Wait for cron to stop if (is_null($oConfig)) { return; } // Use mutex to check if cron is running $oMutex = new iTopMutex( 'cron'.$oConfig->Get('db_name').$oConfig->Get('db_subname'), $oConfig->Get('db_host'), $oConfig->Get('db_user'), $oConfig->Get('db_pwd'), $oConfig->Get('db_tls.enabled'), $oConfig->Get('db_tls.ca') ); $iCount = 1; $iStarted = time(); $iMaxDuration = $oConfig->Get('cron_max_execution_time'); $iTimeLimit = $iStarted + $iMaxDuration; while ($oMutex->IsLocked()) { self::Log("Waiting for cron to stop ($iCount)"); $iCount++; sleep(10); if (time() > $iTimeLimit) { throw new Exception("Cannot enter maintenance mode"); } } } catch(Exception $e) { // Ignore errors } } public final static function ExitMaintenanceMode($bLog = true) { @unlink(MAINTENANCE_MODE_FILE); if ($bLog) { self::Log("<---- Exiting maintenance mode"); } } /** * Create and store Setup authentication token * * @return string token */ public final static function CreateSetupToken() { if (!is_dir(APPROOT.'data')) { mkdir(APPROOT.'data'); } if (!is_dir(APPROOT.'data/setup')) { mkdir(APPROOT.'data/setup'); } $sUID = hash('sha256', rand()); file_put_contents(APPROOT.'data/setup/authent', $sUID); return $sUID; } /** * Verify Setup authentication token (from the request parameter 'authent') * * @throws \SecurityException */ public final static function CheckSetupToken() { $sAuthent = utils::ReadParam('authent', '', false, 'raw_data'); if (!file_exists(APPROOT.'data/setup/authent') || $sAuthent !== file_get_contents(APPROOT.'data/setup/authent')) { throw new SecurityException('Setup operations are not allowed outside of the setup'); } } private final static function Log($sText) { if (class_exists('SetupPage')) { SetupPage::log($sText); } else { IssueLog::Info($sText); } } } /** * Helper class to write rules (as PHP expressions) in the 'auto_select' field of the 'module' */ class SetupInfo { static $aSelectedModules = array(); /** * Called by the setup process to initializes the list of selected modules. Do not call this method * from an 'auto_select' rule * @param hash $aModules * @return void */ static function SetSelectedModules($aModules) { self::$aSelectedModules = $aModules; } /** * Returns true if a module is selected (as a consequence of the end-user's choices, * or because the module is hidden, or mandatory, or because of a previous auto_select rule) * @param string $sModuleId The identifier of the module (without the version number. Example: itop-config-mgmt) * @return boolean True if the module is already selected, false otherwise */ static function ModuleIsSelected($sModuleId) { return (array_key_exists($sModuleId, self::$aSelectedModules)); } }