diff --git a/datamodels/2.x/itop-backup/dbrestore.class.inc.php b/datamodels/2.x/itop-backup/dbrestore.class.inc.php index 35ce559af..d285e8cf2 100644 --- a/datamodels/2.x/itop-backup/dbrestore.class.inc.php +++ b/datamodels/2.x/itop-backup/dbrestore.class.inc.php @@ -187,6 +187,12 @@ class DBRestore extends DBBackup @chmod($sConfigFile, 0770); // Allow overwriting the file rename($sDataDir.'/config-itop.php', $sConfigFile); @chmod($sConfigFile, 0440); // Read-only + + $aExtraFiles = $this->ListExtraFiles($sDataDir); + foreach($aExtraFiles as $sSourceFilePath => $sDestinationFilePath) { + SetupUtils::builddir(dirname($sDestinationFilePath)); + rename($sSourceFilePath, $sDestinationFilePath); + } try { SetupUtils::rrmdir($sDataDir); @@ -211,4 +217,31 @@ class DBRestore extends DBBackup $oRestoreMutex->Unlock(); } } + + /** + * List the 'extra files' found in the decompressed archive + * (i.e. files other than config-itop.php, delta.xml, itop-dump.sql or production-modules/* + * @param string $sDataDir + * @return string[] + */ + protected function ListExtraFiles(string $sDataDir) + { + $aExtraFiles = []; + $aStandardFiles = ['config-itop.php', 'itop-dump.sql', 'production-modules', 'delta.xml']; + $oDirectoryIterator = new RecursiveDirectoryIterator($sDataDir, FilesystemIterator::CURRENT_AS_FILEINFO|FilesystemIterator::SKIP_DOTS); + $oIterator = new RecursiveIteratorIterator($oDirectoryIterator); + foreach ($oIterator as $oFileInfo) + { + if (in_array($oFileInfo->getFilename(), $aStandardFiles)) { + continue; + } + if (strncmp($oFileInfo->getPathname(), $sDataDir.'/production-modules', strlen($sDataDir.'/production-modules')) == 0) { + continue; + } + + $aExtraFiles[$oFileInfo->getPathname()] = APPROOT.substr($oFileInfo->getPathname(), strlen($sDataDir)); + } + + return $aExtraFiles; + } } diff --git a/setup/backup.class.inc.php b/setup/backup.class.inc.php index 2394f8de8..3d5817398 100644 --- a/setup/backup.class.inc.php +++ b/setup/backup.class.inc.php @@ -222,11 +222,12 @@ class DBBackup * * @param string $sSourceConfigFile * @param string $sTmpFolder + * @param bool $bSkipSQLDumpForTesting * * @return array list of files to archive * @throws \Exception */ - protected function PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder) + protected function PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder, $bSkipSQLDumpForTesting = false) { $aRet = array(); if (is_dir($sTmpFolder)) @@ -243,7 +244,7 @@ class DBBackup { $sFile = $sTmpFolder.'/config-itop.php'; $this->LogInfo("backup: adding resource '$sSourceConfigFile'"); - copy($sSourceConfigFile, $sFile); + @copy($sSourceConfigFile, $sFile); // During unattended install config file may be absent $aRet[] = $sFile; } @@ -264,9 +265,43 @@ class DBBackup SetupUtils::copydir($sExtraDir, $sFile); $aRet[] = $sFile; } - $sDataFile = $sTmpFolder.'/itop-dump.sql'; - $this->DoBackup($sDataFile); - $aRet[] = $sDataFile; + if (MetaModel::GetConfig() !== null) // During unattended install config file may be absent + { + $aExtraFiles = MetaModel::GetModuleSetting('itop-backup', 'extra_files', []); + foreach($aExtraFiles as $sExtraFileOrDir) + { + if(!file_exists(APPROOT.'/'.$sExtraFileOrDir)) { + continue; // Ignore non-existing files + } + + $sExtraFullPath = utils::RealPath(APPROOT.'/'.$sExtraFileOrDir, APPROOT); + if ($sExtraFullPath === false) + { + throw new Exception("Backup: Aborting, resource '$sExtraFileOrDir'. Considered as UNSAFE because not inside the iTop directory."); + } + if (is_dir($sExtraFullPath)) + { + $sFile = $sTmpFolder.'/'.$sExtraFileOrDir; + $this->LogInfo("backup: adding directory '$sExtraFileOrDir'"); + SetupUtils::copydir($sExtraFullPath, $sFile); + $aRet[] = $sFile; + } + elseif (file_exists($sExtraFullPath)) + { + $sFile = $sTmpFolder.'/'.$sExtraFileOrDir; + $this->LogInfo("backup: adding file '$sExtraFileOrDir'"); + @mkdir(dirname($sFile), 0755, true); + copy($sExtraFullPath, $sFile); + $aRet[] = $sFile; + } + } + } + if (!$bSkipSQLDumpForTesting) + { + $sDataFile = $sTmpFolder.'/itop-dump.sql'; + $this->DoBackup($sDataFile); + $aRet[] = $sDataFile; + } return $aRet; } diff --git a/tests/php-unit-tests/unitary-tests/setup/DBBackupDataTest.php b/tests/php-unit-tests/unitary-tests/setup/DBBackupDataTest.php new file mode 100644 index 000000000..02ec0734a --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/DBBackupDataTest.php @@ -0,0 +1,119 @@ +SetModuleSetting('itop-backup', 'extra_files', array_keys($aExtraFiles)); + + foreach($aExtraFiles as $sExtraFile => $bExists) + { + if ($bExists) + { + @mkdir(dirname(APPROOT.'/'.$sExtraFile), 0755, true); + file_put_contents(APPROOT.'/'.$sExtraFile, 'Hello World!'); + } + } + + if ($bUnsafeFileException) + { + $this->expectExceptionMessage("Backup: Aborting, resource '$sExtraFile'. Considered as UNSAFE because not inside the iTop directory."); + } + $aFiles = $this->InvokeNonPublicMethod('DBBackup', 'PrepareFilesToBackup', $oBackup, [APPROOT.'/conf/production/config-itop.php', $sTmpDir, true]); + SetupUtils::rrmdir($sTmpDir); + $aExpectedFiles = [ + $sTmpDir.'/config-itop.php', + ]; + foreach($aExtraFiles as $sRelFile => $bExists) + { + if ($bExists) + { + $aExpectedFiles[] = $sTmpDir.'/'.$sRelFile; + } + } + sort($aFiles); + sort($aExpectedFiles); + $this->assertEquals($aFiles, $aExpectedFiles); + + // Cleanup + foreach($aExtraFiles as $sExtraFile => $bExists) + { + if ($bExists) + { + unlink(APPROOT.'/'.$sExtraFile); + } + } + } + + function prepareFilesToBackupProvider() + { + return [ + 'no_extra_file' => ['aExtraFiles' => [], false], + 'one_extra_file' => ['aExtraFiles' => ['foo.txt' => true], false], + 'three_extra_file_and_dir' => ['aExtraFiles' => ['foo.txt' => true, 'gabu/zomeu.xml' => true, 'meuh.html' => true], false], + 'two_extra_file_but_only_one_exists' => ['aExtraFiles' => ['foo.txt' => true, 'meuh.html' => false], false], + 'one_unsafe_file' => ['aExtraFiles' => ['../foo.txt' => true], true], + ]; + } + + /** + * @dataProvider restoreListExtraFilesProvider + */ + function testRestoreListExtraFiles($aFilesToCreate, $aExpectedRelativeExtraFiles) + { + require_once(APPROOT.'/env-production/itop-backup/dbrestore.class.inc.php'); + + $sTmpDir = sys_get_temp_dir().'/testRestoreListExtraFiles-'.time(); + + foreach($aFilesToCreate as $sRelativeName) + { + $sDir = $sTmpDir.'/'.dirname($sRelativeName); + if(!is_dir($sDir)) + { + mkdir($sDir, 0755, true); + } + file_put_contents($sTmpDir.'/'.$sRelativeName, 'Hello world.'); + } + $aExpectedExtraFiles = []; + foreach($aExpectedRelativeExtraFiles as $sRelativeName) + { + $aExpectedExtraFiles[$sTmpDir.'/'.$sRelativeName] = APPROOT.'/'.$sRelativeName; + } + + $oRestore = new DBRestore(MetaModel::GetConfig()); + $aExtraFiles = $this->InvokeNonPublicMethod('DBRestore', 'ListExtraFiles', $oRestore, [$sTmpDir]); + + asort($aExtraFiles); + asort($aExpectedExtraFiles); + $this->assertEquals($aExpectedExtraFiles, $aExtraFiles); + SetupUtils::rrmdir($sTmpDir); + } + + function restoreListExtraFilesProvider() + { + return [ + 'no extra file' => ['aFilesToCreate' => ['config-itop.php', 'itop-dump.sql', 'delta.xml'], 'aExpectedExtraFiles' => []], + 'no extra file (2)' => ['aFilesToCreate' => ['config-itop.php', 'itop-dump.sql', 'delta.xml', 'production-modules/test/module.test.php'], 'aExpectedExtraFiles' => []], + 'one extra file' => ['aFilesToCreate' => ['config-itop.php', 'itop-dump.sql', 'delta.xml', 'production-modules/test/module.test.php', 'collectors/ldap/conf/params.local.xml'], 'aExpectedExtraFiles' => ['collectors/ldap/conf/params.local.xml']], + ]; + } + +}