* @author Romain Quetiez * @author Denis Flaven * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL */ /** * iTopArchive a class to manipulate (read/write) iTop archives with their catalog * Each iTop archive is a zip file that contains (at the root of the archive) * a file called catalog.xml holding the description of the archive */ class iTopArchive { const read = 0; const create = ZipArchive::CREATE; protected $m_sZipPath; protected $m_oZip; protected $m_sVersion; protected $m_sTitle; protected $m_sDescription; protected $m_aPackages; protected $m_aErrorMessages; /** * Construct an iTopArchive object * @param $sArchivePath string The full path the archive file * @param $iMode integrer Either iTopArchive::read for reading an existing archive or iTopArchive::create for creating a new one. Updating is not supported (yet) */ public function __construct($sArchivePath, $iMode = iTopArchive::read) { $this->m_sZipPath = $sArchivePath; $this->m_oZip = new ZipArchive(); $this->m_oZip->open($this->m_sZipPath, $iMode); $this->m_aErrorMessages = array(); $this->m_sVersion = '1.0'; $this->m_sTitle = ''; $this->m_sDescription = ''; $this->m_aPackages = array(); } public function SetTitle($sTitle) { $this->m_sTitle = $sTitle; } public function SetDescription($sDescription) { $this->m_sDescription = $sDescription; } public function GetTitle() { return $this->m_sTitle; } public function GetDescription() { return $this->m_sDescription; } public function GetPackages() { return $this->m_aPackages; } public function __destruct() { $this->m_oZip->close(); } /** * Get the error message explaining the latest error encountered * @return array All the error messages encountered during the validation */ public function GetErrors() { return $this->m_aErrorMessages; } /** * Read the catalog from the archive (zip) file * @param sPath string Path the the zip file * @return boolean True in case of success, false otherwise */ public function ReadCatalog() { if ($this->IsValid()) { $sXmlCatalog = $this->m_oZip->getFromName('catalog.xml'); $oParser = xml_parser_create(); xml_parse_into_struct($oParser, $sXmlCatalog, $aValues, $aIndexes); xml_parser_free($oParser); $iIndex = $aIndexes['ARCHIVE'][0]; $this->m_sVersion = $aValues[$iIndex]['attributes']['VERSION']; $iIndex = $aIndexes['TITLE'][0]; $this->m_sTitle = $aValues[$iIndex]['value']; $iIndex = $aIndexes['DESCRIPTION'][0]; if (array_key_exists('value', $aValues[$iIndex])) { // #@# implement a get_array_value(array, key, default) ? $this->m_sDescription = $aValues[$iIndex]['value']; } foreach($aIndexes['PACKAGE'] as $iIndex) { $this->m_aPackages[$aValues[$iIndex]['attributes']['HREF']] = array( 'type' => $aValues[$iIndex]['attributes']['TYPE'], 'title'=> $aValues[$iIndex]['attributes']['TITLE'], 'description' => $aValues[$iIndex]['value']); } //echo "Archive path: {$this->m_sZipPath}
\n"; //echo "Archive format version: {$this->m_sVersion}
\n"; //echo "Title: {$this->m_sTitle}
\n"; //echo "Description: {$this->m_sDescription}
\n"; //foreach($this->m_aPackages as $aFile) //{ // echo "{$aFile['title']} ({$aFile['type']}): {$aFile['description']}
\n"; //} } return true; } public function WriteCatalog() { $sXml = "\n"; // split the XML closing tag that disturbs PSPad's syntax coloring $sXml .= "\n"; $sXml .= "{$this->m_sTitle}\n"; $sXml .= "{$this->m_sDescription}\n"; foreach($this->m_aPackages as $sFileName => $aFile) { $sXml .= "{$aFile['description']}\n"; } $sXml .= ""; $this->m_oZip->addFromString('catalog.xml', $sXml); } /** * Add a package to the archive * @param string $sExternalFilePath The path to the file to be added to the archive as a package (directories are not yet implemented) * @param string $sFilePath The name of the file inside the archive * @param string $sTitle A short title for this package * @param string $sType Type of the package. SQL scripts must be of type 'text/sql' * @param string $sDescription A longer description of the purpose of this package * @return none */ public function AddPackage($sExternalFilePath, $sFilePath, $sTitle, $sType, $sDescription) { $this->m_aPackages[$sFilePath] = array('title' => $sTitle, 'type' => $sType, 'description' => $sDescription); $this->m_oZip->addFile($sExternalFilePath, $sFilePath); } /** * Reads the contents of the given file from the archive * @param string $sFileName The path to the file inside the archive * @return string The content of the file read from the archive */ public function GetFileContents($sFileName) { return $this->m_oZip->getFromName($sFileName); } /** * Extracts the contents of the given file from the archive * @param string $sFileName The path to the file inside the archive * @param string $sDestinationFileName The path of the file to write * @return none */ public function ExtractToFile($sFileName, $sDestinationFileName) { $iBufferSize = 64 * 1024; // Read 64K at a time $oZipStream = $this->m_oZip->getStream($sFileName); $oDestinationStream = fopen($sDestinationFileName, 'wb'); while (!feof($oZipStream)) { $sContents = fread($oZipStream, $iBufferSize); fwrite($oDestinationStream, $sContents); } fclose($oZipStream); fclose($oDestinationStream); } /** * Apply a SQL script taken from the archive. The package must be listed in the catalog and of type text/sql * @param string $sFileName The path to the SQL package inside the archive * @return boolean false in case of error, true otherwise */ public function ImportSql($sFileName, $sDatabase = 'itop') { if ( ($this->m_oZip->locateName($sFileName) == false) || (!isset($this->m_aPackages[$sFileName])) || ($this->m_aPackages[$sFileName]['type'] != 'text/sql')) { // invalid type or not listed in the catalog return false; } $sTempName = tempnam("../tmp/", "sql"); //echo "Extracting to: '$sTempName'
\n"; $this->ExtractToFile($sFileName, $sTempName); // Note: the command line below works on Windows with the right path to mysql !!! $sCommandLine = 'type "'.$sTempName.'" | "/iTop/MySQL Server 5.0/bin/mysql.exe" -u root '.$sDatabase; //echo "Executing: '$sCommandLine'
\n"; exec($sCommandLine, $aOutput, $iRet); //echo "Return code: $iRet
\n"; //echo "Output:
\n";
		//print_r($aOutput);
		//echo "

\n"; unlink($sTempName); return ($iRet == 0); } /** * Dumps some part of the specified MySQL database into the archive as a text/sql package * @param $sTitle string A short title for this SQL script * @param $sDescription string A longer description of the purpose of this SQL script * @param $sFileName string The name of the package inside the archive * @param $sDatabase string name of the database * @param $aTables array array or table names. If empty, all tables are dumped * @param $bStructureOnly boolean Whether or not to dump the data or just the schema * @return boolean False in case of error, true otherwise */ public function AddDatabaseDump($sTitle, $sDescription, $sFileName, $sDatabase = 'itop', $aTables = array(), $bStructureOnly = true) { $sTempName = tempnam("../tmp/", "sql"); $sNoData = $bStructureOnly ? "--no-data" : ""; $sCommandLine = "\"/iTop/MySQL Server 5.0/bin/mysqldump.exe\" --user=root --opt $sNoData --result-file=$sTempName $sDatabase ".implode(" ", $aTables); //echo "Executing command: '$sCommandLine'
\n"; exec($sCommandLine, $aOutput, $iRet); //echo "Return code: $iRet
\n"; //echo "Output:
\n";
		//print_r($aOutput);
		//echo "

\n"; if ($iRet == 0) { $this->AddPackage($sTempName, $sFileName, $sTitle, 'text/sql', $sDescription); } //unlink($sTempName); return ($iRet == 0); } /** * Check the consistency of the archive * @return boolean True if the archive file is consistent */ public function IsValid() { // TO DO: use a DTD to validate the XML instead of this hand-made validation $bResult = true; $aMandatoryTags = array('ARCHIVE' => array('VERSION'), 'TITLE' => array(), 'DESCRIPTION' => array(), 'PACKAGE' => array('TYPE', 'HREF', 'TITLE')); $sXmlCatalog = $this->m_oZip->getFromName('catalog.xml'); $oParser = xml_parser_create(); xml_parse_into_struct($oParser, $sXmlCatalog, $aValues, $aIndexes); xml_parser_free($oParser); foreach($aMandatoryTags as $sTag => $aAttributes) { // Check that all the required tags are present if (!isset($aIndexes[$sTag])) { $this->m_aErrorMessages[] = "The XML catalog does not contain the mandatory tag $sTag."; $bResult = false; } else { foreach($aIndexes[$sTag] as $iIndex) { switch($aValues[$iIndex]['type']) { case 'complete': case 'open': // Check that all the required attributes are present foreach($aAttributes as $sAttribute) { if (!isset($aValues[$iIndex]['attributes'][$sAttribute])) { $this->m_aErrorMessages[] = "The tag $sTag ($iIndex) does not contain the required attribute $sAttribute."; } } break; default: // ignore other type of tags: close or cdata } } } } return $bResult; } } /* // Unit test - reading an archive $sArchivePath = '../tmp/archive.zip'; $oArchive = new iTopArchive($sArchivePath, iTopArchive::read); $oArchive->ReadCatalog(); $oArchive->ImportSql('full_backup.sql'); // Writing an archive -- $sArchivePath = '../tmp/archive2.zip'; $oArchive = new iTopArchive($sArchivePath, iTopArchive::create); $oArchive->SetTitle('First Archive !'); $oArchive->SetDescription('This is just a test. Does not contain a lot of useful data.'); $oArchive->AddPackage('../tmp/schema.sql', 'test.sql', 'this is just a test', 'text/sql', 'My first attempt at creating an archive from PHP...'); $oArchive->WriteCatalog(); $sArchivePath = '../tmp/archive2.zip'; $oArchive = new iTopArchive($sArchivePath, iTopArchive::create); $oArchive->SetTitle('First Archive !'); $oArchive->SetDescription('This is just a test. Does not contain a lot of useful data.'); $oArchive->AddDatabaseDump('Test', 'This is my first automatic dump', 'schema.sql', 'itop', array('objects')); $oArchive->WriteCatalog(); */ ?>