backup.class.inc.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. <?php
  2. // Copyright (C) 2010-2016 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. /**
  19. * Handles adding directories into a Zip archive
  20. */
  21. if (class_exists('ZipArchive')) // The setup must be able to start even if the "zip" extension is not loaded
  22. {
  23. class ZipArchiveEx extends ZipArchive
  24. {
  25. public function addDir($sDir, $sZipDir = '')
  26. {
  27. if (is_dir($sDir))
  28. {
  29. if ($dh = opendir($sDir))
  30. {
  31. // Add the directory
  32. if (!empty($sZipDir)) $this->addEmptyDir($sZipDir);
  33. // Loop through all the files
  34. while (($sFile = readdir($dh)) !== false)
  35. {
  36. // If it's a folder, run the function again!
  37. if (!is_file($sDir.$sFile))
  38. {
  39. // Skip parent and root directories
  40. if (($sFile !== ".") && ($sFile !== ".."))
  41. {
  42. $this->addDir($sDir.$sFile."/", $sZipDir.$sFile."/");
  43. }
  44. }
  45. else
  46. {
  47. // Add the files
  48. $this->addFile($sDir.$sFile, $sZipDir.$sFile);
  49. }
  50. }
  51. }
  52. }
  53. }
  54. /**
  55. * Extract a whole directory from the archive.
  56. * Usage: $oZip->extractDirTo('/var/www/html/itop/data', '/production-modules/')
  57. * @param string $sDestinationDir
  58. * @param string $sZipDir Must start and end with a slash !!
  59. * @return boolean
  60. */
  61. public function extractDirTo($sDestinationDir, $sZipDir)
  62. {
  63. $aFiles = array();
  64. for($i = 0; $i < $this->numFiles; $i++)
  65. {
  66. $sEntry = $this->getNameIndex($i);
  67. //Use strpos() to check if the entry name contains the directory we want to extract
  68. if (strpos($sEntry, $sZipDir) === 0)
  69. {
  70. //Add the entry to our array if it in in our desired directory
  71. $aFiles[] = $sEntry;
  72. }
  73. }
  74. // Extract only the selcted files
  75. if ((count($aFiles) > 0) && ($this->extractTo($sDestinationDir, $aFiles) === true))
  76. {
  77. return true;
  78. }
  79. return false;
  80. }
  81. } // class ZipArchiveEx
  82. class BackupException extends Exception
  83. {
  84. }
  85. class DBBackup
  86. {
  87. // To be overriden depending on the expected usages
  88. protected function LogInfo($sMsg)
  89. {
  90. }
  91. protected function LogError($sMsg)
  92. {
  93. }
  94. protected $sDBHost;
  95. protected $iDBPort;
  96. protected $sDBUser;
  97. protected $sDBPwd;
  98. protected $sDBName;
  99. protected $sDBSubName;
  100. /**
  101. * Connects to the database to backup
  102. * By default, connects to the current MetaModel (must be loaded)
  103. *
  104. * @param sDBHost string Database host server
  105. * @param $sDBUser string User login
  106. * @param $sDBPwd string User password
  107. * @param $sDBName string Database name
  108. * @param $sDBSubName string Prefix to the tables of itop in the database
  109. */
  110. public function __construct($sDBHost = null, $sDBUser = null, $sDBPwd = null, $sDBName = null, $sDBSubName = null)
  111. {
  112. if (is_null($sDBHost))
  113. {
  114. // Defaulting to the current config
  115. $sDBHost = MetaModel::GetConfig()->GetDBHost();
  116. $sDBUser = MetaModel::GetConfig()->GetDBUser();
  117. $sDBPwd = MetaModel::GetConfig()->GetDBPwd();
  118. $sDBName = MetaModel::GetConfig()->GetDBName();
  119. $sDBSubName = MetaModel::GetConfig()->GetDBSubName();
  120. }
  121. // Compute the port (if present in the host name)
  122. $aConnectInfo = explode(':', $sDBHost);
  123. $sDBHostName = $aConnectInfo[0];
  124. if (count($aConnectInfo) > 1)
  125. {
  126. $iDBPort = $aConnectInfo[1];
  127. }
  128. else
  129. {
  130. $iDBPort = null;
  131. }
  132. $this->sDBHost = $sDBHostName;
  133. $this->iDBPort = $iDBPort;
  134. $this->sDBUser = $sDBUser;
  135. $this->sDBPwd = $sDBPwd;
  136. $this->sDBName = $sDBName;
  137. $this->sDBSubName = $sDBSubName;
  138. }
  139. protected $sMySQLBinDir = '';
  140. /**
  141. * Create a normalized backup name, depending on the current date/time and Database
  142. * @param sNameSpec string Name and path, eventually containing itop placeholders + time formatting specs
  143. */
  144. public function SetMySQLBinDir($sMySQLBinDir)
  145. {
  146. $this->sMySQLBinDir = $sMySQLBinDir;
  147. }
  148. /**
  149. * Create a normalized backup name, depending on the current date/time and Database
  150. * @param sNameSpec string Name and path, eventually containing itop placeholders + time formatting specs
  151. */
  152. public function MakeName($sNameSpec = "__DB__-%Y-%m-%d")
  153. {
  154. $sFileName = $sNameSpec;
  155. $sFileName = str_replace('__HOST__', $this->sDBHost, $sFileName);
  156. $sFileName = str_replace('__DB__', $this->sDBName, $sFileName);
  157. $sFileName = str_replace('__SUBNAME__', $this->sDBSubName, $sFileName);
  158. // Transform %Y, etc.
  159. $sFileName = strftime($sFileName);
  160. return $sFileName;
  161. }
  162. public function CreateZip($sZipFile, $sSourceConfigFile = null)
  163. {
  164. // Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS)
  165. // (delete it before spawning a process)
  166. $sDataFile = tempnam(SetupUtils::GetTmpDir(), 'itop-');
  167. $this->LogInfo("Data file: '$sDataFile'");
  168. $aContents = array();
  169. $aContents[] = array(
  170. 'source' => $sDataFile,
  171. 'dest' => 'itop-dump.sql',
  172. );
  173. if (is_null($sSourceConfigFile))
  174. {
  175. $sSourceConfigFile = MetaModel::GetConfig()->GetLoadedFile();
  176. }
  177. if (!empty($sSourceConfigFile))
  178. {
  179. $aContents[] = array(
  180. 'source' => $sSourceConfigFile,
  181. 'dest' => 'config-itop.php',
  182. );
  183. }
  184. $this->DoBackup($sDataFile);
  185. $sDeltaFile = APPROOT.'data/'.utils::GetCurrentEnvironment().'.delta.xml';
  186. if (file_exists($sDeltaFile))
  187. {
  188. $aContents[] = array(
  189. 'source' => $sDeltaFile,
  190. 'dest' => 'delta.xml',
  191. );
  192. }
  193. $sExtraDir = APPROOT.'data/'.utils::GetCurrentEnvironment().'-modules/';
  194. if (is_dir($sExtraDir))
  195. {
  196. $aContents[] = array(
  197. 'source' => $sExtraDir,
  198. 'dest' => utils::GetCurrentEnvironment().'-modules/',
  199. );
  200. }
  201. $this->DoZip($aContents, $sZipFile);
  202. // Windows/IIS: the data file has been created by the spawned process...
  203. // trying to delete it will issue a warning, itself stopping the setup abruptely
  204. @unlink($sDataFile);
  205. }
  206. protected static function EscapeShellArg($sValue)
  207. {
  208. // Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation
  209. // It suggests to rely on pctnl_* function instead of using escapeshellargs
  210. return escapeshellarg($sValue);
  211. }
  212. /**
  213. * Create a backup file
  214. */
  215. public function DoBackup($sBackupFileName)
  216. {
  217. $sHost = self::EscapeShellArg($this->sDBHost);
  218. $sUser = self::EscapeShellArg($this->sDBUser);
  219. $sPwd = self::EscapeShellArg($this->sDBPwd);
  220. $sDBName = self::EscapeShellArg($this->sDBName);
  221. // Just to check the connection to the DB (better than getting the retcode of mysqldump = 1)
  222. $oMysqli = $this->DBConnect();
  223. $sTables = '';
  224. if ($this->sDBSubName != '')
  225. {
  226. // This instance of iTop uses a prefix for the tables, so there may be other tables in the database
  227. // Let's explicitely list all the tables and views to dump
  228. $aTables = $this->EnumerateTables();
  229. if (count($aTables) == 0)
  230. {
  231. // No table has been found with the given prefix
  232. throw new BackupException("No table has been found with the given prefix");
  233. }
  234. $aEscapedTables = array();
  235. foreach($aTables as $sTable)
  236. {
  237. $aEscapedTables[] = self::EscapeShellArg($sTable);
  238. }
  239. $sTables = implode(' ', $aEscapedTables);
  240. }
  241. $this->LogInfo("Starting backup of $this->sDBHost/$this->sDBName(suffix:'$this->sDBSubName')");
  242. $sMySQLBinDir = utils::ReadParam('mysql_bindir', $this->sMySQLBinDir, true);
  243. if (empty($sMySQLBinDir))
  244. {
  245. $sMySQLDump = 'mysqldump';
  246. }
  247. else
  248. {
  249. $sMySQLDump = '"'.$sMySQLBinDir.'/mysqldump"';
  250. }
  251. // Store the results in a temporary file
  252. $sTmpFileName = self::EscapeShellArg($sBackupFileName);
  253. if (is_null($this->iDBPort))
  254. {
  255. $sPortOption = '';
  256. }
  257. else
  258. {
  259. $sPortOption = '--port='.$this->iDBPort.' ';
  260. }
  261. // Delete the file created by tempnam() so that the spawned process can write into it (Windows/IIS)
  262. unlink($sBackupFileName);
  263. $sCommand = "$sMySQLDump --opt --default-character-set=utf8 --add-drop-database --single-transaction --host=$sHost $sPortOption --user=$sUser --password=$sPwd --result-file=$sTmpFileName $sDBName $sTables 2>&1";
  264. $sCommandDisplay = "$sMySQLDump --opt --default-character-set=utf8 --add-drop-database --single-transaction --host=$sHost $sPortOption --user=xxxxx --password=xxxxx --result-file=$sTmpFileName $sDBName $sTables";
  265. // Now run the command for real
  266. $this->LogInfo("Executing command: $sCommandDisplay");
  267. $aOutput = array();
  268. $iRetCode = 0;
  269. exec($sCommand, $aOutput, $iRetCode);
  270. foreach($aOutput as $sLine)
  271. {
  272. $this->LogInfo("mysqldump said: $sLine");
  273. }
  274. if ($iRetCode != 0)
  275. {
  276. // Cleanup residual output (Happens with Error 2020: Got packet bigger than 'maxallowedpacket' bytes...)
  277. if (file_exists($sBackupFileName))
  278. {
  279. unlink($sBackupFileName);
  280. }
  281. $this->LogError("Failed to execute: $sCommandDisplay. The command returned:$iRetCode");
  282. foreach($aOutput as $sLine)
  283. {
  284. $this->LogError("mysqldump said: $sLine");
  285. }
  286. if (count($aOutput) == 1)
  287. {
  288. $sMoreInfo = trim($aOutput[0]);
  289. }
  290. else
  291. {
  292. $sMoreInfo = "Check the log files '".realpath(APPROOT.'/log/setup.log or error.log')."' for more information.";
  293. }
  294. throw new BackupException("Failed to execute mysqldump: ".$sMoreInfo);
  295. }
  296. }
  297. /**
  298. * Helper to create a ZIP out of a data file and the configuration file
  299. */
  300. protected function DoZip($aFiles, $sZipArchiveFile)
  301. {
  302. foreach ($aFiles as $aFile)
  303. {
  304. $sFile = $aFile['source'];
  305. if (!is_file($sFile) && !is_dir($sFile))
  306. {
  307. throw new BackupException("File '$sFile' does not exist or could not be read");
  308. }
  309. }
  310. // Make sure the target path exists
  311. $sZipDir = dirname($sZipArchiveFile);
  312. SetupUtils::builddir($sZipDir);
  313. $oZip = new ZipArchiveEx();
  314. $res = $oZip->open($sZipArchiveFile, ZipArchive::CREATE | ZipArchive::OVERWRITE);
  315. if ($res === TRUE)
  316. {
  317. foreach ($aFiles as $aFile)
  318. {
  319. if (is_dir($aFile['source']))
  320. {
  321. $oZip->addDir($aFile['source'], $aFile['dest']);
  322. }
  323. else
  324. {
  325. $oZip->addFile($aFile['source'], $aFile['dest']);
  326. }
  327. }
  328. if ($oZip->close())
  329. {
  330. $this->LogInfo("Archive: $sZipArchiveFile created");
  331. }
  332. else
  333. {
  334. $this->LogError("Failed to save zip archive: $sZipArchiveFile");
  335. throw new BackupException("Failed to save zip archive: $sZipArchiveFile");
  336. }
  337. }
  338. else
  339. {
  340. $this->LogError("Failed to create zip archive: $sZipArchiveFile.");
  341. throw new BackupException("Failed to create zip archive: $sZipArchiveFile.");
  342. }
  343. }
  344. /**
  345. * Helper to download the file directly from the browser
  346. */
  347. public function DownloadBackup($sFile)
  348. {
  349. header('Content-Description: File Transfer');
  350. header('Content-Type: multipart/x-zip');
  351. header('Content-Disposition: inline; filename="'.basename($sFile).'"');
  352. header('Expires: 0');
  353. header('Cache-Control: must-revalidate');
  354. header('Pragma: public');
  355. header('Content-Length: '.filesize($sFile));
  356. readfile($sFile);
  357. }
  358. /**
  359. * Helper to open a Database connection
  360. */
  361. protected function DBConnect()
  362. {
  363. if (is_null($this->iDBPort))
  364. {
  365. $oMysqli = new mysqli($this->sDBHost, $this->sDBUser, $this->sDBPwd);
  366. }
  367. else
  368. {
  369. $oMysqli = new mysqli($this->sDBHost, $this->sDBUser, $this->sDBPwd, '', $this->iDBPort);
  370. }
  371. if ($oMysqli->connect_errno)
  372. {
  373. $sHost = is_null($this->iDBPort) ? $this->sDBHost : $this->sDBHost.' on port '.$this->iDBPort;
  374. throw new BackupException("Cannot connect to the MySQL server '$this->sDBHost' (".$oMysqli->connect_errno . ") ".$oMysqli->connect_error);
  375. }
  376. if (!$oMysqli->select_db($this->sDBName))
  377. {
  378. throw new BackupException("The database '$this->sDBName' does not seem to exist");
  379. }
  380. return $oMysqli;
  381. }
  382. /**
  383. * Helper to enumerate the tables of the database
  384. */
  385. protected function EnumerateTables()
  386. {
  387. $oMysqli = $this->DBConnect();
  388. if ($this->sDBSubName != '')
  389. {
  390. $oResult = $oMysqli->query("SHOW TABLES LIKE '{$this->sDBSubName}%'");
  391. }
  392. else
  393. {
  394. $oResult = $oMysqli->query("SHOW TABLES");
  395. }
  396. if (!$oResult)
  397. {
  398. throw new BackupException("Failed to execute the SHOW TABLES query: ".$oMysqli->error);
  399. }
  400. $aTables = array();
  401. while ($aRow = $oResult->fetch_row())
  402. {
  403. $aTables[] = $aRow[0];
  404. }
  405. return $aTables;
  406. }
  407. }
  408. }