1<?php
2// Copyright (C) 2010-2018 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
19require_once('tar.php');
20
21interface BackupArchive
22{
23	/**
24	 * @param string $sFile
25	 *
26	 * @return bool <b>TRUE</b> if the file is present, <b>FALSE</b> otherwise.
27	 */
28	public function hasFile($sFile);
29
30	/**
31	 * @param string $sDirectory
32	 *
33	 * @return bool <b>TRUE</b> if the directory is present, <b>FALSE</b> otherwise.
34	 */
35	public function hasDir($sDirectory);
36
37	/**
38	 * @param string $sDestinationDir
39	 * @param string $sArchiveFile
40	 *
41	 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
42	 */
43	public function extractFileTo($sDestinationDir, $sArchiveFile);
44
45	/**
46	 * Extract a whole directory from the archive.
47	 * Usage: $oArchive->extractDirTo('/var/www/html/itop/data', '/production-modules/')
48	 *
49	 * @param string $sDestinationDir
50	 * @param string $sArchiveDir Note: must start and end with a slash !!
51	 *
52	 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
53	 */
54	public function extractDirTo($sDestinationDir, $sArchiveDir);
55
56	/**
57	 * Returns the entry contents using its name
58	 *
59	 * @param string $name Name of the entry
60	 * @param int $length [optional] The length to be read from the entry. If 0, then the entire entry is read.
61	 * @param int $flags [optional] The flags to use to open the archive. the following values may be ORed to it.
62	 *     <b>ZipArchive::FL_UNCHANGED</b>
63	 *
64	 * @return string the contents of the entry on success or <b>FALSE</b> on failure.
65	 */
66	public function getFromName($name, $length = 0, $flags = null);
67}
68
69if (class_exists('ZipArchive')) // The setup must be able to start even if the "zip" extension is not loaded
70{
71	/**
72	 * Handles adding directories into a Zip archive, and a unified API for archive read
73	 * suggested enhancement: refactor the API for writing as well
74	 */
75	class ZipArchiveEx extends ZipArchive implements BackupArchive
76	{
77		public function addDir($sDir, $sZipDir = '')
78		{
79			if (is_dir($sDir))
80			{
81				if ($dh = opendir($sDir))
82				{
83					// Add the directory
84					if (!empty($sZipDir))
85					{
86						$this->addEmptyDir($sZipDir);
87					}
88
89					// Loop through all the files
90					while (($sFile = readdir($dh)) !== false)
91					{
92						// If it's a folder, run the function again!
93						if (!is_file($sDir.$sFile))
94						{
95							// Skip parent and root directories
96							if (($sFile !== ".") && ($sFile !== ".."))
97							{
98								$this->addDir($sDir.$sFile."/", $sZipDir.$sFile."/");
99							}
100						}
101						else
102						{
103							// Add the files
104							$this->addFile($sDir.$sFile, $sZipDir.$sFile);
105						}
106					}
107				}
108			}
109		}
110
111		/**
112		 * @param string $sFile
113		 *
114		 * @return bool <b>TRUE</b> if the file is present, <b>FALSE</b> otherwise.
115		 */
116		public function hasFile($sFile)
117		{
118			return ($this->locateName($sFile) !== false);
119		}
120
121		/**
122		 * @param string $sDirectory
123		 *
124		 * @return bool <b>TRUE</b> if the directory is present, <b>FALSE</b> otherwise.
125		 */
126		public function hasDir($sDirectory)
127		{
128			return ($this->locateName($sDirectory) !== false);
129		}
130
131		/**
132		 * @param string $sDestinationDir
133		 * @param string $sArchiveFile
134		 *
135		 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
136		 */
137		public function extractFileTo($sDestinationDir, $sArchiveFile)
138		{
139			return $this->extractTo($sDestinationDir, $sArchiveFile);
140		}
141
142		/**
143		 * Extract a whole directory from the archive.
144		 * Usage: $oZip->extractDirTo('/var/www/html/itop/data', '/production-modules/')
145		 *
146		 * @param string $sDestinationDir
147		 * @param string $sZipDir Must start and end with a slash !!
148		 *
149		 * @return boolean
150		 */
151		public function extractDirTo($sDestinationDir, $sZipDir)
152		{
153			$aFiles = array();
154			for ($i = 0; $i < $this->numFiles; $i++)
155			{
156				$sEntry = $this->getNameIndex($i);
157				//Use strpos() to check if the entry name contains the directory we want to extract
158				if (strpos($sEntry, $sZipDir) === 0)
159				{
160					//Add the entry to our array if it in in our desired directory
161					$aFiles[] = $sEntry;
162				}
163			}
164			// Extract only the selected files
165			if ((count($aFiles) > 0) && ($this->extractTo($sDestinationDir, $aFiles) === true))
166			{
167				return true;
168			}
169
170			return false;
171		}
172	} // class ZipArchiveEx
173
174	class BackupException extends Exception
175	{
176	}
177
178	class DBBackup
179	{
180		/**
181		 * utf8mb4 was added in MySQL 5.5.3 but works with programs like mysqldump only since MySQL 5.5.33
182		 *
183		 * @since 2.5 see N°1001
184		 */
185		const MYSQL_VERSION_WITH_UTF8MB4_IN_PROGRAMS = '5.5.33';
186
187		// To be overriden depending on the expected usages
188		protected function LogInfo($sMsg)
189		{
190		}
191
192		protected function LogError($sMsg)
193		{
194		}
195
196		/** @var Config */
197		protected $oConfig;
198
199		// shortcuts used for log purposes
200		/** @var string */
201		protected $sDBHost;
202		/** @var int */
203		protected $iDBPort;
204		/** @var string */
205		protected $sDBName;
206		/** @var string */
207		protected $sDBSubName;
208
209		/**
210		 * Connects to the database to backup
211		 *
212		 * @param Config $oConfig object containing the database configuration.<br>
213		 * If null then uses the default configuration ({@see MetaModel::GetConfig})
214		 *
215		 * @since 2.5 uses a Config object instead of passing each attribute (there were far too many with the addition of MySQL TLS parameters !)
216		 */
217		public function __construct($oConfig = null)
218		{
219			if (is_null($oConfig))
220			{
221				// Defaulting to the current config
222				$oConfig = MetaModel::GetConfig();
223			}
224
225			$this->oConfig = $oConfig;
226
227			// init log variables
228			CMDBSource::InitServerAndPort($oConfig->Get('db_host'), $this->sDBHost, $this->iDBPort);
229			$this->sDBName = $oConfig->get('db_name');
230			$this->sDBSubName = $oConfig->get('db_subname');
231		}
232
233		protected $sMySQLBinDir = '';
234
235		/**
236		 * Create a normalized backup name, depending on the current date/time and Database
237		 *
238		 * @param string sMySQLBinDir  Name and path, eventually containing itop placeholders + time formatting specs
239		 */
240		public function SetMySQLBinDir($sMySQLBinDir)
241		{
242			$this->sMySQLBinDir = $sMySQLBinDir;
243		}
244
245        /**
246         * Create a normalized backup name, depending on the current date/time and Database
247         *
248         * @param string sNameSpec Name and path, eventually containing itop placeholders + time formatting specs
249         *
250         * @return string
251         */
252		public function MakeName($sNameSpec = "__DB__-%Y-%m-%d")
253		{
254			$sFileName = $sNameSpec;
255			$sFileName = str_replace('__HOST__', $this->sDBHost, $sFileName);
256			$sFileName = str_replace('__DB__', $this->sDBName, $sFileName);
257			$sFileName = str_replace('__SUBNAME__', $this->sDBSubName, $sFileName);
258			// Transform %Y, etc.
259			$sFileName = strftime($sFileName);
260
261			return $sFileName;
262		}
263
264		/**
265		 * @deprecated 2.4.0 Zip files are limited to 4 Gb, use CreateCompressedBackup to create tar.gz files
266		 *
267		 * @param string $sZipFile
268		 * @param string|null $sSourceConfigFile
269		 *
270		 * @throws \BackupException
271		 */
272		public function CreateZip($sZipFile, $sSourceConfigFile = null)
273		{
274			$aContents = array();
275
276			// Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS)
277			// (delete it before spawning a process)
278			$sDataFile = tempnam(SetupUtils::GetTmpDir(), 'itop-');
279			$this->LogInfo("Data file: '$sDataFile'");
280			$this->DoBackup($sDataFile);
281			$aContents[] = array(
282				'source' => $sDataFile,
283				'dest' => 'itop-dump.sql',
284			);
285
286			foreach ($this->GetAdditionalFiles($sSourceConfigFile) as $sArchiveFile => $sSourceFile)
287			{
288				$aContents[] = array(
289					'source' => $sSourceFile,
290					'dest' => $sArchiveFile,
291				);
292			}
293
294			$this->DoZip($aContents, $sZipFile);
295
296			// Windows/IIS: the data file has been created by the spawned process...
297			//   trying to delete it will issue a warning, itself stopping the setup abruptely
298			@unlink($sDataFile);
299		}
300
301		/**
302		 * @param string $sTargetFile Path and name, without the extension
303		 * @param string|null $sSourceConfigFile Configuration file to embed into the backup, if not the current one
304		 *
305		 * @throws \Exception
306		 */
307		public function CreateCompressedBackup($sTargetFile, $sSourceConfigFile = null)
308		{
309			$this->LogInfo("Creating backup: '$sTargetFile.tar.gz'");
310
311			$oArchive = new ArchiveTar($sTargetFile.'.tar.gz');
312
313			$sTmpFolder = APPROOT.'data/tmp-backup-'.rand(10000, getrandmax());
314			$aFiles = $this->PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder);
315
316			$sFilesList = var_export($aFiles, true);
317			$this->LogInfo("backup: adding to archive files '$sFilesList'");
318			$oArchive->createModify($aFiles, '', $sTmpFolder);
319
320			$this->LogInfo("backup: removing tmp folder '$sTmpFolder'");
321			SetupUtils::rrmdir($sTmpFolder);
322		}
323
324		/**
325		 * Copy files to store into the temporary folder, in addition to the SQL dump
326		 *
327		 * @param string $sSourceConfigFile
328		 * @param string $sTmpFolder
329		 *
330		 * @return array list of files to archive
331		 * @throws \Exception
332		 */
333		protected function PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder)
334		{
335			$aRet = array();
336			if (is_dir($sTmpFolder))
337			{
338				SetupUtils::rrmdir($sTmpFolder);
339			}
340			$this->LogInfo("backup: creating tmp dir '$sTmpFolder'");
341			@mkdir($sTmpFolder, 0777, true);
342			if (is_null($sSourceConfigFile))
343			{
344				$sSourceConfigFile = MetaModel::GetConfig()->GetLoadedFile();
345			}
346			if (!empty($sSourceConfigFile))
347			{
348				$sFile = $sTmpFolder.'/config-itop.php';
349				$this->LogInfo("backup: adding resource '$sSourceConfigFile'");
350				copy($sSourceConfigFile, $sFile);
351				$aRet[] = $sFile;
352			}
353
354			$sDeltaFile = APPROOT.'data/'.utils::GetCurrentEnvironment().'.delta.xml';
355			if (file_exists($sDeltaFile))
356			{
357				$sFile = $sTmpFolder.'/delta.xml';
358				$this->LogInfo("backup: adding resource '$sDeltaFile'");
359				copy($sDeltaFile, $sFile);
360				$aRet[] = $sFile;
361			}
362			$sExtraDir = APPROOT.'data/'.utils::GetCurrentEnvironment().'-modules/';
363			if (is_dir($sExtraDir))
364			{
365				$sModules = utils::GetCurrentEnvironment().'-modules';
366				$sFile = $sTmpFolder.'/'.$sModules;
367				$this->LogInfo("backup: adding resource '$sExtraDir'");
368				SetupUtils::copydir($sExtraDir, $sFile);
369				$aRet[] = $sFile;
370			}
371			$sDataFile = $sTmpFolder.'/itop-dump.sql';
372			$this->DoBackup($sDataFile);
373			$aRet[] = $sDataFile;
374
375			return $aRet;
376		}
377
378		protected static function EscapeShellArg($sValue)
379		{
380			// Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation
381			//    It suggests to rely on pctnl_* function instead of using escapeshellargs
382			return escapeshellarg($sValue);
383		}
384
385		/**
386		 * Create a backup file
387		 *
388		 * @param string $sBackupFileName
389		 *
390		 * @throws \BackupException
391		 */
392		public function DoBackup($sBackupFileName)
393		{
394			$sHost = self::EscapeShellArg($this->sDBHost);
395			$sUser = self::EscapeShellArg($this->oConfig->Get('db_user'));
396			$sPwd = self::EscapeShellArg($this->oConfig->Get('db_pwd'));
397			$sDBName = self::EscapeShellArg($this->sDBName);
398
399			// Just to check the connection to the DB (better than getting the retcode of mysqldump = 1)
400			$this->DBConnect();
401
402			$sTables = '';
403			if ($this->sDBSubName != '')
404			{
405				// This instance of iTop uses a prefix for the tables, so there may be other tables in the database
406				// Let's explicitely list all the tables and views to dump
407				$aTables = $this->EnumerateTables();
408				if (count($aTables) == 0)
409				{
410					// No table has been found with the given prefix
411					throw new BackupException("No table has been found with the given prefix");
412				}
413				$aEscapedTables = array();
414				foreach ($aTables as $sTable)
415				{
416					$aEscapedTables[] = self::EscapeShellArg($sTable);
417				}
418				$sTables = implode(' ', $aEscapedTables);
419			}
420
421			$this->LogInfo("Starting backup of $this->sDBHost/$this->sDBName(suffix:'$this->sDBSubName')");
422
423			$sMySQLDump = $this->GetMysqldumpCommand();
424
425			// Store the results in a temporary file
426			$sTmpFileName = self::EscapeShellArg($sBackupFileName);
427
428			$sPortOption = self::GetMysqliCliSingleOption('port', $this->iDBPort);
429			$sTlsOptions = self::GetMysqlCliTlsOptions($this->oConfig);
430
431			$sMysqldumpVersion = self::GetMysqldumpVersion($sMySQLDump);
432			$bIsMysqldumpSupportUtf8mb4 = (version_compare($sMysqldumpVersion,
433					self::MYSQL_VERSION_WITH_UTF8MB4_IN_PROGRAMS) == -1);
434			$sMysqldumpCharset = $bIsMysqldumpSupportUtf8mb4 ? 'utf8' : DEFAULT_CHARACTER_SET;
435
436			// Delete the file created by tempnam() so that the spawned process can write into it (Windows/IIS)
437			@unlink($sBackupFileName);
438			// Note: opt implicitely sets lock-tables... which cancels the benefit of single-transaction!
439			//       skip-lock-tables compensates and allows for writes during a backup
440			$sCommand = "$sMySQLDump --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption --user=$sUser --password=$sPwd $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables 2>&1";
441			$sCommandDisplay = "$sMySQLDump --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption --user=xxxxx --password=xxxxx $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables";
442
443			// Now run the command for real
444			$this->LogInfo("backup: generate data file with command: $sCommandDisplay");
445			$aOutput = array();
446			$iRetCode = 0;
447			exec($sCommand, $aOutput, $iRetCode);
448			foreach ($aOutput as $sLine)
449			{
450				$this->LogInfo("mysqldump said: $sLine");
451			}
452			if ($iRetCode != 0)
453			{
454				// Cleanup residual output (Happens with Error 2020: Got packet bigger than 'maxallowedpacket' bytes...)
455				if (file_exists($sBackupFileName))
456				{
457					unlink($sBackupFileName);
458				}
459
460				$this->LogError("Failed to execute: $sCommandDisplay. The command returned:$iRetCode");
461				foreach ($aOutput as $sLine)
462				{
463					$this->LogError("mysqldump said: $sLine");
464				}
465				if (count($aOutput) == 1)
466				{
467					$sMoreInfo = trim($aOutput[0]);
468				}
469				else
470				{
471					$sMoreInfo = "Check the log files '".realpath(APPROOT.'/log/setup.log or error.log')."' for more information.";
472				}
473				throw new BackupException("Failed to execute mysqldump: ".$sMoreInfo);
474			}
475		}
476
477		/**
478		 * Helper to create a ZIP out of several files
479		 *
480		 * @param array $aFiles
481		 * @param string $sZipArchiveFile
482		 *
483		 * @throws \BackupException
484		 */
485		protected function DoZip($aFiles, $sZipArchiveFile)
486		{
487			foreach ($aFiles as $aFile)
488			{
489				$sFile = $aFile['source'];
490				if (!is_file($sFile) && !is_dir($sFile))
491				{
492					throw new BackupException("File '$sFile' does not exist or could not be read");
493				}
494			}
495			// Make sure the target path exists
496			$sZipDir = dirname($sZipArchiveFile);
497			SetupUtils::builddir($sZipDir);
498
499			$oZip = new ZipArchiveEx();
500			$res = $oZip->open($sZipArchiveFile, ZipArchive::CREATE | ZipArchive::OVERWRITE);
501			if ($res === true)
502			{
503				foreach ($aFiles as $aFile)
504				{
505					if (is_dir($aFile['source']))
506					{
507						$oZip->addDir($aFile['source'], $aFile['dest']);
508					}
509					else
510					{
511						$oZip->addFile($aFile['source'], $aFile['dest']);
512					}
513				}
514				if ($oZip->close())
515				{
516					$this->LogInfo("Archive: $sZipArchiveFile created");
517				}
518				else
519				{
520					$this->LogError("Failed to save zip archive: $sZipArchiveFile");
521					throw new BackupException("Failed to save zip archive: $sZipArchiveFile");
522				}
523			}
524			else
525			{
526				$this->LogError("Failed to create zip archive: $sZipArchiveFile.");
527				throw new BackupException("Failed to create zip archive: $sZipArchiveFile.");
528			}
529		}
530
531		/**
532		 * Helper to download the file directly from the browser
533		 *
534		 * @param string $sFile
535		 */
536		public function DownloadBackup($sFile)
537		{
538			if (file_exists($sFile))
539			{
540				header('Content-Description: File Transfer');
541				header('Content-Type: multipart/x-zip');
542				header('Content-Disposition: inline; filename="'.basename($sFile).'"');
543				header('Expires: 0');
544				header('Cache-Control: must-revalidate');
545				header('Pragma: public');
546				header('Content-Length: '.filesize($sFile));
547				readfile($sFile) ;
548			}
549			else
550			{
551				throw new InvalidParameterException('Invalid file path');
552			}
553		}
554
555		/**
556		 * Helper to open a Database connection
557		 *
558		 * @return \mysqli
559		 * @throws \BackupException
560		 * @uses CMDBSource
561		 */
562		protected function DBConnect()
563		{
564			$oConfig = $this->oConfig;
565			$sServer = $oConfig->Get('db_host');
566			$sUser = $oConfig->Get('db_user');
567			$sPwd = $oConfig->Get('db_pwd');
568			$sSource = $oConfig->Get('db_name');
569			$sTlsEnabled = $oConfig->Get('db_tls.enabled');
570			$sTlsCA = $oConfig->Get('db_tls.ca');
571
572			try
573			{
574				$oMysqli = CMDBSource::GetMysqliInstance($sServer, $sUser, $sPwd, $sSource, $sTlsEnabled, $sTlsCA,
575					false);
576
577				if ($oMysqli->connect_errno)
578				{
579					$sHost = is_null($this->iDBPort) ? $this->sDBHost : $this->sDBHost.' on port '.$this->iDBPort;
580					throw new BackupException("Cannot connect to the MySQL server '$sHost' (".$oMysqli->connect_errno.") ".$oMysqli->connect_error);
581				}
582				if (!$oMysqli->select_db($this->sDBName))
583				{
584					throw new BackupException("The database '$this->sDBName' does not seem to exist");
585				}
586
587				return $oMysqli;
588			}
589			catch (MySQLException $e)
590			{
591				throw new BackupException($e->getMessage());
592			}
593		}
594
595		/**
596		 * Helper to enumerate the tables of the database
597		 *
598		 * @throws \BackupException
599		 */
600		protected function EnumerateTables()
601		{
602			$oMysqli = $this->DBConnect();
603			if ($this->sDBSubName != '')
604			{
605				$oResult = $oMysqli->query("SHOW TABLES LIKE '{$this->sDBSubName}%'");
606			}
607			else
608			{
609				$oResult = $oMysqli->query("SHOW TABLES");
610			}
611			if (!$oResult)
612			{
613				throw new BackupException("Failed to execute the SHOW TABLES query: ".$oMysqli->error);
614			}
615			$aTables = array();
616			while ($aRow = $oResult->fetch_row())
617			{
618				$aTables[] = $aRow[0];
619			}
620
621			return $aTables;
622		}
623
624
625		/**
626		 * @param Config $oConfig
627		 *
628		 * @return string TLS arguments for CLI programs such as mysqldump. Empty string if the config does not use TLS.
629		 *
630		 * @see https://dev.mysql.com/doc/refman/5.6/en/encrypted-connection-options.html
631		 * @since 2.5
632		 */
633		public static function GetMysqlCliTlsOptions($oConfig)
634		{
635			$bDbTlsEnabled = $oConfig->Get('db_tls.enabled');
636			if (!$bDbTlsEnabled)
637			{
638				return '';
639			}
640
641			$sTlsOptions = '';
642			$sTlsOptions .= ' --ssl';
643
644			// ssl-key parameter : not implemented
645			// ssl-cert parameter : not implemented
646
647			$sTlsOptions .= self::GetMysqliCliSingleOption('ssl-ca', $oConfig->Get('db_tls.ca'));
648
649			// ssl-cipher parameter : not implemented
650			// ssl-capath parameter : not implemented
651
652			return $sTlsOptions;
653		}
654
655		/**
656		 * @param string $sCliArgName
657		 * @param string $sData
658		 *
659		 * @return string empty if data is empty, else argument in form of ' --cliargname=data'
660		 */
661		private static function GetMysqliCliSingleOption($sCliArgName, $sData)
662		{
663			if (empty($sData))
664			{
665				return '';
666			}
667
668			return ' --'.$sCliArgName.'='.self::EscapeShellArg($sData);
669		}
670
671		/**
672		 * @return string the command to launch mysqldump (without its params)
673		 */
674		private function GetMysqldumpCommand()
675		{
676			$sMySQLBinDir = utils::ReadParam('mysql_bindir', $this->sMySQLBinDir, true);
677			if (empty($sMySQLBinDir))
678			{
679				$sMysqldumpCommand = 'mysqldump';
680			}
681			else
682			{
683				$sMysqldumpCommand = '"'.$sMySQLBinDir.'/mysqldump"';
684			}
685
686			return $sMysqldumpCommand;
687		}
688
689		/**
690		 * @param string $sMysqldumpCommand
691		 *
692		 * @return string version of the mysqldump program, as parsed from program return
693		 *
694		 * @uses mysqldump -V Sample return value : mysqldump  Ver 10.13 Distrib 5.7.19, for Win64 (x86_64)
695		 * @since 2.5 needed to check compatibility with utf8mb4 (N°1001)
696		 * @throws \BackupException
697		 */
698		private static function GetMysqldumpVersion($sMysqldumpCommand)
699		{
700			$sCommand = $sMysqldumpCommand.' -V';
701			$aOutput = array();
702			exec($sCommand, $aOutput, $iRetCode);
703
704			if ($iRetCode != 0)
705			{
706				throw new BackupException("mysqldump could not be executed (retcode=$iRetCode): Please make sure it is installed and located at : $sMysqldumpCommand");
707			}
708
709			$sMysqldumpOutput = $aOutput[0];
710			$aDumpVersionMatchResults = array();
711			preg_match('/Distrib (\d+\.\d+\.\d+)/', $sMysqldumpOutput, $aDumpVersionMatchResults);
712
713			return $aDumpVersionMatchResults[1];
714		}
715	}
716}
717
718class TarGzArchive implements BackupArchive
719{
720	/*
721	 * @var ArchiveTar
722	 */
723	protected $oArchive;
724	/*
725	 * string[]
726	 */
727	protected $aFiles = null;
728
729	public function __construct($sFile)
730	{
731		$this->oArchive = new ArchiveTar($sFile);
732	}
733
734	/**
735	 * @param string $sFile
736	 *
737	 * @return bool <b>TRUE</b> if the file is present, <b>FALSE</b> otherwise.
738	 */
739	public function hasFile($sFile)
740	{
741		// remove leading and tailing /
742		$sFile = trim($sFile, "/ \t\n\r\0\x0B");
743		if ($this->aFiles === null)
744		{
745			// Initial load
746			$this->buildFileList();
747		}
748		foreach ($this->aFiles as $aArchFile)
749		{
750			if ($aArchFile['filename'] == $sFile)
751			{
752				return true;
753			}
754		}
755
756		return false;
757	}
758
759	/**
760	 * @param string $sDirectory
761	 *
762	 * @return bool <b>TRUE</b> if the directory is present, <b>FALSE</b> otherwise.
763	 */
764	public function hasDir($sDirectory)
765	{
766		// remove leading and tailing /
767		$sDirectory = trim($sDirectory, "/ \t\n\r\0\x0B");
768		if ($this->aFiles === null)
769		{
770			// Initial load
771			$this->buildFileList();
772		}
773		foreach ($this->aFiles as $aArchFile)
774		{
775			if (($aArchFile['typeflag'] == 5) && ($aArchFile['filename'] == $sDirectory))
776			{
777				return true;
778			}
779		}
780
781		return false;
782	}
783
784	/**
785	 * @param string $p_path
786	 * @param null $aEntries
787	 *
788	 * @return bool
789	 */
790	public function extractTo($p_path = '', $aEntries = null)
791	{
792		return $this->oArchive->extract($p_path);
793	}
794
795	/**
796	 * @param string $sDestinationDir
797	 * @param string $sArchiveFile
798	 *
799	 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
800	 */
801	public function extractFileTo($sDestinationDir, $sArchiveFile)
802	{
803		return $this->oArchive->extractList($sArchiveFile, $sDestinationDir);
804	}
805
806	/**
807	 * Extract a whole directory from the archive.
808	 * Usage: $oArchive->extractDirTo('/var/www/html/itop/data', '/production-modules/')
809	 *
810	 * @param string $sDestinationDir
811	 * @param string $sArchiveDir
812	 *
813	 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
814	 */
815	public function extractDirTo($sDestinationDir, $sArchiveDir)
816	{
817		return $this->oArchive->extractList($sArchiveDir, $sDestinationDir);
818	}
819
820	/**
821	 * Returns the entry contents using its name
822	 *
823	 * @param string $name Name of the entry
824	 * @param int $length unused.
825	 * @param int $flags unused.
826	 *
827	 * @return string the contents of the entry on success or <b>FALSE</b> on failure.
828	 */
829	public function getFromName($name, $length = 0, $flags = null)
830	{
831		return $this->oArchive->extractInString($name);
832	}
833
834	/**
835	 */
836	protected function buildFileList()
837	{
838		$this->aFiles = $this->oArchive->listContent();
839	}
840}
841
842