PageRenderTime 54ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/administrator/components/com_akeeba/akeeba/plugins/engines/archiver/jps.php

https://bitbucket.org/kraymitchell/saiu
PHP | 649 lines | 490 code | 58 blank | 101 comment | 63 complexity | 77daf522094793293285ba3dec44e73a 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();
  12. if(!defined('_JPS_MAJOR'))
  13. {
  14. define('_JPS_MAJOR', 1);
  15. define('_JPS_MINOR', 9);
  16. }
  17. if(!function_exists('akstringlen')) {
  18. function akstringlen($string) {
  19. return function_exists('mb_strlen') ? mb_strlen($string,'8bit') : strlen($string);
  20. }
  21. }
  22. /**
  23. * JoomlaPack Archive Secure (JPS) creation class
  24. *
  25. * JPS Format 1.9 implemented, minus BZip2 compression support
  26. */
  27. class AEArchiverJps extends AEAbstractArchiver
  28. {
  29. /** @var integer How many files are contained in the archive */
  30. private $_fileCount = 0;
  31. /** @var integer The total size of files contained in the archive as they are stored */
  32. private $_compressedSize = 0;
  33. /** @var integer The total size of files contained in the archive when they are extracted to disk. */
  34. private $_uncompressedSize = 0;
  35. /** @var string The name of the file holding the ZIP's data, which becomes the final archive */
  36. private $_dataFileName;
  37. /** @var string Standard Header signature */
  38. private $_archive_signature = "\x4A\x50\x53"; // JPS
  39. /** @var string Standard Header signature */
  40. private $_end_of_archive_signature = "\x4A\x50\x45"; // JPE
  41. /** @var string Entity Block signature */
  42. private $_fileHeader = "\x4A\x50\x46"; // JPF
  43. /** @var string Marks the split archive's extra header */
  44. private $_extraHeaderSplit = "\x4A\x50\x01\x01"; //
  45. /** @var bool Should I use Split ZIP? */
  46. private $_useSplitZIP = false;
  47. /** @var int Maximum fragment size, in bytes */
  48. private $_fragmentSize = 0;
  49. /** @var int Current fragment number */
  50. private $_currentFragment = 1;
  51. /** @var int Total number of fragments */
  52. private $_totalFragments = 1;
  53. /** @var string Archive full path without extension */
  54. private $_dataFileNameBase = '';
  55. /** @var bool Should I store symlinks as such (no dereferencing?) */
  56. private $_symlink_store_target = false;
  57. /** @var string The password to use */
  58. private $password = null;
  59. /**
  60. * Extend the bootstrap code to add some define's used by the JPS format engine
  61. * @see backend/akeeba/abstract/AEAbstractArchiver#__bootstrap_code()
  62. */
  63. protected function __bootstrap_code()
  64. {
  65. if(!defined('_JPS_MAJOR'))
  66. {
  67. define( '_JPS_MAJOR', 1 ); // JPS Format major version number
  68. define( '_JPS_MINOR', 9 ); // JPS Format minor version number
  69. }
  70. parent::__bootstrap_code();
  71. }
  72. public function initialize( $targetArchivePath, $options = array() )
  73. {
  74. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__." :: new instance - archive $targetArchivePath");
  75. $this->_dataFileName = $targetArchivePath;
  76. // Make sure the encryption functions are all there
  77. $test = AEUtilEncrypt::AESEncryptCBC('test', 'test');
  78. if($test === false) {
  79. $this->setError('Sorry, your server does not support AES-128 encryption. Please use a different archive format.');
  80. return;
  81. }
  82. // Make sure we can really compress stuff
  83. if(!function_exists('gzcompress')) {
  84. $this->setError('Sorry, your server does not support GZip compression which is required for the JPS format. Please use a different archive format.');
  85. return;
  86. }
  87. // Get and memorise the password
  88. $config = AEFactory::getConfiguration();
  89. $this->password = $config->get('engine.archiver.jps.key','');
  90. if(empty($this->password))
  91. {
  92. $this->setWarning('You are using an empty password. This is not secure at all!');
  93. }
  94. // Should we enable split archive feature?
  95. $registry = AEFactory::getConfiguration();
  96. $fragmentsize = $registry->get('engine.archiver.common.part_size', 0);
  97. if($fragmentsize >= 65536)
  98. {
  99. // If the fragment size is AT LEAST 64Kb, enable split archive
  100. $this->_useSplitZIP = true;
  101. $this->_fragmentSize = $fragmentsize;
  102. // Indicate that we have at least 1 part
  103. $statistics = AEFactory::getStatistics();
  104. $statistics->updateMultipart(1);
  105. $this->_totalFragments = 1;
  106. AEUtilLogger::WriteLog(_AE_LOG_INFO, __CLASS__." :: Spanned JPS creation enabled");
  107. $this->_dataFileNameBase = dirname($targetArchivePath).'/'.basename($targetArchivePath,'.jps');
  108. $this->_dataFileName = $this->_dataFileNameBase.'.j01';
  109. }
  110. // Should I use Symlink Target Storage?
  111. $dereferencesymlinks = $registry->get('engine.archiver.common.dereference_symlinks', true);
  112. if(!$dereferencesymlinks)
  113. {
  114. // We are told not to dereference symlinks. Are we on Windows?
  115. if (function_exists('php_uname'))
  116. {
  117. $isWindows = stristr(php_uname(), 'windows');
  118. }
  119. else
  120. {
  121. $isWindows = (DIRECTORY_SEPARATOR == '\\');
  122. }
  123. // If we are not on Windows, enable symlink target storage
  124. $this->_symlink_store_target = !$isWindows;
  125. }
  126. // Try to kill the archive if it exists
  127. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__." :: Killing old archive");
  128. $fp = @fopen( $this->_dataFileName, "wb" );
  129. if (!($fp === false)) {
  130. @ftruncate( $fp,0 );
  131. @fclose( $fp );
  132. } else {
  133. if( file_exists($this->_dataFileName) ) @unlink( $this->_dataFileName );
  134. @touch( $this->_dataFileName );
  135. if(function_exists('chmod')) {
  136. chmod($this->_dataFileName, 0666);
  137. }
  138. }
  139. // Write the initial instance of the archive header
  140. $this->writeArchiveHeader();
  141. if($this->getError()) return;
  142. }
  143. /**
  144. * Updates the Standard Header with current information
  145. */
  146. public function finalize()
  147. {
  148. // If spanned JPS and there is no .jps file, rename the last fragment to .jps
  149. if($this->_useSplitZIP)
  150. {
  151. $extension = substr($this->_dataFileName, -3);
  152. if($extension != '.jps')
  153. {
  154. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, 'Renaming last JPS part to .JPS extension');
  155. $newName = $this->_dataFileNameBase.'.jps';
  156. if(!@rename($this->_dataFileName, $newName))
  157. {
  158. $this->setError('Could not rename last JPS part to .JPS extension.');
  159. return false;
  160. }
  161. $this->_dataFileName = $newName;
  162. }
  163. }
  164. // Write the end of archive header
  165. $this->writeEndOfArchiveHeader();
  166. if($this->getError()) return;
  167. }
  168. /**
  169. * Returns a string with the extension (including the dot) of the files produced
  170. * by this class.
  171. * @return string
  172. */
  173. public function getExtension()
  174. {
  175. return '.jps';
  176. }
  177. private function writeArchiveHeader()
  178. {
  179. $fp = @fopen( $this->_dataFileName, 'r+' );
  180. if($fp === false)
  181. {
  182. $this->setError('Could not open '.$this->_dataFileName.' for writing. Check permissions and open_basedir restrictions.');
  183. return;
  184. }
  185. $this->_fwrite( $fp, $this->_archive_signature ); // ID string (JPS)
  186. if($this->getError()) return;
  187. $this->_fwrite( $fp, pack('C', _JPS_MAJOR ) ); // Major version
  188. $this->_fwrite( $fp, pack('C', _JPS_MINOR ) ); // Minor version
  189. $this->_fwrite( $fp, pack('C', $this->_useSplitZIP ? 1 : 0 ) ); // Is it a split archive?
  190. $this->_fwrite( $fp, pack('v', 0 ) ); // Extra header length (0 bytes)
  191. @fclose( $fp );
  192. if( function_exists('chmod') )
  193. {
  194. @chmod($this->_dataFileName, 0755);
  195. }
  196. }
  197. private function writeEndOfArchiveHeader()
  198. {
  199. $fp = @fopen( $this->_dataFileName, 'ab' );
  200. if($fp === false)
  201. {
  202. $this->setError('Could not open '.$this->_dataFileName.' for writing. Check permissions and open_basedir restrictions.');
  203. return;
  204. }
  205. $this->_fwrite( $fp, $this->_end_of_archive_signature ); // ID string (JPE)
  206. $this->_fwrite( $fp, pack('v', $this->_totalFragments) ); // Total number of parts
  207. $this->_fwrite( $fp, pack('V', $this->_fileCount) ); // Total number of files
  208. $this->_fwrite( $fp, pack('V', $this->_uncompressedSize) ); // Uncompressed size
  209. $this->_fwrite( $fp, pack('V', $this->_compressedSize) ); // Compressed size
  210. }
  211. protected function _addFile( $isVirtual, &$sourceNameOrData, $targetName )
  212. {
  213. if($isVirtual)
  214. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "-- Adding $targetName to archive (virtual data)");
  215. else AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "-- Adding $targetName to archive (source: $sourceNameOrData)");
  216. $configuration = AEFactory::getConfiguration();
  217. $timer = AEFactory::getTimer();
  218. // Initialize archive file pointer
  219. $fp = null;
  220. // Initialize inode change timestamp
  221. $filectime = 0;
  222. $processingFile = $configuration->get('volatile.engine.archiver.processingfile',false);
  223. if(!$processingFile)
  224. {
  225. // Uncache data
  226. $configuration->set('volatile.engine.archiver.sourceNameOrData', null);
  227. $configuration->set('volatile.engine.archiver.unc_len', null);
  228. $configuration->set('volatile.engine.archiver.resume', null);
  229. $configuration->set('volatile.engine.archiver.processingfile',false);
  230. // See if it's a directory
  231. $isDir = $isVirtual ? false : is_dir($sourceNameOrData);
  232. // See if it's a symlink (w/out dereference)
  233. $isSymlink = false;
  234. if($this->_symlink_store_target && !$isVirtual)
  235. {
  236. $isSymlink = is_link($sourceNameOrData);
  237. }
  238. // Get real size before compression
  239. if($isVirtual)
  240. {
  241. $fileSize = akstringlen($sourceNameOrData);
  242. $filectime = time();
  243. }
  244. else
  245. {
  246. if($isSymlink)
  247. {
  248. $fileSize = akstringlen( @readlink($sourceNameOrData) );
  249. }
  250. else
  251. {
  252. // Is the file readable?
  253. if(!is_readable($sourceNameOrData))
  254. {
  255. // Unreadable files won't be recorded in the archive file
  256. $this->setWarning( 'Unreadable file '.$sourceNameOrData.'. Check permissions' );
  257. return false;
  258. }
  259. else
  260. {
  261. // Really, REALLY check if it is readable (PHP sometimes lies, dammit!)
  262. $myfp = @fopen($sourceNameOrData, 'rb');
  263. if($myfp === false)
  264. {
  265. // Unreadable file, skip it.
  266. $this->setWarning( 'Unreadable file '.$sourceNameOrData.'. Check permissions' );
  267. return false;
  268. }
  269. @fclose($myfp);
  270. }
  271. // Get the filesize and modification time
  272. $fileSize = $isDir ? 0 : @filesize($sourceNameOrData);
  273. $filectime = $isDir ? 0 : @filemtime($sourceNameOrData);
  274. }
  275. }
  276. // Decide if we will compress
  277. if ($isDir || $isSymlink) {
  278. // don't compress directories and symlinks...
  279. $compressionMethod = 0;
  280. } else {
  281. // always compress files using gzip
  282. $compressionMethod = 1;
  283. }
  284. // Fix stored name for directories
  285. $storedName = $targetName;
  286. $storedName .= ($isDir) ? "/" : "";
  287. // Get file permissions
  288. $perms = $isVirtual ? 0755 : @fileperms( $sourceNameOrData );
  289. // Get file type
  290. if( (!$isDir) && (!$isSymlink) ) { $fileType = 1; }
  291. elseif($isSymlink) { $fileType = 2; }
  292. elseif($isDir) { $fileType = 0; }
  293. // Create the Entity Description Block Data
  294. $headerData =
  295. pack('v', akstringlen($storedName)) // Length of entity path
  296. . $storedName // Entity path
  297. . pack('c', $fileType ) // Entity type
  298. . pack('c', $compressionMethod) // Compression type
  299. . pack('V', $fileSize) // Uncompressed size
  300. . pack('V', $perms) // Entity permissions
  301. . pack('V', $filectime) // File Modification Time
  302. ;
  303. // Create and write the Entity Description Block Header
  304. $decryptedSize = akstringlen($headerData);
  305. $headerData = AEUtilEncrypt::AESEncryptCBC($headerData, $this->password, 128);
  306. $encryptedSize = akstringlen($headerData);
  307. $headerData =
  308. $this->_fileHeader . // JPF
  309. pack('v', $encryptedSize) . // Encrypted size
  310. pack('v', $decryptedSize) . // Decrypted size
  311. $headerData // Encrypted Entity Description Block Data
  312. ;
  313. // Do we have enough space to store the header?
  314. if($this->_useSplitZIP)
  315. {
  316. // Compare to free part space
  317. clearstatcache();
  318. $current_part_size = @filesize($this->_dataFileName);
  319. $free_space = $this->_fragmentSize - ($current_part_size === false ? 0 : $current_part_size);
  320. if($free_space <= akstringlen($headerData))
  321. {
  322. // Not enough space on current part, create new part
  323. if(!$this->_createNewPart())
  324. {
  325. $this->setError('Could not create new JPS part file '.basename($this->_dataFileName));
  326. return false;
  327. }
  328. }
  329. }
  330. // Open data file for output
  331. $fp = @fopen( $this->_dataFileName, "ab");
  332. if ($fp === false)
  333. {
  334. $this->setError("Could not open archive file '{$this->_dataFileName}' for append!");
  335. return;
  336. }
  337. // Write the header data
  338. $this->_fwrite($fp, $headerData);
  339. // Cache useful information about the file
  340. $configuration->set('volatile.engine.archiver.sourceNameOrData', $sourceNameOrData);
  341. $configuration->set('volatile.engine.archiver.unc_len', $fileSize);
  342. // Update global stats
  343. $this->_fileCount++;
  344. $this->_uncompressedSize += $fileSize;
  345. }
  346. else
  347. {
  348. $isDir = false;
  349. $isSymlink = false;
  350. // Open data file for output
  351. $fp = @fopen( $this->_dataFileName, "ab");
  352. if ($fp === false)
  353. {
  354. $this->setError("Could not open archive file '{$this->_dataFileName}' for append!");
  355. return;
  356. }
  357. }
  358. // Symlink: Single step, one block, uncompressed
  359. if($isSymlink)
  360. {
  361. $data = @readlink($sourceNameOrData);
  362. $this->_writeEncryptedBlock($fp, $data);
  363. $this->_compressedSize += akstringlen($data);
  364. if($this->getError()) return;
  365. }
  366. // Virtual: Single step, multiple blocks, compressed
  367. elseif($isVirtual)
  368. {
  369. // Loop in 64Kb blocks
  370. while( strlen($sourceNameOrData) > 0 )
  371. {
  372. $data = substr($sourceNameOrData, 0, 65535);
  373. if(akstringlen($data) < akstringlen($sourceNameOrData)) {
  374. $sourceNameOrData = substr($sourceNameOrData,65535);
  375. } else {
  376. $sourceNameOrData = '';
  377. }
  378. $data = gzcompress($data);
  379. $data = substr(substr($data, 0, -4), 2);
  380. $this->_writeEncryptedBlock($fp, $data);
  381. $this->_compressedSize += akstringlen($data);
  382. if($this->getError()) return;
  383. }
  384. }
  385. // Regular file: multiple step, multiple blocks, compressed
  386. else
  387. {
  388. // Get resume information of required
  389. if( $configuration->get('volatile.engine.archiver.processingfile',false) )
  390. {
  391. $sourceNameOrData = $configuration->get('volatile.engine.archiver.sourceNameOrData', '');
  392. $fileSize = $configuration->get('volatile.engine.archiver.unc_len', 0);
  393. $resume = $configuration->get('volatile.engine.archiver.resume', 0);
  394. AEUtilLogger::WriteLog(_AE_LOG_DEBUG,"(cont) Source: $sourceNameOrData - Size: $fileSize - Resume: $resume");
  395. }
  396. // Open the file
  397. $zdatafp = @fopen( $sourceNameOrData, "rb" );
  398. if( $zdatafp === FALSE )
  399. {
  400. $this->setWarning( 'Unreadable file '.$sourceNameOrData.'. Check permissions' );
  401. @fclose($fp);
  402. return false;
  403. }
  404. // Seek to the resume point if required
  405. if( $configuration->get('volatile.engine.archiver.processingfile',false) )
  406. {
  407. // Seek to new offset
  408. $seek_result = @fseek($zdatafp, $resume);
  409. if( $seek_result === -1 )
  410. {
  411. // What?! We can't resume!
  412. $this->setError(sprintf('Could not resume packing of file %s. Your archive is damaged!', $sourceNameOrData));
  413. @fclose($zdatafp);
  414. @fclose($fp);
  415. return false;
  416. }
  417. // Doctor the uncompressed size to match the remainder of the data
  418. $fileSize = $fileSize - $resume;
  419. }
  420. while( !feof($zdatafp) && ($timer->getTimeLeft() > 0) && ($fileSize > 0) ) {
  421. $zdata = @fread($zdatafp, AKEEBA_CHUNK);
  422. $fileSize -= min(akstringlen($zdata), AKEEBA_CHUNK);
  423. $zdata = gzcompress($zdata);
  424. $zdata = substr(substr($zdata, 0, -4), 2);
  425. $this->_writeEncryptedBlock( $fp, $zdata );
  426. $this->_compressedSize += akstringlen($zdata);
  427. if($this->getError()) {
  428. @fclose($zdatafp);
  429. @fclose($fp);
  430. return;
  431. }
  432. }
  433. // WARNING!!! The extra $fileSize != 0 check is necessary as PHP won't reach EOF for 0-byte files.
  434. if(!feof($zdatafp) && ($fileSize != 0))
  435. {
  436. // We have to break, or we'll time out!
  437. $resume = @ftell($zdatafp);
  438. $configuration->set('volatile.engine.archiver.resume', $resume);
  439. $configuration->set('volatile.engine.archiver.processingfile',true);
  440. @fclose($zdatafp);
  441. @fclose($fp);
  442. return true;
  443. }
  444. else
  445. {
  446. $configuration->set('volatile.engine.archiver.resume', null);
  447. $configuration->set('volatile.engine.archiver.processingfile',false);
  448. }
  449. @fclose( $zdatafp );
  450. }
  451. }
  452. /**
  453. * Creates a new archive part
  454. * @param bool $finalPart Set to true if it is the final part (therefore has the .jps extension)
  455. */
  456. private function _createNewPart($finalPart = false)
  457. {
  458. // Push the previous part if we have to post-process it immediately
  459. $configuration = AEFactory::getConfiguration();
  460. if($configuration->get('engine.postproc.common.after_part',0))
  461. {
  462. $this->finishedPart[] = $this->_dataFileName;
  463. }
  464. $this->_totalFragments++;
  465. $this->_currentFragment = $this->_totalFragments;
  466. if($finalPart)
  467. {
  468. $this->_dataFileName = $this->_dataFileNameBase.'.jps';
  469. }
  470. else
  471. {
  472. $this->_dataFileName = $this->_dataFileNameBase.'.j'.sprintf('%02d', $this->_currentFragment);
  473. }
  474. AEUtilLogger::WriteLog(_AE_LOG_INFO, 'Creating new JPS part #'.$this->_currentFragment.', file '.$this->_dataFileName);
  475. // Inform that we have chenged the multipart number
  476. $statistics = AEFactory::getStatistics();
  477. $statistics->updateMultipart($this->_totalFragments);
  478. // Try to remove any existing file
  479. @unlink($this->_dataFileName);
  480. // Touch the new file
  481. $result = @touch($this->_dataFileName);
  482. if(function_exists('chmod')) {
  483. chmod($this->_dataFileName, 0666);
  484. }
  485. return $result;
  486. }
  487. /**
  488. * Writes an encrypted block to the archive
  489. * @param resource $fp The file pointer resource of the file to write to
  490. * @param string $data Raw binary data to encrypt and write
  491. */
  492. private function _writeEncryptedBlock( &$fp, $data )
  493. {
  494. $decryptedSize = akstringlen($data);
  495. $data = AEUtilEncrypt::AESEncryptCBC($data, $this->password, 128);
  496. $encryptedSize = akstringlen($data);
  497. // Do we have enough space to store the 8 byte header?
  498. if($this->_useSplitZIP)
  499. {
  500. // Compare to free part space
  501. clearstatcache();
  502. $current_part_size = @filesize($this->_dataFileName);
  503. $free_space = $this->_fragmentSize - ($current_part_size === false ? 0 : $current_part_size);
  504. if($free_space <= 8)
  505. {
  506. @fclose($fp);
  507. // Not enough space on current part, create new part
  508. if(!$this->_createNewPart())
  509. {
  510. $this->setError('Could not create new JPS part file '.basename($this->_dataFileName));
  511. return false;
  512. }
  513. // Open data file for output
  514. $fp = @fopen( $this->_dataFileName, "ab");
  515. if ($fp === false)
  516. {
  517. $this->setError("Could not open archive file '{$this->_dataFileName}' for append!");
  518. return;
  519. }
  520. }
  521. } else {
  522. $free_space = $encryptedSize + 8;
  523. }
  524. // Write the header
  525. $this->_fwrite($fp,
  526. pack('V',$encryptedSize) .
  527. pack('V',$decryptedSize)
  528. );
  529. if($this->getError()) return;
  530. $free_space -= 8;
  531. // Do we have enough space to write the data in one part?
  532. if($free_space >= $encryptedSize)
  533. {
  534. $this->_fwrite($fp, $data);
  535. if($this->getError()) return;
  536. }
  537. else
  538. {
  539. // Split between parts - Write first part
  540. $firstPart = substr( $data, 0, $free_space );
  541. $secondPart = substr( $data, $free_space );
  542. if( md5($firstPart.$secondPart) != md5($data) ) {die('DEBUG -- Multibyte character problems!'); die();}
  543. $this->_fwrite( $fp, $firstPart, $free_space );
  544. if($this->getError()) {
  545. @fclose($fp);
  546. return;
  547. }
  548. // Create new part
  549. if(!$this->_createNewPart())
  550. {
  551. // Die if we couldn't create the new part
  552. $this->setError('Could not create new JPA part file '.basename($this->_dataFileName));
  553. @fclose($fp);
  554. return false;
  555. }
  556. else
  557. {
  558. // Close the old data file
  559. @fclose($fp);
  560. // Open data file for output
  561. $fp = @fopen( $this->_dataFileName, "ab");
  562. if ($fp === false)
  563. {
  564. $this->setError("Could not open archive file {$this->_dataFileName} for append!");
  565. return false;
  566. }
  567. }
  568. // Write the rest of the data
  569. $this->_fwrite( $fp, $secondPart, $encryptedSize-$free_space );
  570. }
  571. }
  572. }