diff --git a/application/utils.inc.php b/application/utils.inc.php index a470a0c72d..479ed3b4d0 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -384,24 +384,8 @@ class utils $sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex]; $doc_content = file_get_contents($sTmpName); - if (function_exists('finfo_file')) - { - // as of PHP 5.3 the fileinfo extension is bundled within PHP - // in which case we don't trust the mime type provided by the browser - $rInfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension - if ($rInfo !== false) - { - $sType = @finfo_file($rInfo, $sTmpName); - if ( ($sType !== false) - && is_string($sType) - && (strlen($sType)>0)) - { - $sMimeType = $sType; - } - } - @finfo_close($rInfo); - } - $oDocument = new ormDocument($doc_content, $sMimeType, $sName); + $sMimeType = self::GetFileMimeType($sTmpName); + $oDocument = new ormDocument($doc_content, $sMimeType, $sName); break; case UPLOAD_ERR_NO_FILE: @@ -2045,4 +2029,38 @@ class utils { return ITOP_REVISION === 'svn'; } + + /** + * @see https://php.net/manual/en/function.finfo-file.php + * + * @param string $sFilePath file full path + * @param string $sDefaultMimeType + * + * @return string mime type, defaults to application/octet-stream + * @uses finfo_file in FileInfo extension (bundled in PHP since version 5.3) + * @since 2.7.0 N°2366 + */ + public static function GetFileMimeType($sFilePath, $sDefaultMimeType = 'application/octet-stream') + { + if (!function_exists('finfo_file')) + { + return $sDefaultMimeType; + } + + $sMimeType = $sDefaultMimeType; + $rInfo = @finfo_open(FILEINFO_MIME_TYPE); + if ($rInfo !== false) + { + $sType = @finfo_file($rInfo, $sFilePath); + if (($sType !== false) + && is_string($sType) + && ($sType !== '')) + { + $sMimeType = $sType; + } + } + @finfo_close($rInfo); + + return $sMimeType; + } } diff --git a/setup/backup.class.inc.php b/setup/backup.class.inc.php index de4ddd9522..57080f2a81 100644 --- a/setup/backup.class.inc.php +++ b/setup/backup.class.inc.php @@ -56,672 +56,484 @@ interface BackupArchive /** * Returns the entry contents using its name * + * Note: both params $length and $flags are unused in the current archive format (TarGzArchive)... But this is a + * public interface and there might be some other implementations out there so: DON'T REMOVE THEM! + * * @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 + * @param int $flags [optional] The flags to use to open the archive.
+ * Was used with Zip archives, example : 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 BackupException extends Exception +{ +} + + +class DBBackup { /** - * Handles adding directories into a Zip archive, and a unified API for archive read - * suggested enhancement: refactor the API for writing as well + * utf8mb4 was added in MySQL 5.5.3 but works with programs like mysqldump only since MySQL 5.5.33 + * + * @since 2.5 see N°1001 */ - class ZipArchiveEx extends ZipArchive implements BackupArchive - { - public function addDir($sDir, $sZipDir = '') - { - if (is_dir($sDir)) - { - if ($dh = opendir($sDir)) - { - // Add the directory - if (!empty($sZipDir)) - { - $this->addEmptyDir($sZipDir); - } + const MYSQL_VERSION_WITH_UTF8MB4_IN_PROGRAMS = '5.5.33'; - // Loop through all the files - while (($sFile = readdir($dh)) !== false) - { - // If it's a folder, run the function again! - if (!is_file($sDir.$sFile)) - { - // Skip parent and root directories - if (($sFile !== ".") && ($sFile !== "..")) - { - $this->addDir($sDir.$sFile."/", $sZipDir.$sFile."/"); - } - } - else - { - // Add the files - $this->addFile($sDir.$sFile, $sZipDir.$sFile); - } - } - } - } - } - - /** - * @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/') - * - * @param string $sDestinationDir - * @param string $sZipDir Must start and end with a slash !! - * - * @return boolean - */ - public function extractDirTo($sDestinationDir, $sZipDir) - { - $aFiles = array(); - for ($i = 0; $i < $this->numFiles; $i++) - { - $sEntry = $this->getNameIndex($i); - //Use strpos() to check if the entry name contains the directory we want to extract - if (strpos($sEntry, $sZipDir) === 0) - { - //Add the entry to our array if it in in our desired directory - $aFiles[] = $sEntry; - } - } - // Extract only the selected files - if ((count($aFiles) > 0) && ($this->extractTo($sDestinationDir, $aFiles) === true)) - { - return true; - } - - return false; - } - } // class ZipArchiveEx - - class BackupException extends Exception + // To be overriden depending on the expected usages + protected function LogInfo($sMsg) { } - class DBBackup + protected function LogError($sMsg) { - /** - * utf8mb4 was added in MySQL 5.5.3 but works with programs like mysqldump only since MySQL 5.5.33 - * - * @since 2.5 see N°1001 - */ - const MYSQL_VERSION_WITH_UTF8MB4_IN_PROGRAMS = '5.5.33'; + } - // To be overriden depending on the expected usages - protected function LogInfo($sMsg) + /** @var Config */ + protected $oConfig; + + // shortcuts used for log purposes + /** @var string */ + protected $sDBHost; + /** @var int */ + protected $iDBPort; + /** @var string */ + protected $sDBName; + /** @var string */ + protected $sDBSubName; + + /** + * Connects to the database to backup + * + * @param Config $oConfig object containing the database configuration.
+ * If null then uses the default configuration ({@see MetaModel::GetConfig}) + * + * @since 2.5 uses a Config object instead of passing each attribute (there were far too many with the addition of MySQL TLS parameters + * !) + */ + public function __construct($oConfig = null) + { + if (is_null($oConfig)) { + // Defaulting to the current config + $oConfig = MetaModel::GetConfig(); } - protected function LogError($sMsg) + $this->oConfig = $oConfig; + + // init log variables + CMDBSource::InitServerAndPort($oConfig->Get('db_host'), $this->sDBHost, $this->iDBPort); + $this->sDBName = $oConfig->get('db_name'); + $this->sDBSubName = $oConfig->get('db_subname'); + } + + protected $sMySQLBinDir = ''; + + /** + * Create a normalized backup name, depending on the current date/time and Database + * + * @param string sMySQLBinDir Name and path, eventually containing itop placeholders + time formatting specs + */ + public function SetMySQLBinDir($sMySQLBinDir) + { + $this->sMySQLBinDir = $sMySQLBinDir; + } + + /** + * Create a normalized backup name, depending on the current date/time and Database + * + * @param string sNameSpec Name and path, eventually containing itop placeholders + time formatting specs + * + * @return string + */ + public function MakeName($sNameSpec = "__DB__-%Y-%m-%d") + { + $sFileName = $sNameSpec; + $sFileName = str_replace('__HOST__', $this->sDBHost, $sFileName); + $sFileName = str_replace('__DB__', $this->sDBName, $sFileName); + $sFileName = str_replace('__SUBNAME__', $this->sDBSubName, $sFileName); + // Transform %Y, etc. + $sFileName = strftime($sFileName); + + return $sFileName; + } + + /** + * @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 + * + * @throws \CoreException if CMDBSource not initialized + * @throws \BackupException if archive cannot be created + * @throws \Exception + */ + public function CreateCompressedBackup($sTargetFile, $sSourceConfigFile = null) + { + $bIsCmdbSourceInitialized = CMDBSource::GetMysqli() instanceof mysqli; + if (!$bIsCmdbSourceInitialized) { + $sErrorMsg = 'Cannot backup : CMDBSource not initialized !'; + $this->LogError($sErrorMsg); + throw new CoreException($sErrorMsg); } - /** @var Config */ - protected $oConfig; + $this->LogInfo("Creating backup: '$sTargetFile.tar.gz'"); - // shortcuts used for log purposes - /** @var string */ - protected $sDBHost; - /** @var int */ - protected $iDBPort; - /** @var string */ - protected $sDBName; - /** @var string */ - protected $sDBSubName; + $oArchive = new ITopArchiveTar($sTargetFile.'.tar.gz'); - /** - * Connects to the database to backup - * - * @param Config $oConfig object containing the database configuration.
- * If null then uses the default configuration ({@see MetaModel::GetConfig}) - * - * @since 2.5 uses a Config object instead of passing each attribute (there were far too many with the addition of MySQL TLS parameters !) - */ - public function __construct($oConfig = null) + $sTmpFolder = APPROOT.'data/tmp-backup-'.rand(10000, getrandmax()); + $aFiles = $this->PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder); + + $sFilesList = var_export($aFiles, true); + $this->LogInfo("backup: adding to archive files '$sFilesList'"); + $bArchiveCreationResult = $oArchive->createModify($aFiles, '', $sTmpFolder); + if (!$bArchiveCreationResult) { - if (is_null($oConfig)) - { - // Defaulting to the current config - $oConfig = MetaModel::GetConfig(); - } - - $this->oConfig = $oConfig; - - // init log variables - CMDBSource::InitServerAndPort($oConfig->Get('db_host'), $this->sDBHost, $this->iDBPort); - $this->sDBName = $oConfig->get('db_name'); - $this->sDBSubName = $oConfig->get('db_subname'); + $sErrorMsg = 'Cannot backup : unable to create archive'; + $this->LogError($sErrorMsg); + throw new BackupException($sErrorMsg); } - protected $sMySQLBinDir = ''; + $this->LogInfo("backup: removing tmp folder '$sTmpFolder'"); + SetupUtils::rrmdir($sTmpFolder); + } - /** - * Create a normalized backup name, depending on the current date/time and Database - * - * @param string sMySQLBinDir Name and path, eventually containing itop placeholders + time formatting specs - */ - public function SetMySQLBinDir($sMySQLBinDir) + /** + * Copy files to store into the temporary folder, in addition to the SQL dump + * + * @param string $sSourceConfigFile + * @param string $sTmpFolder + * + * @return array list of files to archive + * @throws \Exception + */ + protected function PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder) + { + $aRet = array(); + if (is_dir($sTmpFolder)) { - $this->sMySQLBinDir = $sMySQLBinDir; - } - - /** - * Create a normalized backup name, depending on the current date/time and Database - * - * @param string sNameSpec Name and path, eventually containing itop placeholders + time formatting specs - * - * @return string - */ - public function MakeName($sNameSpec = "__DB__-%Y-%m-%d") - { - $sFileName = $sNameSpec; - $sFileName = str_replace('__HOST__', $this->sDBHost, $sFileName); - $sFileName = str_replace('__DB__', $this->sDBName, $sFileName); - $sFileName = str_replace('__SUBNAME__', $this->sDBSubName, $sFileName); - // Transform %Y, etc. - $sFileName = strftime($sFileName); - - return $sFileName; - } - - /** - * @deprecated 2.4.0 Zip files are limited to 4 Gb, use CreateCompressedBackup to create tar.gz files - * - * @param string $sZipFile - * @param string|null $sSourceConfigFile - * - * @throws \BackupException - */ - 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'"); - $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 - * - * @throws \CoreException if CMDBSource not initialized - * @throws \BackupException if archive cannot be created - * @throws \Exception - */ - public function CreateCompressedBackup($sTargetFile, $sSourceConfigFile = null) - { - $bIsCmdbSourceInitialized = CMDBSource::GetMysqli() instanceof mysqli; - if (!$bIsCmdbSourceInitialized) - { - $sErrorMsg = 'Cannot backup : CMDBSource not initialized !'; - $this->LogError($sErrorMsg); - throw new CoreException($sErrorMsg); - } - - $this->LogInfo("Creating backup: '$sTargetFile.tar.gz'"); - - $oArchive = new ITopArchiveTar($sTargetFile.'.tar.gz'); - - $sTmpFolder = APPROOT.'data/tmp-backup-'.rand(10000, getrandmax()); - $aFiles = $this->PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder); - - $sFilesList = var_export($aFiles, true); - $this->LogInfo("backup: adding to archive files '$sFilesList'"); - $bArchiveCreationResult = $oArchive->createModify($aFiles, '', $sTmpFolder); - if (!$bArchiveCreationResult) - { - $sErrorMsg = 'Cannot backup : unable to create archive'; - $this->LogError($sErrorMsg); - throw new BackupException($sErrorMsg); - } - - $this->LogInfo("backup: removing tmp folder '$sTmpFolder'"); SetupUtils::rrmdir($sTmpFolder); } - - /** - * Copy files to store into the temporary folder, in addition to the SQL dump - * - * @param string $sSourceConfigFile - * @param string $sTmpFolder - * - * @return array list of files to archive - * @throws \Exception - */ - protected function PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder) + $this->LogInfo("backup: creating tmp dir '$sTmpFolder'"); + @mkdir($sTmpFolder, 0777, true); + if (is_null($sSourceConfigFile)) { - $aRet = array(); - if (is_dir($sTmpFolder)) - { - SetupUtils::rrmdir($sTmpFolder); - } - $this->LogInfo("backup: creating tmp dir '$sTmpFolder'"); - @mkdir($sTmpFolder, 0777, true); - if (is_null($sSourceConfigFile)) - { - $sSourceConfigFile = MetaModel::GetConfig()->GetLoadedFile(); - } - if (!empty($sSourceConfigFile)) - { - $sFile = $sTmpFolder.'/config-itop.php'; - $this->LogInfo("backup: adding resource '$sSourceConfigFile'"); - copy($sSourceConfigFile, $sFile); - $aRet[] = $sFile; - } - - $sDeltaFile = APPROOT.'data/'.utils::GetCurrentEnvironment().'.delta.xml'; - if (file_exists($sDeltaFile)) - { - $sFile = $sTmpFolder.'/delta.xml'; - $this->LogInfo("backup: adding resource '$sDeltaFile'"); - copy($sDeltaFile, $sFile); - $aRet[] = $sFile; - } - $sExtraDir = APPROOT.'data/'.utils::GetCurrentEnvironment().'-modules/'; - if (is_dir($sExtraDir)) - { - $sModules = utils::GetCurrentEnvironment().'-modules'; - $sFile = $sTmpFolder.'/'.$sModules; - $this->LogInfo("backup: adding resource '$sExtraDir'"); - SetupUtils::copydir($sExtraDir, $sFile); - $aRet[] = $sFile; - } - $sDataFile = $sTmpFolder.'/itop-dump.sql'; - $this->DoBackup($sDataFile); - $aRet[] = $sDataFile; - - return $aRet; + $sSourceConfigFile = MetaModel::GetConfig()->GetLoadedFile(); + } + if (!empty($sSourceConfigFile)) + { + $sFile = $sTmpFolder.'/config-itop.php'; + $this->LogInfo("backup: adding resource '$sSourceConfigFile'"); + copy($sSourceConfigFile, $sFile); + $aRet[] = $sFile; } - protected static function EscapeShellArg($sValue) + $sDeltaFile = APPROOT.'data/'.utils::GetCurrentEnvironment().'.delta.xml'; + if (file_exists($sDeltaFile)) { - // Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation - // It suggests to rely on pctnl_* function instead of using escapeshellargs - return escapeshellarg($sValue); + $sFile = $sTmpFolder.'/delta.xml'; + $this->LogInfo("backup: adding resource '$sDeltaFile'"); + copy($sDeltaFile, $sFile); + $aRet[] = $sFile; + } + $sExtraDir = APPROOT.'data/'.utils::GetCurrentEnvironment().'-modules/'; + if (is_dir($sExtraDir)) + { + $sModules = utils::GetCurrentEnvironment().'-modules'; + $sFile = $sTmpFolder.'/'.$sModules; + $this->LogInfo("backup: adding resource '$sExtraDir'"); + SetupUtils::copydir($sExtraDir, $sFile); + $aRet[] = $sFile; + } + $sDataFile = $sTmpFolder.'/itop-dump.sql'; + $this->DoBackup($sDataFile); + $aRet[] = $sDataFile; + + return $aRet; + } + + protected static function EscapeShellArg($sValue) + { + // Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation + // It suggests to rely on pctnl_* function instead of using escapeshellargs + return escapeshellarg($sValue); + } + + /** + * Create a backup file + * + * @param string $sBackupFileName + * + * @throws \BackupException + */ + public function DoBackup($sBackupFileName) + { + $sHost = self::EscapeShellArg($this->sDBHost); + $sUser = self::EscapeShellArg($this->oConfig->Get('db_user')); + $sPwd = self::EscapeShellArg($this->oConfig->Get('db_pwd')); + $sDBName = self::EscapeShellArg($this->sDBName); + + // Just to check the connection to the DB (better than getting the retcode of mysqldump = 1) + $this->DBConnect(); + + $sTables = ''; + if ($this->sDBSubName != '') + { + // This instance of iTop uses a prefix for the tables, so there may be other tables in the database + // Let's explicitely list all the tables and views to dump + $aTables = $this->EnumerateTables(); + if (count($aTables) == 0) + { + // No table has been found with the given prefix + throw new BackupException("No table has been found with the given prefix"); + } + $aEscapedTables = array(); + foreach ($aTables as $sTable) + { + $aEscapedTables[] = self::EscapeShellArg($sTable); + } + $sTables = implode(' ', $aEscapedTables); } - /** - * Create a backup file - * - * @param string $sBackupFileName - * - * @throws \BackupException - */ - public function DoBackup($sBackupFileName) - { - $sHost = self::EscapeShellArg($this->sDBHost); - $sUser = self::EscapeShellArg($this->oConfig->Get('db_user')); - $sPwd = self::EscapeShellArg($this->oConfig->Get('db_pwd')); - $sDBName = self::EscapeShellArg($this->sDBName); + $this->LogInfo("Starting backup of $this->sDBHost/$this->sDBName(suffix:'$this->sDBSubName')"); - // Just to check the connection to the DB (better than getting the retcode of mysqldump = 1) - $this->DBConnect(); + $sMySQLDump = $this->GetMysqldumpCommand(); - $sTables = ''; - if ($this->sDBSubName != '') - { - // This instance of iTop uses a prefix for the tables, so there may be other tables in the database - // Let's explicitely list all the tables and views to dump - $aTables = $this->EnumerateTables(); - if (count($aTables) == 0) - { - // No table has been found with the given prefix - throw new BackupException("No table has been found with the given prefix"); - } - $aEscapedTables = array(); - foreach ($aTables as $sTable) - { - $aEscapedTables[] = self::EscapeShellArg($sTable); - } - $sTables = implode(' ', $aEscapedTables); - } + // Store the results in a temporary file + $sTmpFileName = self::EscapeShellArg($sBackupFileName); - $this->LogInfo("Starting backup of $this->sDBHost/$this->sDBName(suffix:'$this->sDBSubName')"); + $sPortOption = self::GetMysqliCliSingleOption('port', $this->iDBPort); + $sTlsOptions = self::GetMysqlCliTlsOptions($this->oConfig); - $sMySQLDump = $this->GetMysqldumpCommand(); + $sMysqlVersion = CMDBSource::GetDBVersion(); + $bIsMysqlSupportUtf8mb4 = (version_compare($sMysqlVersion, self::MYSQL_VERSION_WITH_UTF8MB4_IN_PROGRAMS) === -1); + $sMysqldumpCharset = $bIsMysqlSupportUtf8mb4 ? 'utf8' : DEFAULT_CHARACTER_SET; - // Store the results in a temporary file - $sTmpFileName = self::EscapeShellArg($sBackupFileName); + // Delete the file created by tempnam() so that the spawned process can write into it (Windows/IIS) + @unlink($sBackupFileName); - $sPortOption = self::GetMysqliCliSingleOption('port', $this->iDBPort); - $sTlsOptions = self::GetMysqlCliTlsOptions($this->oConfig); - - $sMysqlVersion = CMDBSource::GetDBVersion(); - $bIsMysqlSupportUtf8mb4 = (version_compare($sMysqlVersion, self::MYSQL_VERSION_WITH_UTF8MB4_IN_PROGRAMS) === -1); - $sMysqldumpCharset = $bIsMysqlSupportUtf8mb4 ? 'utf8' : DEFAULT_CHARACTER_SET; - - // Delete the file created by tempnam() so that the spawned process can write into it (Windows/IIS) - @unlink($sBackupFileName); - - // Store the password into a temporary file to avoid mysql complaint - $sMySQLDumpCnfFile = tempnam(SetupUtils::GetTmpDir(), 'itop-mysqldump-'); - $sMySQLDumpCnf = <<&1"; - $sCommandDisplay = "$sMySQLDump --defaults-file=\"$sMySQLDumpCnfFile\" --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption --user=xxxxx $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables"; + // Note: opt implicitely sets lock-tables... which cancels the benefit of single-transaction! + // skip-lock-tables compensates and allows for writes during a backup + $sCommand = "$sMySQLDump --defaults-file=\"$sMySQLDumpCnfFile\" --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption --user=$sUser $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables 2>&1"; + $sCommandDisplay = "$sMySQLDump --defaults-file=\"$sMySQLDumpCnfFile\" --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption --user=xxxxx $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables"; - // Now run the command for real - $this->LogInfo("backup: generate data file with command: $sCommandDisplay"); - $aOutput = array(); - $iRetCode = 0; - exec($sCommand, $aOutput, $iRetCode); - @unlink($sMySQLDumpCnfFile); + // Now run the command for real + $this->LogInfo("backup: generate data file with command: $sCommandDisplay"); + $aOutput = array(); + $iRetCode = 0; + exec($sCommand, $aOutput, $iRetCode); + @unlink($sMySQLDumpCnfFile); + foreach ($aOutput as $sLine) + { + $this->LogInfo("mysqldump said: $sLine"); + } + if ($iRetCode != 0) + { + // Cleanup residual output (Happens with Error 2020: Got packet bigger than 'maxallowedpacket' bytes...) + if (file_exists($sBackupFileName)) + { + unlink($sBackupFileName); + } + + $this->LogError("Failed to execute: $sCommandDisplay. The command returned:$iRetCode"); foreach ($aOutput as $sLine) { - $this->LogInfo("mysqldump said: $sLine"); + $this->LogError("mysqldump said: $sLine"); } - if ($iRetCode != 0) + if (count($aOutput) == 1) { - // Cleanup residual output (Happens with Error 2020: Got packet bigger than 'maxallowedpacket' bytes...) - if (file_exists($sBackupFileName)) - { - unlink($sBackupFileName); - } - - $this->LogError("Failed to execute: $sCommandDisplay. The command returned:$iRetCode"); - foreach ($aOutput as $sLine) - { - $this->LogError("mysqldump said: $sLine"); - } - if (count($aOutput) == 1) - { - $sMoreInfo = trim($aOutput[0]); - } - else - { - $sMoreInfo = "Check the log files 'log/setup.log' or 'log/error.log' for more information."; - } - throw new BackupException("Failed to execute mysqldump: ".$sMoreInfo); - } - } - - /** - * Helper to create a ZIP out of several files - * - * @param array $aFiles - * @param string $sZipArchiveFile - * - * @throws \BackupException - */ - protected function DoZip($aFiles, $sZipArchiveFile) - { - foreach ($aFiles as $aFile) - { - $sFile = $aFile['source']; - if (!is_file($sFile) && !is_dir($sFile)) - { - throw new BackupException("File '$sFile' does not exist or could not be read"); - } - } - // Make sure the target path exists - $sZipDir = dirname($sZipArchiveFile); - SetupUtils::builddir($sZipDir); - - $oZip = new ZipArchiveEx(); - $res = $oZip->open($sZipArchiveFile, ZipArchive::CREATE | ZipArchive::OVERWRITE); - if ($res === true) - { - foreach ($aFiles as $aFile) - { - if (is_dir($aFile['source'])) - { - $oZip->addDir($aFile['source'], $aFile['dest']); - } - else - { - $oZip->addFile($aFile['source'], $aFile['dest']); - } - } - if ($oZip->close()) - { - $this->LogInfo("Archive: $sZipArchiveFile created"); - } - else - { - $this->LogError("Failed to save zip archive: $sZipArchiveFile"); - throw new BackupException("Failed to save zip archive: $sZipArchiveFile"); - } + $sMoreInfo = trim($aOutput[0]); } else { - $this->LogError("Failed to create zip archive: $sZipArchiveFile."); - throw new BackupException("Failed to create zip archive: $sZipArchiveFile."); + $sMoreInfo = "Check the log files 'log/setup.log' or 'log/error.log' for more information."; } + throw new BackupException("Failed to execute mysqldump: ".$sMoreInfo); } + } - /** - * Helper to download the file directly from the browser - * - * @param string $sFile - */ - public function DownloadBackup($sFile) + /** + * Helper to download the file directly from the browser + * + * @param string $sFile full file path + * + * @throws \InvalidParameterException if the file doesn't exists + */ + public function DownloadBackup($sFile) + { + if (!file_exists($sFile)) { - if (file_exists($sFile)) - { - header('Content-Description: File Transfer'); - header('Content-Type: multipart/x-zip'); - header('Content-Disposition: inline; filename="'.basename($sFile).'"'); - header('Expires: 0'); - header('Cache-Control: must-revalidate'); - header('Pragma: public'); - header('Content-Length: '.filesize($sFile)); - readfile($sFile) ; - } - else - { - throw new InvalidParameterException('Invalid file path'); - } + throw new InvalidParameterException('Invalid file path'); } - /** - * Helper to open a Database connection - * - * @return \mysqli - * @throws \BackupException - * @uses CMDBSource - */ - protected function DBConnect() + $sMimeType = utils::GetFileMimeType($sFile); + + header('Content-Description: File Transfer'); + header('Content-Type: '.$sMimeType); + header('Content-Disposition: inline; filename="'.basename($sFile).'"'); + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + header('Pragma: public'); + header('Content-Length: '.filesize($sFile)); + readfile($sFile); + } + + /** + * Helper to open a Database connection + * + * @return \mysqli + * @throws \BackupException + * @uses CMDBSource + */ + protected function DBConnect() + { + $oConfig = $this->oConfig; + $sServer = $oConfig->Get('db_host'); + $sUser = $oConfig->Get('db_user'); + $sPwd = $oConfig->Get('db_pwd'); + $sSource = $oConfig->Get('db_name'); + $sTlsEnabled = $oConfig->Get('db_tls.enabled'); + $sTlsCA = $oConfig->Get('db_tls.ca'); + + try { - $oConfig = $this->oConfig; - $sServer = $oConfig->Get('db_host'); - $sUser = $oConfig->Get('db_user'); - $sPwd = $oConfig->Get('db_pwd'); - $sSource = $oConfig->Get('db_name'); - $sTlsEnabled = $oConfig->Get('db_tls.enabled'); - $sTlsCA = $oConfig->Get('db_tls.ca'); + $oMysqli = CMDBSource::GetMysqliInstance($sServer, $sUser, $sPwd, $sSource, $sTlsEnabled, $sTlsCA, + false); - try + if ($oMysqli->connect_errno) { - $oMysqli = CMDBSource::GetMysqliInstance($sServer, $sUser, $sPwd, $sSource, $sTlsEnabled, $sTlsCA, - false); - - if ($oMysqli->connect_errno) - { - $sHost = is_null($this->iDBPort) ? $this->sDBHost : $this->sDBHost.' on port '.$this->iDBPort; - throw new BackupException("Cannot connect to the MySQL server '$sHost' (".$oMysqli->connect_errno.") ".$oMysqli->connect_error); - } - if (!$oMysqli->select_db($this->sDBName)) - { - throw new BackupException("The database '$this->sDBName' does not seem to exist"); - } - - return $oMysqli; + $sHost = is_null($this->iDBPort) ? $this->sDBHost : $this->sDBHost.' on port '.$this->iDBPort; + throw new BackupException("Cannot connect to the MySQL server '$sHost' (".$oMysqli->connect_errno.") ".$oMysqli->connect_error); } - catch (MySQLException $e) + if (!$oMysqli->select_db($this->sDBName)) { - throw new BackupException($e->getMessage()); + throw new BackupException("The database '$this->sDBName' does not seem to exist"); } + + return $oMysqli; } - - /** - * Helper to enumerate the tables of the database - * - * @throws \BackupException - */ - protected function EnumerateTables() + catch (MySQLException $e) { - $oMysqli = $this->DBConnect(); - if ($this->sDBSubName != '') - { - $oResult = $oMysqli->query("SHOW TABLES LIKE '{$this->sDBSubName}%'"); - } - else - { - $oResult = $oMysqli->query("SHOW TABLES"); - } - if (!$oResult) - { - throw new BackupException("Failed to execute the SHOW TABLES query: ".$oMysqli->error); - } - $aTables = array(); - while ($aRow = $oResult->fetch_row()) - { - $aTables[] = $aRow[0]; - } - - return $aTables; + throw new BackupException($e->getMessage()); } + } - - /** - * @param Config $oConfig - * - * @return string TLS arguments for CLI programs such as mysqldump. Empty string if the config does not use TLS. - * - * @see https://dev.mysql.com/doc/refman/5.6/en/encrypted-connection-options.html - * @since 2.5 - */ - public static function GetMysqlCliTlsOptions($oConfig) + /** + * Helper to enumerate the tables of the database + * + * @throws \BackupException + */ + protected function EnumerateTables() + { + $oMysqli = $this->DBConnect(); + if ($this->sDBSubName != '') { - $bDbTlsEnabled = $oConfig->Get('db_tls.enabled'); - if (!$bDbTlsEnabled) - { - return ''; - } - $sTlsOptions = ''; - - $sDBVendor= CMDBSource::GetDBVendor(); - $sDBVersion = CMDBSource::GetDBVersion(); - $sMysqlSSLModeVersion = '5.7.0'; //Mysql 5.7.0 and upper deprecated --ssl and uses --ssl-mode instead - if ($sDBVendor === CMDBSource::ENUM_DB_VENDOR_MYSQL && version_compare($sDBVersion, $sMysqlSSLModeVersion, '>=')) - { - $sTlsOptions .= ' --ssl-mode=VERIFY_CA'; - } - else - { - $sTlsOptions .= ' --ssl'; - } - - // ssl-key parameter : not implemented - // ssl-cert parameter : not implemented - - $sTlsOptions .= self::GetMysqliCliSingleOption('ssl-ca', $oConfig->Get('db_tls.ca')); - - // ssl-cipher parameter : not implemented - // ssl-capath parameter : not implemented - - return $sTlsOptions; + $oResult = $oMysqli->query("SHOW TABLES LIKE '{$this->sDBSubName}%'"); } - - /** - * @param string $sCliArgName - * @param string $sData - * - * @return string empty if data is empty, else argument in form of ' --cliargname=data' - */ - private static function GetMysqliCliSingleOption($sCliArgName, $sData) + else { - if (empty($sData)) - { - return ''; - } - - return ' --'.$sCliArgName.'='.self::EscapeShellArg($sData); + $oResult = $oMysqli->query("SHOW TABLES"); } - - /** - * @return string the command to launch mysqldump (without its params) - */ - private function GetMysqldumpCommand() + if (!$oResult) { - $sMySQLBinDir = utils::ReadParam('mysql_bindir', $this->sMySQLBinDir, true); - if (empty($sMySQLBinDir)) - { - $sMysqldumpCommand = 'mysqldump'; - } - else - { - $sMysqldumpCommand = '"'.$sMySQLBinDir.'/mysqldump"'; - } - - return $sMysqldumpCommand; + throw new BackupException("Failed to execute the SHOW TABLES query: ".$oMysqli->error); } + $aTables = array(); + while ($aRow = $oResult->fetch_row()) + { + $aTables[] = $aRow[0]; + } + + return $aTables; + } + + + /** + * @see https://dev.mysql.com/doc/refman/5.6/en/encrypted-connection-options.html + * + * @param Config $oConfig + * + * @return string TLS arguments for CLI programs such as mysqldump. Empty string if the config does not use TLS. + * + * @since 2.5 + */ + public static function GetMysqlCliTlsOptions($oConfig) + { + $bDbTlsEnabled = $oConfig->Get('db_tls.enabled'); + if (!$bDbTlsEnabled) + { + return ''; + } + $sTlsOptions = ''; + + $sDBVendor = CMDBSource::GetDBVendor(); + $sDBVersion = CMDBSource::GetDBVersion(); + $sMysqlSSLModeVersion = '5.7.0'; //Mysql 5.7.0 and upper deprecated --ssl and uses --ssl-mode instead + if ($sDBVendor === CMDBSource::ENUM_DB_VENDOR_MYSQL && version_compare($sDBVersion, $sMysqlSSLModeVersion, '>=')) + { + $sTlsOptions .= ' --ssl-mode=VERIFY_CA'; + } + else + { + $sTlsOptions .= ' --ssl'; + } + + // ssl-key parameter : not implemented + // ssl-cert parameter : not implemented + + $sTlsOptions .= self::GetMysqliCliSingleOption('ssl-ca', $oConfig->Get('db_tls.ca')); + + // ssl-cipher parameter : not implemented + // ssl-capath parameter : not implemented + + return $sTlsOptions; + } + + /** + * @param string $sCliArgName + * @param string $sData + * + * @return string empty if data is empty, else argument in form of ' --cliargname=data' + */ + private static function GetMysqliCliSingleOption($sCliArgName, $sData) + { + if (empty($sData)) + { + return ''; + } + + return ' --'.$sCliArgName.'='.self::EscapeShellArg($sData); + } + + /** + * @return string the command to launch mysqldump (without its params) + */ + private function GetMysqldumpCommand() + { + $sMySQLBinDir = utils::ReadParam('mysql_bindir', $this->sMySQLBinDir, true); + if (empty($sMySQLBinDir)) + { + $sMysqldumpCommand = 'mysqldump'; + } + else + { + $sMysqldumpCommand = '"'.$sMySQLBinDir.'/mysqldump"'; + } + + return $sMysqldumpCommand; } }