*/
public static function GetBasePackageModules(array $aAnalyzeInstallationModules, string $sSourceDir): array
{
$aBasePackageModules = [];
$sNormalizedSourceDir = self::NormalizePathForComparison($sSourceDir);
if ($sNormalizedSourceDir === '') {
return $aBasePackageModules;
}
foreach ($aAnalyzeInstallationModules as $sModuleId => $aModuleInfo) {
if ($sModuleId === ROOT_MODULE) {
continue;
}
$sRootDir = $aModuleInfo['root_dir'] ?? '';
if ($sRootDir === '') {
continue;
}
$sModuleRootDir = self::NormalizePathForComparison($sRootDir);
if (utils::StartsWith($sModuleRootDir, $sNormalizedSourceDir)) {
$aBasePackageModules[$sModuleId] = true;
}
}
return $aBasePackageModules;
}
/**
* Returns true when all modules of a non-package extension are already included in base package modules.
*/
public static function IsIncludedInPackage(?iTopExtension $oExtension, array $aBasePackageModules): bool
{
if (($oExtension === null) || ($oExtension->sSource === iTopExtension::SOURCE_WIZARD)) {
return false;
}
$aModules = $oExtension->aModules ?? [];
if (!is_array($aModules) || empty($aModules)) {
return false;
}
foreach ($aModules as $sModuleId) {
if (!array_key_exists($sModuleId, $aBasePackageModules)) {
return false;
}
}
return true;
}
private static function NormalizePathForComparison(string $sPath): string
{
return rtrim(str_replace('\\', '/', $sPath), '/');
}
/**
* @param array $aModules List of available module codes
*
* @return bool true if the Hub connector is installed
*
* @since 2.7.8 3.0.3 3.1.0 N°5758 method creation
*/
public static function IsConnectableToITopHub($aModules)
{
return array_key_exists('itop-hub-connector', $aModules);
}
/**
* @param array $aModules Available modules with code as key and metadata array as values
* Same structure as the one returned by {@link \RunTimeEnvironment::AnalyzeInstallation}
* @param string $sExtensionsDir In the setup, get value with the 'extensions_dir' parameter
*
* @return string Error message if has manually installed modules, empty string otherwise
*
* @since 2.7.0 N°2533
*/
public static function CheckManualInstallDirEmpty($aModules, $sExtensionsDir = 'extensions')
{
if (!static::IsProductVersion($aModules)) {
return '';
}
$sManualInstallModulesFullPath = APPROOT.$sExtensionsDir.DIRECTORY_SEPARATOR;
//simple test in order to prevent install iTop pro with module in extension folder
$aFileInfo = scandir($sManualInstallModulesFullPath);
foreach ($aFileInfo as $sFolder) {
if ($sFolder != "." && $sFolder != ".." && is_dir($sManualInstallModulesFullPath.$sFolder) === true) {
return "Some modules are present in the '$sExtensionsDir' directory, this is not allowed when using ".ITOP_APPLICATION;
}
}
return '';
}
/**
* 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 array $aResult Used for recursion
* @return array 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 = ['.svn', '.git'], $aResult = null)
{
//echo "CheckDirAgainstManifest($sBaseDir, $sSourceDir ...)\n";
if ($aResult === null) {
$aResult = ['added' => [], 'removed' => [], 'modified' => []];
}
if (substr($sSourceDir, 0, 1) == '/') {
$sSourceDir = substr($sSourceDir, 1);
}
// Manifest limited to all the files supposed to be located in this directory
$aDirManifest = [];
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 = [];
foreach ($oXML as $oFileInfo) {
$aManifest[] = ['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 = [];
foreach ($oXML as $oFileInfo) {
$aManifest[] = ['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 = [];
foreach ($oXML as $oFileInfo) {
$aManifest[] = ['path' => (string)$oFileInfo->path, 'size' => (int)$oFileInfo->size, 'md5' => (string)$oFileInfo->md5];
}
$aResults = ['added' => [], 'removed' => [], 'modified' => []];
foreach (['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|array
* @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:
DataModel:
".print_r($aDMchanges, true)."
");
//echo("Changes detected compared to $sInstalledVersion:
DataModel:
".print_r($aDMchanges, true)."
Portal:
".print_r($aPortalChanges, true)."
Code:
".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;
}
/**
* Check paths relative to APPROOT : is existing, is dir, is writable
*
* @param string[] $aWritableDirs list of dirs to check, relative to APPROOT (for example : `['log','conf','data']`)
*
* @return array full path as key, CheckResult error as value
*
* @uses \is_dir()
* @uses \is_writable()
* @uses \file_exists()
*/
public static function CheckWritableDirs($aWritableDirs)
{
$aNonWritableDirs = [];
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.");
} elseif (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.");
} elseif (!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 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.
*/
public static function GetLicenses($sEnv = null)
{
$aLicenses = [];
$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', GLOB_BRACE));
$aLicenceFiles = array_merge($aLicenceFiles, glob(utils::GetDataPath().'*-modules/{*,*/*}/license.*.xml', GLOB_BRACE));
} 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
*/
public static function GetSetupQueriesFilePath()
{
return APPROOT.'log/setup-queries-'.date('Y-m-d_H_i').'.sql';
}
/**
* @param $oConfig
*
* @return bool
* @since 3.0.0 returns true if the app. was already in maintenance mode, false otherwise
*/
public static function EnterMaintenanceMode($oConfig): bool
{
$bPreviousMode = self::IsInMaintenanceMode();
@touch(MAINTENANCE_MODE_FILE);
SetupLog::Info("----> Entering maintenance mode");
self::WaitCronTermination($oConfig, "maintenance");
return $bPreviousMode;
}
public static function ExitMaintenanceMode($bLog = true)
{
@unlink(MAINTENANCE_MODE_FILE);
if ($bLog) {
SetupLog::Info("<---- Exiting maintenance mode");
}
}
public static function IsInMaintenanceMode()
{
return file_exists(MAINTENANCE_MODE_FILE);
}
public static function EnterReadOnlyMode($oConfig): bool
{
$bPreviousMode = self::IsInReadOnlyMode();
@touch(READONLY_MODE_FILE);
SetupLog::Info("----> Entering read only mode");
self::WaitCronTermination($oConfig, "read only");
return $bPreviousMode;
}
public static function ExitReadOnlyMode($bLog = true)
{
@unlink(READONLY_MODE_FILE);
if ($bLog) {
SetupLog::Info("<---- Exiting read only mode");
}
}
public static function IsInReadOnlyMode()
{
return file_exists(READONLY_MODE_FILE);
}
/**
* @param Config $oConfig
* @param string $sMode
*/
private static function WaitCronTermination($oConfig, $sMode)
{
try {
// Wait for cron to stop
if (is_null($oConfig) || ContextTag::Check(ContextTag::TAG_CRON)) {
return;
}
// Use mutex to check if cron is running
$oMutex = self::GetCronMutex($oConfig);
$iCount = 1;
$iStarted = time();
$iMaxDuration = $oConfig->Get('cron_max_execution_time');
$iTimeLimit = $iStarted + $iMaxDuration;
while ($oMutex->IsLocked()) {
SetupLog::Info("Waiting for cron to stop ($iCount)");
$iCount++;
sleep(1);
if (time() > $iTimeLimit) {
throw new Exception("Cannot enter $sMode mode, consider stopping the cron temporarily");
}
}
} catch (Exception $e) {
// Ignore errors
}
}
/**
* @param \Config $oConfig
*
* @return \iTopMutex
* @since 3.3.0
*/
public static function GetCronMutex(Config $oConfig): iTopMutex
{
$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')
);
return $oMutex;
}
/**
* Create and store Setup authentication token
*
* @return string token
* @since 2.6.5 2.7.0 N°3952
*/
final public static function CreateSetupToken()
{
if (!is_dir(APPROOT.'data')) {
mkdir(APPROOT.'data');
}
if (!is_dir(utils::GetDataPath().'setup')) {
mkdir(utils::GetDataPath().'setup');
}
$sUID = hash('sha256', rand());
file_put_contents(utils::GetDataPath().'setup/authent', $sUID);
Session::Set('setup_token', $sUID);
return $sUID;
}
/**
* Verify Setup authentication token (from the request parameter 'authent')
*
* @param bool $bRemoveToken
*
* @throws \SecurityException
* @since 2.6.5 2.7.0 N°3952
*/
final public static function CheckSetupToken($bRemoveToken = false)
{
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
$sTokenFile = utils::GetDataPath().'setup/authent';
if (!file_exists($sTokenFile) || $sAuthent !== file_get_contents($sTokenFile)) {
throw new SecurityException('Setup operations are not allowed outside of the setup');
}
if ($bRemoveToken) {
@unlink($sTokenFile);
}
}
/**
* Check setup transaction and create a new one if necessary
*
* @return bool
* @since 2.6.5 2.7.5 3.0.0 N°3952
*/
public static function IsSessionSetupTokenValid()
{
if (Session::IsSet('setup_token')) {
$sAuth = Session::Get('setup_token');
$sTokenFile = utils::GetDataPath().'setup/authent';
if (file_exists($sTokenFile) && $sAuth === file_get_contents($sTokenFile)) {
return true;
}
}
return false;
}
/**
* @since 2.6.5 2.7.5 3.0.0 N°3952
*/
public static function EraseSetupToken()
{
$sTokenFile = utils::GetDataPath().'setup/authent';
if (is_file($sTokenFile)) {
unlink($sTokenFile);
}
Session::Unset('setup_token');
}
/**
* @return string[]
*/
public static function GetPHPMandatoryExtensions()
{
return [
'mysqli',
'iconv',
'simplexml',
'soap',
'hash',
'json',
'session',
'pcre',
'dom',
'zlib',
'zip',
'fileinfo', // N°3123 if disabled, will throw "wrong format" when uploading AttributeImage
'mbstring', // N°2891, N°2899
'gd', // test image type (always returns false if not installed), image resizing, PDF export
'curl', // N°5270 Needed for one of authent-cas dependencies
];
}
/**
* @return array
*/
public static function GetPHPOptionalExtensions()
{
$aOptionalExtensions = [
'mcrypt, sodium or openssl' => [
'mcrypt' => 'Strong encryption will not be used.',
'sodium' => 'Strong encryption will not be used.',
'openssl' => 'Strong encryption will not be used.',
],
'apcu' => 'Performances will be slightly degraded.',
'ldap' => 'LDAP authentication will be disabled.',
];
if (utils::IsDevelopmentEnvironment()) {
$aOptionalExtensions['xdebug'] = 'For debugging';
}
return $aOptionalExtensions;
}
public static function GetBackButtonInfo($sReturnApplication): array
{
$sButtonUrl = '';
$sButtonLabel = '';
if ($sReturnApplication !== '') {
switch ($sReturnApplication) {
case 'DataFeatureRemoval':
$sButtonUrl = utils::GetAbsoluteUrlModulePage('combodo-data-feature-removal', 'index.php');
$sButtonLabel = 'Back to application';
break;
case 'designer':
$sButtonUrl = utils::GetAbsoluteUrlModulePage('itsm-designer-connector', 'launch.php');
$sButtonLabel = 'Back to Designer';
break;
case 'hub':
$sButtonUrl = utils::GetAbsoluteUrlModulePage('itop-hub-connector', 'launch.php');
$sButtonLabel = 'Back to hub';
break;
default:
break;
}
}
return [$sButtonLabel, $sButtonUrl];
}
}
/**
* Helper class to write rules (as PHP expressions) in the 'auto_select' field of the 'module'
*/
class SetupInfo
{
public static $aSelectedModules = [];
/**
* Called by the setup process to initializes the list of selected modules. Do not call this method
* from an 'auto_select' rule
* @param array $aModules
* @return void
*/
public 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
*/
public static function ModuleIsSelected($sModuleId)
{
return (array_key_exists($sModuleId, self::$aSelectedModules));
}
}