PageRenderTime 60ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/administrator/components/com_akeeba/akeeba/abstract/dump.php

https://bitbucket.org/kraymitchell/apex
PHP | 660 lines | 426 code | 90 blank | 144 comment | 71 complexity | 0333e5ce199f45bfb2d083fb4d9bb652 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-3.0, BSD-3-Clause, LGPL-2.1, GPL-3.0
  1. <?php
  2. /**
  3. * Akeeba Engine
  4. * The modular PHP5 site backup engine
  5. * @copyright Copyright (c)2009-2012 Nicholas K. Dionysopoulos
  6. * @license GNU GPL version 3 or, at your option, any later version
  7. * @package akeebaengine
  8. *
  9. */
  10. // Protection against direct access
  11. defined('AKEEBAENGINE') or die('Restricted access');
  12. abstract class AEAbstractDump extends AEAbstractPart
  13. {
  14. // **********************************************************************
  15. // Configuration parameters
  16. // **********************************************************************
  17. /** @var string Prefix to this database */
  18. protected $prefix = '';
  19. /** @var string MySQL database server host name or IP address */
  20. protected $host = '';
  21. /** @var string MySQL database server port (optional) */
  22. protected $port = '';
  23. /** @var string MySQL user name, for authentication */
  24. protected $username = '';
  25. /** @var string MySQL password, for authentication */
  26. protected $password = '';
  27. /** @var string MySQL database */
  28. protected $database = '';
  29. /** @var string The database driver to use */
  30. protected $driver = '';
  31. // **********************************************************************
  32. // File handling fields
  33. // **********************************************************************
  34. /** @var string Absolute path to dump file; must be writable (optional; if left blank it is automatically calculated) */
  35. protected $dumpFile = '';
  36. /** @var string Data cache, used to cache data before being written to disk */
  37. protected $data_cache = '';
  38. /** @var int Size of the data cache, default 128Kb */
  39. protected $cache_size = 131072;
  40. /** @var bool Should I process empty prefixes when creating abstracted names? */
  41. protected $processEmptyPrefix = true;
  42. /** @var int Current dump file part number */
  43. public $partNumber = 0;
  44. /** @var resource Filepointer to the current dump part */
  45. private $fp = null;
  46. /** @var string Absolute path to the temp file */
  47. protected $tempFile = '';
  48. /** @var string Relative path of how the file should be saved in the archive */
  49. protected $saveAsName = '';
  50. // **********************************************************************
  51. // Protected fields (data handling)
  52. // **********************************************************************
  53. /** @var array Contains the sorted (by dependencies) list of tables/views to backup */
  54. protected $tables = array();
  55. /** @var array Contains the configuration data of the tables */
  56. protected $tables_data = array();
  57. /** @var array Maps database table names to their abstracted format */
  58. protected $table_name_map = array();
  59. /** @var array Contains the dependencies of tables and views (temporary) */
  60. protected $dependencies = array();
  61. /** @var string The next table to backup */
  62. protected $nextTable;
  63. /** @var integer The next row of the table to start backing up from */
  64. protected $nextRange;
  65. /** @var integer Current table's row count */
  66. protected $maxRange;
  67. /** @var bool Use extended INSERTs */
  68. protected $extendedInserts = false;
  69. /** @var integer Maximum packet size for extended INSERTs, in bytes */
  70. protected $packetSize = 0;
  71. /** @var string Extended INSERT query, while it's being constructed */
  72. protected $query = '';
  73. /** @var int Dump part's maximum size */
  74. protected $partSize = 0;
  75. /**
  76. * Find where to store the backup files
  77. * @param $partNumber int The SQL part number, default is 0 (.sql)
  78. */
  79. protected function getBackupFilePaths( $partNumber = 0 )
  80. {
  81. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, 'XXX '.__CLASS__." :: Getting temporary file");
  82. $this->tempFile = AEUtilTempfiles::registerTempFile( dechex(crc32(microtime())).'.sql' );
  83. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, 'XXX '.__CLASS__." :: Temporary file is {$this->tempFile}");
  84. // Get the base name of the dump file
  85. $partNumber = intval($partNumber);
  86. $baseName = $this->dumpFile;
  87. if($partNumber > 0)
  88. {
  89. // The file names are in the format dbname.sql, dbname.s01, dbname.s02, etc
  90. if( strtolower(substr($baseName, -4)) == '.sql' )
  91. {
  92. $baseName = substr($baseName, 0, -4).'.s'.sprintf('%02u', $partNumber);
  93. }
  94. else
  95. {
  96. $baseName = $baseName.'.s'.sprintf('%02u', $partNumber);
  97. }
  98. }
  99. if(empty($this->installerSettings)) {
  100. // Fetch the installer settings
  101. $this->installerSettings = (object)array(
  102. 'installerroot' => 'installation',
  103. 'sqlroot' => 'installation/sql',
  104. 'databasesini' => 1,
  105. 'readme' => 1,
  106. 'extrainfo' => 1
  107. );
  108. $config = AEFactory::getConfiguration();
  109. $installerKey = $config->get('akeeba.advanced.embedded_installer');
  110. $installerDescriptors = AEUtilInihelper::getInstallerList();
  111. if(array_key_exists($installerKey, $installerDescriptors)) {
  112. // The selected installer exists, use it
  113. $this->installerSettings = (object)$installerDescriptors[$installerKey];
  114. } elseif(array_key_exists('abi', $installerDescriptors)) {
  115. // The selected installer doesn't exist, but ABI exists; use that instead
  116. $this->installerSettings = (object)$installerDescriptors['abi'];
  117. }
  118. }
  119. switch(AEUtilScripting::getScriptingParameter('db.saveasname','normal'))
  120. {
  121. case 'output':
  122. // The SQL file will be stored uncompressed in the output directory
  123. $statistics = AEFactory::getStatistics();
  124. $statRecord = $statistics->getRecord();
  125. $this->saveAsName = $statRecord['absolute_path'];
  126. break;
  127. case 'normal':
  128. // The SQL file will be stored in the SQL root of the archive, as
  129. // specified by the particular embedded installer's settings
  130. $this->saveAsName = $this->installerSettings->sqlroot.'/'.$baseName;
  131. break;
  132. case 'short':
  133. // The SQL file will be stored on archive's root
  134. $this->saveAsName = $baseName;
  135. break;
  136. }
  137. if($partNumber > 0)
  138. {
  139. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "AkeebaDomainDBBackup :: Creating new SQL dump part #$partNumber");
  140. }
  141. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "AkeebaDomainDBBackup :: SQL temp file is " . $this->tempFile);
  142. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "AkeebaDomainDBBackup :: SQL file location in archive is " . $this->saveAsName);
  143. }
  144. /**
  145. * Deletes any leftover files from previous backup attempts
  146. *
  147. */
  148. protected function removeOldFiles()
  149. {
  150. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "AkeebaDomainDBBackup :: Deleting leftover files, if any");
  151. if( file_exists( $this->tempFile ) ) @unlink( $this->tempFile );
  152. }
  153. /**
  154. * Populates the table arrays with the information for the db entities to backup
  155. *
  156. * @return null
  157. */
  158. protected abstract function getTablesToBackup();
  159. /**
  160. * Runs a step of the database dump
  161. *
  162. * @return null
  163. */
  164. protected abstract function stepDatabaseDump();
  165. /**
  166. * Implements the _prepare abstract method
  167. *
  168. */
  169. protected function _prepare()
  170. {
  171. // Process parameters, passed to us using the setup() public method
  172. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__." :: Processing parameters");
  173. if( is_array($this->_parametersArray) ) {
  174. $this->driver = array_key_exists('driver', $this->_parametersArray) ? $this->_parametersArray['driver'] : $this->driver;
  175. $this->host = array_key_exists('host', $this->_parametersArray) ? $this->_parametersArray['host'] : $this->host;
  176. $this->port = array_key_exists('port', $this->_parametersArray) ? $this->_parametersArray['port'] : $this->port;
  177. $this->username = array_key_exists('username', $this->_parametersArray) ? $this->_parametersArray['username'] : $this->username;
  178. $this->username = array_key_exists('user', $this->_parametersArray) ? $this->_parametersArray['user'] : $this->username;
  179. $this->password = array_key_exists('password', $this->_parametersArray) ? $this->_parametersArray['password'] : $this->password;
  180. $this->database = array_key_exists('database', $this->_parametersArray) ? $this->_parametersArray['database'] : $this->database;
  181. $this->prefix = array_key_exists('prefix', $this->_parametersArray) ? $this->_parametersArray['prefix'] : $this->prefix;
  182. $this->dumpFile = array_key_exists('dumpFile', $this->_parametersArray) ? $this->_parametersArray['dumpFile'] : $this->dumpFile;
  183. $this->processEmptyPrefix = array_key_exists('process_empty_prefix', $this->_parametersArray) ? $this->_parametersArray['process_empty_prefix'] : $this->processEmptyPrefix;
  184. }
  185. // Make sure we have self-assigned the first part
  186. $this->partNumber = 0;
  187. // Get DB backup only mode
  188. $configuration = AEFactory::getConfiguration();
  189. // Find tables to be included and put them in the $_tables variable
  190. $this->getTablesToBackup();
  191. if($this->getError()) return;
  192. // Find where to store the database backup files
  193. $this->getBackupFilePaths($this->partNumber);
  194. // Remove any leftovers
  195. $this->removeOldFiles();
  196. // Initialize the extended INSERTs feature
  197. $this->extendedInserts = ($configuration->get('engine.dump.common.extended_inserts', 0) != 0);
  198. $this->packetSize = $configuration->get('engine.dump.common.packet_size', 0);
  199. if( $this->packetSize == 0 ) $this->extendedInserts = false;
  200. // Initialize the split dump feature
  201. $this->partSize = $configuration->get('engine.dump.common.splitsize', 1048756);
  202. if( AEUtilScripting::getScriptingParameter('db.saveasname','normal') == 'output' )
  203. {
  204. $this->partSize = 0;
  205. }
  206. if( ($this->partSize != 0) && ($this->packetSize != 0) && ($this->packetSize > $this->partSize) )
  207. {
  208. $this->packetSize = $this->partSize / 2;
  209. }
  210. // Initialize the algorithm
  211. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__." :: Initializing algorithm for first run");
  212. $this->nextTable = array_shift( $this->tables );
  213. $this->nextRange = 0;
  214. $this->query = '';
  215. // FIX 2.2: First table of extra databases was not being written to disk.
  216. // This deserved a place in the Bug Fix Hall Of Fame. In subsequent calls to _init, the $fp in
  217. // _writeline() was not nullified. Therefore, the first dump chunk (that is, the first table's
  218. // definition and first chunk of its data) were not written to disk. This call causes $fp to be
  219. // nullified, causing it to be recreated, pointing to the correct file. Holly crap, it took me
  220. // half an hour to get it!
  221. $null = null;
  222. $this->writeline($null);
  223. // Finally, mark ourselves "prepared".
  224. $this->setState('prepared');
  225. }
  226. /**
  227. * Implements the _run() abstract method
  228. */
  229. protected function _run()
  230. {
  231. // Check if we are already done
  232. if ($this->getState() == 'postrun') {
  233. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__." :: Already finished");
  234. $this->setStep("");
  235. $this->setSubstep("");
  236. return;
  237. }
  238. // Mark ourselves as still running (we will test if we actually do towards the end ;) )
  239. $this->setState('running');
  240. // Check if we are still adding a database dump part to the archive, or if
  241. // we have to post-process a part
  242. if( AEUtilScripting::getScriptingParameter('db.saveasname','normal') != 'output' )
  243. {
  244. $archiver = AEFactory::getArchiverEngine();
  245. $configuration = AEFactory::getConfiguration();
  246. if($configuration->get('engine.postproc.common.after_part',0))
  247. {
  248. if(!empty($archiver->finishedPart))
  249. {
  250. $filename = array_shift($archiver->finishedPart);
  251. AEUtilLogger::WriteLog(_AE_LOG_INFO, 'Preparing to post process '.basename($filename));
  252. $post_proc = AEFactory::getPostprocEngine();
  253. $result = $post_proc->processPart( $filename );
  254. $this->propagateFromObject($post_proc);
  255. if($result === false)
  256. {
  257. $this->setWarning('Failed to process file '.basename($filename));
  258. }
  259. else
  260. {
  261. AEUtilLogger::WriteLog(_AE_LOG_INFO, 'Successfully processed file '.basename($filename));
  262. }
  263. // Should we delete the file afterwards?
  264. if(
  265. $configuration->get('engine.postproc.common.delete_after',false)
  266. && $post_proc->allow_deletes
  267. && ($result !== false)
  268. )
  269. {
  270. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, 'Deleting already processed file '.basename($filename));
  271. AEPlatform::getInstance()->unlink($filename);
  272. }
  273. if($post_proc->break_after) {
  274. $configuration->set('volatile.breakflag', true);
  275. return;
  276. }
  277. }
  278. }
  279. if($configuration->get('volatile.engine.archiver.processingfile',false))
  280. {
  281. // We had already started archiving the db file, but it needs more time
  282. $finished = true;
  283. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Continuing adding the SQL dump part to the archive");
  284. $archiver->addFile(null,null,null);
  285. $this->propagateFromObject($archiver);
  286. if($this->getError()) return;
  287. $finished = !$configuration->get('volatile.engine.archiver.processingfile',false);
  288. if($finished)
  289. {
  290. $this->getNextDumpPart();
  291. }
  292. else
  293. {
  294. return;
  295. }
  296. }
  297. }
  298. $this->stepDatabaseDump();
  299. $null = null;
  300. $this->writeline($null);
  301. }
  302. /**
  303. * Implements the _finalize() abstract method
  304. *
  305. */
  306. protected function _finalize()
  307. {
  308. static $addedExtraSQL = false;
  309. // This makes sure that we don't re-add the extra SQL if the archiver needed more time
  310. // to include our file in the archive...
  311. if(!$addedExtraSQL)
  312. {
  313. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Adding any extra SQL statements imposed by the filters");
  314. $filters = AEFactory::getFilters();
  315. $this->writeline( $filters->getExtraSQL($this->databaseRoot) );
  316. }
  317. // Close the file pointer (otherwise the SQL file is left behind)
  318. $this->closeFile();
  319. // If we are not just doing a main db only backup, add the SQL file to the archive
  320. $finished = true;
  321. $configuration = AEFactory::getConfiguration();
  322. if( AEUtilScripting::getScriptingParameter('db.saveasname','normal') != 'output' )
  323. {
  324. $archiver = AEFactory::getArchiverEngine();
  325. $configuration = AEFactory::getConfiguration();
  326. if( $configuration->get('volatile.engine.archiver.processingfile',false) )
  327. {
  328. // We had already started archiving the db file, but it needs more time
  329. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Continuing adding the SQL dump to the archive");
  330. $archiver->addFile(null,null,null);
  331. if($this->getError()) return;
  332. $finished = !$configuration->get('volatile.engine.archiver.processingfile',false);
  333. }
  334. else
  335. {
  336. // We have to add the dump file to the archive
  337. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Adding the final SQL dump to the archive");
  338. $archiver->addFileRenamed( $this->tempFile, $this->saveAsName );
  339. if($this->getError()) return;
  340. $finished = !$configuration->get('volatile.engine.archiver.processingfile',false);
  341. }
  342. }
  343. else
  344. {
  345. // We just have to move the dump file to its final destination
  346. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Moving the SQL dump to its final location");
  347. $result = AEPlatform::getInstance()->move( $this->tempFile, $this->saveAsName );
  348. if(!$result)
  349. {
  350. $this->setError('Could not move the SQL dump to its final location');
  351. }
  352. }
  353. // Make sure that if the archiver needs more time to process the file we can supply it
  354. if($finished)
  355. {
  356. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Removing temporary file of final SQL dump");
  357. AEUtilTempfiles::unregisterAndDeleteTempFile( $this->tempFile, true );
  358. if($this->getError()) return;
  359. $this->setState('finished');
  360. }
  361. }
  362. /**
  363. * Creates a new dump part
  364. */
  365. protected function getNextDumpPart()
  366. {
  367. // On database dump only mode we mustn't create part files!
  368. if( AEUtilScripting::getScriptingParameter('db.saveasname','normal') == 'output' ) return false;
  369. // If the archiver is still processing, quit
  370. $finished = true;
  371. $configuration = AEFactory::getConfiguration();
  372. $archiver = AEFactory::getArchiverEngine();
  373. if( $configuration->get('volatile.engine.archiver.processingfile',false) ) return false;
  374. // We have to add the dump file to the archive
  375. $this->closeFile();
  376. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Adding the SQL dump part to the archive");
  377. $archiver->addFileRenamed( $this->tempFile, $this->saveAsName );
  378. if($this->getError()) return false;
  379. $finished = !$configuration->get('volatile.engine.archiver.processingfile',false);
  380. if(!$finished) return false; // Return if the file didn't finish getting added to the archive
  381. // Remove the old file
  382. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Removing dump part's temporary file");
  383. AEUtilTempfiles::unregisterAndDeleteTempFile( $this->tempFile, true );
  384. // Create the new dump part
  385. $this->partNumber++;
  386. $this->getBackupFilePaths($this->partNumber);
  387. $null = null;
  388. $this->writeline($null);
  389. return true;
  390. }
  391. /**
  392. * Creates a new dump part, but only if required to do so
  393. *
  394. * @return type
  395. */
  396. protected function createNewPartIfRequired()
  397. {
  398. if( $this->partSize == 0 ) return true;
  399. $filesize = 0;
  400. if(@file_exists($this->tempFile)) $filesize = @filesize($this->tempFile);
  401. if( $this->extendedInserts )
  402. {
  403. $projectedSize = $filesize + $this->packetSize;
  404. }
  405. else
  406. {
  407. $projectedSize = $filesize + strlen($this->query);
  408. }
  409. if( $projectedSize > $this->partSize )
  410. {
  411. return $this->getNextDumpPart();
  412. }
  413. return true;
  414. }
  415. /**
  416. * Returns a table's abstract name (replacing the prefix with the magic #__ string)
  417. *
  418. * @param string $tableName The canonical name, e.g. 'jos_content'
  419. * @return string The abstract name, e.g. '#__content'
  420. */
  421. protected function getAbstract( $tableName )
  422. {
  423. // Don't return abstract names for non-CMS tables
  424. if(is_null($this->prefix)) return $tableName;
  425. switch( $this->prefix )
  426. {
  427. case '':
  428. if($this->processEmptyPrefix)
  429. {
  430. // This is more of a hack; it assumes all tables are core CMS tables if the prefix is empty.
  431. return '#__' . $tableName;
  432. }
  433. else
  434. {
  435. // If $this->processEmptyPrefix (the process_empty_prefix config flag) is false, we don't
  436. // assume anything.
  437. return $tableName;
  438. }
  439. break;
  440. default:
  441. // Normal behaviour for 99% of sites
  442. // Fix 2.4 : Abstracting the prefix only if it's found in the beginning of the table name
  443. $tableAbstract = $tableName;
  444. if(!empty($this->prefix)) {
  445. if( substr($tableName, 0, strlen($this->prefix)) == $this->prefix ) {
  446. $tableAbstract = '#__' . substr($tableName, strlen($this->prefix));
  447. } else {
  448. // FIX 2.4: If there is no prefix, it's a non-Joomla! table.
  449. $tableAbstract = $tableName;
  450. }
  451. }
  452. return $tableAbstract;
  453. break;
  454. }
  455. }
  456. /**
  457. * Writes the SQL dump into the output files. If it fails, it sets the error
  458. *
  459. * @param string $data Data to write to the dump file. Pass NULL to force flushing to file.
  460. * @return boolean TRUE on successful write, FALSE otherwise
  461. */
  462. protected function writeDump( &$data )
  463. {
  464. if(!empty($data)) $this->data_cache .= $data;
  465. if( (strlen($this->data_cache) >= $this->cache_size) || (is_null($data) && (!empty($this->data_cache)) ) )
  466. {
  467. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Writing ".strlen($this->data_cache)." bytes to the dump file");
  468. $result = $this->writeline( $this->data_cache );
  469. if( !$result )
  470. {
  471. $errorMessage = 'Couldn\'t write to the SQL dump file ' . $this->tempFile . '; check the temporary directory permissions and make sure you have enough disk space available.';
  472. $this->setError($errorMessage);
  473. return false;
  474. }
  475. $this->data_cache = '';
  476. }
  477. return true;
  478. }
  479. /**
  480. * Saves the string in $fileData to the file $backupfile. Returns TRUE. If saving
  481. * failed, return value is FALSE.
  482. * @param string $fileData Data to write. Set to null to close the file handle.
  483. * @return boolean TRUE is saving to the file succeeded
  484. */
  485. protected function writeline(&$fileData) {
  486. if(!$this->fp)
  487. {
  488. $this->fp = @fopen($this->tempFile, 'a');
  489. if($this->fp === false)
  490. {
  491. $this->setError('Could not open '.$this->tempFile.' for append, in DB dump.');
  492. return;
  493. }
  494. }
  495. if(is_null($fileData))
  496. {
  497. if(is_resource($this->fp)) @fclose($this->fp);
  498. $this->fp = null;
  499. return true;
  500. }
  501. else
  502. {
  503. if ($this->fp) {
  504. $ret = fwrite($this->fp, $fileData);
  505. @clearstatcache();
  506. // Make sure that all data was written to disk
  507. return ($ret == strlen($fileData));
  508. } else {
  509. return false;
  510. }
  511. }
  512. }
  513. function _onSerialize()
  514. {
  515. $this->closeFile();
  516. }
  517. function __destruct()
  518. {
  519. $this->closeFile();
  520. }
  521. public function closeFile()
  522. {
  523. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Closing SQL dump file.");
  524. if(is_resource($this->fp)) @fclose($this->fp);
  525. }
  526. /**
  527. * Return an instance of AEAbstractDriver
  528. *
  529. * @return AEAbstractDriver
  530. */
  531. protected function &getDB()
  532. {
  533. $host = $this->host . ($this->port != '' ? ':' . $this->port : '');
  534. $user = $this->username;
  535. $password = $this->password;
  536. $driver = $this->driver;
  537. $database = $this->database;
  538. $prefix = is_null($this->prefix) ? '' : $this->prefix;
  539. $options = array ( 'driver' => $driver, 'host' => $host, 'user' => $user, 'password' => $password, 'database' => $database, 'prefix' => $prefix );
  540. $db = AEFactory::getDatabase($options);
  541. if( $error = $db->getError() )
  542. {
  543. $this->setError(__CLASS__.' :: Database Error: '.$error);
  544. return false;
  545. }
  546. if( $db->getErrorNum() > 0 )
  547. {
  548. $this->setError(__CLASS__.' :: Database Error: '.$db->getErrorMsg());
  549. return false;
  550. }
  551. return $db;
  552. }
  553. public function callStage($stage)
  554. {
  555. switch($stage) {
  556. case '_prepare':
  557. return $this->_prepare();
  558. break;
  559. case '_run':
  560. return $this->_run();
  561. break;
  562. case '_finalize':
  563. return $this->_finalize();
  564. break;
  565. }
  566. }
  567. }