diff --git a/datamodels/2.x/itop-backup/ajax.backup.php b/datamodels/2.x/itop-backup/ajax.backup.php index d6049a480..701c97a8b 100644 --- a/datamodels/2.x/itop-backup/ajax.backup.php +++ b/datamodels/2.x/itop-backup/ajax.backup.php @@ -1,5 +1,5 @@ RestoreFromZip($sBackupFile, $sEnvironment); + $sRes = $oDBRS->RestoreFromCompressedBackup($sBackupFile, $sEnvironment); IssueLog::Info('Backup Restore - Done, releasing the LOCK'); $oRestoreMutex->Unlock(); diff --git a/datamodels/2.x/itop-backup/backup.php b/datamodels/2.x/itop-backup/backup.php index 4b731b3de..9b8ad2d96 100644 --- a/datamodels/2.x/itop-backup/backup.php +++ b/datamodels/2.x/itop-backup/backup.php @@ -1,5 +1,5 @@ SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '')); $sBackupFile = $oBackup->MakeName($sBackupFile); -$sZipArchiveFile = $sBackupFile.'.zip'; $bSimulate = utils::ReadParam('simulate', false, true); $res = false; if ($bSimulate) { - $oP->p("Simulate: would create file '$sZipArchiveFile'"); + $oP->p("Simulate: would create file '$sBackupFile'"); } elseif (MetaModel::GetConfig()->Get('demo_mode')) { @@ -190,11 +189,10 @@ elseif (MetaModel::GetConfig()->Get('demo_mode')) } else { - $oBackup->CreateZip($sZipArchiveFile); + $oBackup->CreateCompressedBackup($sBackupFile); } if ($res && $bDownloadBackup) { - $oBackup->DownloadBackup($sZipArchiveFile); + $oBackup->DownloadBackup($sBackupFile); } $oP->output(); -?> diff --git a/datamodels/2.x/itop-backup/check-backup.php b/datamodels/2.x/itop-backup/check-backup.php index 9ab638d73..88615f83b 100644 --- a/datamodels/2.x/itop-backup/check-backup.php +++ b/datamodels/2.x/itop-backup/check-backup.php @@ -1,5 +1,5 @@ LogInfo("Starting restore of ".basename($sZipFile)); + $this->RestoreFromCompressedBackup($sZipFile, $sEnvironment); + } - $oZip = new ZipArchiveEx(); - $res = $oZip->open($sZipFile); + /** + * @param $sFile A file with the extension .zip or .tar.gz + * @param string $sEnvironment Target environment + */ + public function RestoreFromCompressedBackup($sFile, $sEnvironment = 'production') + { + $this->LogInfo("Starting restore of ".basename($sFile)); + + $sNormalizedFile = strtolower(basename($sFile)); + if (substr($sNormalizedFile, -4) == '.zip') + { + $this->LogInfo('zip file detected'); + $oArchive = new ZipArchiveEx(); + $res = $oArchive->open($sFile); + } + elseif (substr($sNormalizedFile, -7) == '.tar.gz') + { + $this->LogInfo('tar.gz file detected'); + $oArchive = new TarGzArchive($sFile); + } + else + { + throw new Exception('Unsupported format for a backup file: '.$sFile); + } // Load the database // $sDataDir = tempnam(SetupUtils::GetTmpDir(), 'itop-'); unlink($sDataDir); // I need a directory, not a file... SetupUtils::builddir($sDataDir); // Here is the directory - $oZip->extractTo($sDataDir, 'itop-dump.sql'); + $oArchive->extractFileTo($sDataDir, 'itop-dump.sql'); $sDataFile = $sDataDir.'/itop-dump.sql'; $this->LoadDatabase($sDataFile); unlink($sDataFile); @@ -108,10 +136,10 @@ class DBRestore extends DBBackup // Update the code // $sDeltaFile = APPROOT.'data/'.$sEnvironment.'.delta.xml'; - if ($oZip->locateName('delta.xml') !== false) + if ($oArchive->hasFile('delta.xml') !== false) { // Extract and rename delta.xml => .delta.xml; - file_put_contents($sDeltaFile, $oZip->getFromName('delta.xml')); + file_put_contents($sDeltaFile, $oArchive->getFromName('delta.xml')); } else { @@ -121,18 +149,17 @@ class DBRestore extends DBBackup { SetupUtils::rrmdir(APPROOT.'data/production-modules/'); } - if ($oZip->locateName('production-modules/') !== false) + if ($oArchive->hasDir('production-modules/') !== false) { - $oZip->extractDirTo(APPROOT.'data/', 'production-modules/'); + $oArchive->extractDirTo(APPROOT.'data/', 'production-modules/'); } $sConfigFile = APPROOT.'conf/'.$sEnvironment.'/config-itop.php'; @chmod($sConfigFile, 0770); // Allow overwriting the file - $oZip->extractTo(APPROOT.'conf/'.$sEnvironment, 'config-itop.php'); + $oArchive->extractFileTo(APPROOT.'conf/'.$sEnvironment, 'config-itop.php'); @chmod($sConfigFile, 0444); // Read-only $oEnvironment = new RunTimeEnvironment($sEnvironment); $oEnvironment->CompileFrom($sEnvironment); } } - diff --git a/datamodels/2.x/itop-backup/main.itop-backup.php b/datamodels/2.x/itop-backup/main.itop-backup.php index 72f4cc166..d9eaf7e0e 100644 --- a/datamodels/2.x/itop-backup/main.itop-backup.php +++ b/datamodels/2.x/itop-backup/main.itop-backup.php @@ -1,5 +1,5 @@ SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '')); - $sBackupFile = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'file_name_format', '__DB__-%Y-%m-%d_%H_%M'); - $sName = $oBackup->MakeName($sBackupFile); + $sBackupFileFormat = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'file_name_format', '__DB__-%Y-%m-%d_%H_%M'); + $sName = $oBackup->MakeName($sBackupFileFormat); if ($sName == '') { $sName = $oBackup->MakeName(BACKUP_DEFAULT_FORMAT); } - $sZipFile = $this->sBackupDir.$sName.'.zip'; + $sBackupFile = $this->sBackupDir.$sName; $sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE; - $oBackup->CreateZip($sZipFile, $sSourceConfigFile); + $oBackup->CreateCompressedBackup($sBackupFile, $sSourceConfigFile); } catch (Exception $e) { @@ -172,7 +179,7 @@ class BackupExec implements iScheduledProcess throw $e; } $oMutex->Unlock(); - return "Created the backup: $sZipFile"; + return "Created the backup: $sBackupFile"; } /* diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index 2d313cb05..e2de6f5cb 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -1,5 +1,5 @@ oParams->Get('preinstall'); - // __DB__-%Y-%m-%d.zip + // __DB__-%Y-%m-%d $sDestination = $aPreinstall['backup']['destination']; $sSourceConfigFile = $aPreinstall['backup']['configuration_file']; $aDBParams = $this->oParams->Get('database'); @@ -412,11 +412,11 @@ class ApplicationInstaller return $sReport; } - protected static function DoBackup($sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sBackupFile, $sSourceConfigFile) + protected static function DoBackup($sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sBackupFileFormat, $sSourceConfigFile) { $oBackup = new SetupDBBackup($sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix); - $sZipFile = $oBackup->MakeName($sBackupFile); - $oBackup->CreateZip($sZipFile, $sSourceConfigFile); + $sTargetFile = $oBackup->MakeName($sBackupFileFormat); + $oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile); } diff --git a/setup/backup.class.inc.php b/setup/backup.class.inc.php index 5cf104c9d..cf1b6990c 100644 --- a/setup/backup.class.inc.php +++ b/setup/backup.class.inc.php @@ -15,12 +15,54 @@ // // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see -/** - * Handles adding directories into a Zip archive - */ + +interface BackupArchive +{ + /** + * @param string $sFile + * @return bool TRUE if the file is present, FALSE otherwise. + */ + public function hasFile($sFile); + + /** + * @param string $sDirectory + * @return bool TRUE if the directory is present, FALSE otherwise. + */ + public function hasDir($sDirectory); + + /** + * @param string $sDestinationDir + * @param string $sArchiveFile + * @return bool TRUE on success or FALSE on failure. + */ + public function extractFileTo($sDestinationDir, $sArchiveFile); + + /** + * Extract a whole directory from the archive. + * Usage: $oArchive->extractDirTo('/var/www/html/itop/data', '/production-modules/') + * @param string $sDestinationDir + * @param string $sArchiveDir Note: must start and end with a slash !! + * @return bool TRUE on success or FALSE on failure. + */ + public function extractDirTo($sDestinationDir, $sArchiveDir); + + /** + * Returns the entry contents using its name + * @param string $name Name of the entry + * @param int $length [optional] The length to be read from the entry. If 0, then the entire entry is read. + * @param int $flags [optional] The flags to use to open the archive. the following values may be ORed to it. ZipArchive::FL_UNCHANGED + * @return string the contents of the entry on success or FALSE on failure. + */ + public function getFromName($name, $length = 0, $flags = null); +} + if (class_exists('ZipArchive')) // The setup must be able to start even if the "zip" extension is not loaded { - class ZipArchiveEx extends ZipArchive + /** + * Handles adding directories into a Zip archive, and a unified API for archive read + * suggested enhancement: refactor the API for writing as well + */ + class ZipArchiveEx extends ZipArchive implements BackupArchive { public function addDir($sDir, $sZipDir = '') { @@ -52,6 +94,34 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the " } } } + /** + * @param string $sFile + * @return bool TRUE if the file is present, FALSE otherwise. + */ + public function hasFile($sFile) + { + return ($this->locateName($sFile) !== false); + } + + /** + * @param string $sDirectory + * @return bool TRUE if the directory is present, FALSE otherwise. + */ + public function hasDir($sDirectory) + { + return ($this->locateName($sDirectory) !== false); + } + + /** + * @param string $sDestinationDir + * @param string $sArchiveFile + * @return bool TRUE on success or FALSE on failure. + */ + public function extractFileTo($sDestinationDir, $sArchiveFile) + { + return $this->extractTo($sDestinationDir, $sArchiveFile); + } + /** * Extract a whole directory from the archive. * Usage: $oZip->extractDirTo('/var/www/html/itop/data', '/production-modules/') @@ -72,7 +142,7 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the " $aFiles[] = $sEntry; } } - // Extract only the selcted files + // Extract only the selected files if ((count($aFiles) > 0) && ($this->extractTo($sDestinationDir, $aFiles) === true)) { return true; @@ -169,58 +239,170 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the " $sFileName = strftime($sFileName); return $sFileName; } - + + /** + * @deprecated 2.4.0 Zip files are limited to 4 Gb, use CreateCompressedBackup to create tar.gz files + * @param $sZipFile + * @param null $sSourceConfigFile + */ public function CreateZip($sZipFile, $sSourceConfigFile = null) { + $aContents = array(); + // Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS) // (delete it before spawning a process) $sDataFile = tempnam(SetupUtils::GetTmpDir(), 'itop-'); $this->LogInfo("Data file: '$sDataFile'"); - - $aContents = array(); + $this->DoBackup($sDataFile); $aContents[] = array( 'source' => $sDataFile, 'dest' => 'itop-dump.sql', ); - + + foreach ($this->GetAdditionalFiles($sSourceConfigFile) as $sArchiveFile => $sSourceFile) + { + $aContents[] = array( + 'source' => $sSourceFile, + 'dest' => $sArchiveFile, + ); + } + + $this->DoZip($aContents, $sZipFile); + + // Windows/IIS: the data file has been created by the spawned process... + // trying to delete it will issue a warning, itself stopping the setup abruptely + @unlink($sDataFile); + } + + /** + * @param string $sTargetFile Path and name, without the extension + * @param string|null $sSourceConfigFile Configuration file to embed into the backup, if not the current one + */ + public function CreateCompressedBackup($sTargetFile, $sSourceConfigFile = null) + { + $this->LogInfo("Creating backup: '$sTargetFile.tar.gz'"); + + // Note: PharData::compress strips averything after the first dot found in the name of the tar, then it adds .tar.gz + // Hence, we have to create our own file in the target directory, and rename it when the process is complete + $sTarFile = str_replace('.', '_', $sTargetFile).'.tar'; + $this->LogInfo("Tar file: '$sTarFile'"); + $oArchive = new PharData($sTarFile); + + // Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS) + // (delete it before spawning a process) + // Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS) + // (delete it before spawning a process) + $sDataFile = tempnam(SetupUtils::GetTmpDir(), 'itop-'); + $this->LogInfo("Data file: '$sDataFile'"); + $this->DoBackup($sDataFile); + + $oArchive->addFile($sDataFile, 'itop-dump.sql'); + // todo: reduce disk space needed by the operation by piping the output of mysqldump directly into the tar + // tip1 : this syntax works fine (did not work with addFile) + //$oArchive->buildFromIterator( + // new ArrayIterator( + // array('production.delta.xml' => fopen(ROOTDIR.'production.delta.xml', 'rb')) + // ) + //); + // tip2 : use the phar stream by redirecting the output of mysqldump into + // phar://var/www/itop/data/backups/manual/trunk_pro-2017-07-05_15_10.tar.gz/itop-dump.sql + // + // new ArrayIterator( + // array('production.delta.xml' => fopen(ROOTDIR.'production.delta.xml', 'rb')) + // ) + //); + + // Windows/IIS: the data file has been created by the spawned process... + // trying to delete it will issue a warning, itself stopping the setup abruptely + @unlink($sDataFile); + + foreach ($this->GetAdditionalFiles($sSourceConfigFile) as $sArchiveFile => $sSourceFile) + { + if (is_dir($sSourceFile)) + { + $this->LogInfo("Adding directory into tar file: '$sSourceFile', recorded as '$sArchiveFile'"); + // Note: Phar::buildFromDirectory does not allow to specify a destination subdirectory + // Hence we have to add all files one by one + $sSourceDir = realpath($sSourceFile); + $sArchiveDir = trim($sArchiveFile, '/'); + + $oDirectoryIterator = new RecursiveDirectoryIterator($sSourceDir, RecursiveDirectoryIterator::SKIP_DOTS); + $oAllFiles = new RecursiveIteratorIterator($oDirectoryIterator); + foreach ($oAllFiles as $oSomeFile) + { + if ($oSomeFile->isDir()) continue; + + // Replace the local path by the archive path - the resulting string starts with a '/' + $sRelativePathName = substr($oSomeFile->getRealPath(), strlen($sSourceDir)); + // Under Windows realpath gives a mix of backslashes and slashes + $sRelativePathName = str_replace('\\', '/', $sRelativePathName); + $sArchiveFile = $sArchiveDir.$sRelativePathName; + $oArchive->addFile($oSomeFile->getPathName(), $sArchiveFile); + } + } + else + { + $this->LogInfo("Adding file into tar file: '$sSourceFile', recorded as '$sArchiveFile'"); + $oArchive->addFile($sSourceFile, $sArchiveFile); + }; + } + + if (file_exists($sTarFile.'.gz')) + { + // Prevent the gzip compression from failing -> the whole operation is an overwrite + $this->LogInfo("Overwriting tar.gz: '$sTarFile'"); + unlink($sTarFile.'.gz'); + } + // zlib is a must! + $oArchive->compress(Phar::GZ); + + // Cleanup + unset($oArchive); + unlink($sTarFile); + + if ($sTargetFile != $sTarFile) + { + // Give the file the expected name + if (file_exists($sTargetFile.'.gz')) + { + // Remove it -> the whole operation is an overwrite + $this->LogInfo("Overwriting tar.gz: '$sTargetFile'"); + unlink($sTargetFile.'.gz'); + } + rename($sTarFile.'.gz', $sTargetFile.'.tar.gz'); + } + } + + /** + * List files to store into the archive, in addition to the SQL dump + * @return array of sArchiveName => sFilePath + */ + protected function GetAdditionalFiles($sSourceConfigFile) + { + $aRet = array(); if (is_null($sSourceConfigFile)) { $sSourceConfigFile = MetaModel::GetConfig()->GetLoadedFile(); } if (!empty($sSourceConfigFile)) { - $aContents[] = array( - 'source' => $sSourceConfigFile, - 'dest' => 'config-itop.php', - ); + $aRet['config-itop.php'] = $sSourceConfigFile; } - - $this->DoBackup($sDataFile); - + $sDeltaFile = APPROOT.'data/'.utils::GetCurrentEnvironment().'.delta.xml'; if (file_exists($sDeltaFile)) { - $aContents[] = array( - 'source' => $sDeltaFile, - 'dest' => 'delta.xml', - ); + $aRet['delta.xml'] = $sDeltaFile; } $sExtraDir = APPROOT.'data/'.utils::GetCurrentEnvironment().'-modules/'; if (is_dir($sExtraDir)) { - $aContents[] = array( - 'source' => $sExtraDir, - 'dest' => utils::GetCurrentEnvironment().'-modules/', - ); + $sModules = utils::GetCurrentEnvironment().'-modules/'; + $aRet[$sModules] = $sExtraDir; } - - $this->DoZip($aContents, $sZipFile); - // Windows/IIS: the data file has been created by the spawned process... - // trying to delete it will issue a warning, itself stopping the setup abruptely - @unlink($sDataFile); + return $aRet; } - - + protected static function EscapeShellArg($sValue) { // Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation @@ -324,7 +506,7 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the " } /** - * Helper to create a ZIP out of a data file and the configuration file + * Helper to create a ZIP out of several files */ protected function DoZip($aFiles, $sZipArchiveFile) { @@ -439,3 +621,169 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the " } } } + +class TarGzArchive implements BackupArchive +{ + /* + * @var PharData + */ + protected $oPharArchive; + /* + * string[] + */ + protected $aFiles = null; + + public function __construct($sFile) + { + $this->oPharArchive = new PharData($sFile); + } + + /** + * @param string $sFile + * @return bool TRUE if the file is present, FALSE otherwise. + */ + public function hasFile($sFile) + { + return $this->oPharArchive->offsetExists($sFile); + } + + /** + * @param string $sDirectory + * @return bool TRUE if the directory is present, FALSE otherwise. + */ + public function hasDir($sDirectory) + { + return $this->oPharArchive->offsetExists($sDirectory); + } + + /** + * @param string $sDestinationDir + * @param string $sArchiveFile + * @return bool TRUE on success or FALSE on failure. + */ + public function extractFileTo($sDestinationDir, $sArchiveFile) + { + return $this->oPharArchive->extractTo($sDestinationDir, $sArchiveFile, true); + } + + /** + * Extract a whole directory from the archive. + * Usage: $oArchive->extractDirTo('/var/www/html/itop/data', '/production-modules/') + * @param string $sDestinationDir + * @param string $sArchiveDir Note: must start and end with a slash !! + * @return bool TRUE on success or FALSE on failure. + */ + public function extractDirTo($sDestinationDir, $sArchiveDir) + { + $aFiles = array(); + foreach ($this->getFiles($sArchiveDir) as $oFileInfo) + { + $aFiles[] = $oFileInfo->getRelativePath(); + } + if ((count($aFiles) > 0) && ($this->oPharArchive->extractTo($sDestinationDir, $aFiles, true) === true)) + { + return true; + } + return false; + } + + /** + * Returns the entry contents using its name + * @param string $name Name of the entry + * @param int $length [optional] The length to be read from the entry. If 0, then the entire entry is read. + * @param int $flags [optional] The flags to use to open the archive. the following values may be ORed to it. ZipArchive::FL_UNCHANGED + * @return string the contents of the entry on success or FALSE on failure. + */ + public function getFromName($name, $length = 0, $flags = null) + { + $oFileInfo = $this->oPharArchive->offsetGet($name); + $sFile = $oFileInfo->getPathname(); + $sRet = file_get_contents($sFile); + return $sRet; + } + + /** + * @param string|null $sArchivePath Path to search for + * @return null + */ + public function getFiles($sArchivePath = null) + { + if ($this->aFiles === null) + { + // Initial load + $this->buildFileList(); + } + if ($sArchivePath === null) + { + // Take them all + $aRet = $this->aFiles; + } + else + { + // Filter out files not in the given path + $aRet = array(); + foreach ($this->aFiles as $oFileInfo) + { + if ($oFileInfo->isUnder($sArchivePath)) + { + $aRet[] = $oFileInfo; + } + } + } + return $aRet; + } + + /** + * @param PharData|null $oPharData + * @param string $sArchivePath Path relatively to the archive root + */ + protected function buildFileList($oPharData = null, $sArchivePath = '/') + { + if ($oPharData === null) + { + $oPharData = $this->oPharArchive; + } + foreach($oPharData as $oPharFileInfo) + { + if($oPharFileInfo->isDir()) + { + $oSubDirectory = new PharData($oPharFileInfo->getPathname()); + // Recurse + $this->buildFileList($oSubDirectory, $sArchivePath.'/'.$oPharFileInfo->getFileName()); + } + else + { + $this->aFiles[] = new TarGzFileInfo($oPharFileInfo, $sArchivePath); + } + } + } +} + +class TarGzFileInfo +{ + public function __construct(PharFileInfo $oFileInfo, $sArchivePath) + { + $this->oPharFileInfo = $oFileInfo; + $this->sArchivePath = trim($sArchivePath, '/'); + } + + protected $sArchivePath; + protected $oPharFileInfo; + + public function getPathname() + { + return $this->oPharFileInfo->getPathname(); + } + + public function getRelativePath() + { + return $this->sArchivePath.'/'.$this->oPharFileInfo->getFilename(); + } + + public function isUnder($sArchivePath) + { + $sTestedPath = trim($sArchivePath, '/'); + return (strpos($this->sArchivePath, $sTestedPath) === 0); + } +} + diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index 8bcc4ffd9..be065940e 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -1,5 +1,5 @@ 'Strong encryption will not be used.', 'ldap' => 'LDAP authentication will be disabled.', 'gd' => 'PDF export will be disabled. Also, image resizing will be disabled on profile pictures (May increase database size).'); @@ -376,10 +376,15 @@ class SetupUtils // zip extension // - if (!extension_loaded('zip')) + if (!extension_loaded('phar')) { - $sMissingExtensionLink = "zip"; - $aResult[] = new CheckResult(CheckResult::ERROR, "Missing PHP extension: zip", $sMissingExtensionLink); + $sMissingExtensionLink = "zip"; + $aResult[] = new CheckResult(CheckResult::ERROR, "Missing PHP extension: phar", $sMissingExtensionLink); + } + if (!extension_loaded('zlib')) + { + $sMissingExtensionLink = "zip"; + $aResult[] = new CheckResult(CheckResult::ERROR, "Missing PHP extension: zlib", $sMissingExtensionLink); } // availability of exec() diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 754faa224..4c4ea5394 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -1,5 +1,5 @@ oWizard->GetParameter('mode', '') == 'upgrade') && $this->oWizard->GetParameter('db_backup', false)) { $sBackupDestination = $this->oWizard->GetParameter('db_backup_path', ''); - if (file_exists($sBackupDestination)) + if (file_exists($sBackupDestination.'.tar.gz')) { // To mitigate security risks: pass only the filename without the extension, the download will add the extension itself - $sTruncatedFilePath = preg_replace('/\.zip$/', '', $sBackupDestination); $oPage->p('Your backup is ready'); $oPage->p(' Download '.basename($sBackupDestination).''); } @@ -2418,14 +2417,13 @@ class WizStepDone extends WizardStep public function AsyncAction(WebPage $oPage, $sCode, $aParameters) { - $oParameters = new PHPParameters(); // For security reasons: add the extension now so that this action can be used to read *only* .zip files from the disk... - $sBackupFile = $aParameters['backup'].'.zip'; + $sBackupFile = $aParameters['backup'].'.tar.gz'; if (file_exists($sBackupFile)) { // Make sure there is NO output at all before our content, otherwise the document will be corrupted $sPreviousContent = ob_get_clean(); - $oPage->SetContentType('application/zip'); + $oPage->SetContentType('application/gzip'); $oPage->SetContentDisposition('attachment', basename($sBackupFile)); $oPage->add(file_get_contents($sBackupFile)); }