get('engine.archiver.common.chunk_size', 1048756); define('AKEEBA_CHUNK', $chunksize); } /** * Abstract parent class of all archiver engines */ abstract class Base extends BaseObject { /** @var resource JPA transformation source handle */ private $_xform_fp; /** @var string The archive's comment. It's currently used ONLY in the ZIP file format */ protected $_comment; /** @var array The last part which has been finalized and waits to be post-processed */ public $finishedPart = array(); /** @var array An array of open file pointers */ private $filePointers = array(); /** @var array An array of the last open files for writing and their last written to offsets */ private $fileOffsets = array(); /** @var resource File pointer to the archive being currently written to */ protected $fp = null; /** @var resource File pointer to the archive's central directory file (for ZIP) */ protected $cdfp = null; /** @var Filesystem Filesystem utilities object */ protected $fsUtils = null; /** * Common code which gets called on instance creation or wake-up (unserialization) * * @codeCoverageIgnore * * @return void */ protected function __bootstrap_code() { if ( !defined('AKEEBA_CHUNK')) { // Cache chunk override as a constant $registry = Factory::getConfiguration(); $chunk_override = $registry->get('engine.archiver.common.chunk_size', 0); define('AKEEBA_CHUNK', $chunk_override > 0 ? $chunk_override : 1048756); } $this->fsUtils = Factory::getFilesystemTools(); } /** * Public constructor * * @codeCoverageIgnore * * @return Base */ public function __construct() { $this->__bootstrap_code(); } /** * Wakeup (unserialization) function * * @codeCoverageIgnore * * @return void */ public function __wakeup() { $this->__bootstrap_code(); } /** * Release file pointers when the object is being serialized * * @codeCoverageIgnore * * @return void */ public function _onSerialize() { $this->_closeAllFiles(); $this->fp = null; $this->cdfp = null; parent::_onSerialize(); } /** * Release file pointers when the object is being destroyed * * @codeCoverageIgnore * * @return void */ public function __destruct() { $this->_closeAllFiles(); $this->fp = null; $this->cdfp = null; } /** * Overrides setError() in order to also write the error message to the log file * * @param string $error The error message * * @codeCoverageIgnore * * @return void */ public function setError($error) { parent::setError($error); Factory::getLog()->log(LogLevel::ERROR, $error); } /** * Overrides setWarning() in order to also write the warning message to the log file * * @param string $warning The warning message * * @codeCoverageIgnore * * @return void */ public function setWarning($warning) { parent::setWarning($warning); Factory::getLog()->log(LogLevel::WARNING, $warning); } /** * Notifies the engine on the backup comment and converts it to plain text for * inclusion in the archive file, if applicable. * * @param string $comment The archive's comment * * @return void */ public function setComment($comment) { // First, sanitize the comment in a text-only format $comment = str_replace("\n", " ", $comment); // Replace newlines with spaces $comment = str_replace("
", "\n", $comment); // Replace HTML4
with single newlines $comment = str_replace("
", "\n", $comment); // Replace HTML4
with single newlines $comment = str_replace("
", "\n", $comment); // Replace HTML
with single newlines $comment = str_replace("

", "\n\n", $comment); // Replace paragraph endings with double newlines $comment = str_replace("", "*", $comment); // Replace bold with star notation $comment = str_replace("", "*", $comment); // Replace bold with star notation $comment = str_replace("", "_", $comment); // Replace italics with underline notation $comment = str_replace("", "_", $comment); // Replace italics with underline notation $this->_comment = strip_tags($comment, ''); } /** * Adds a list of files into the archive, removing $removePath from the * file names and adding $addPath to them. * * @param array $fileList A simple string array of filepaths to include * @param string $removePath Paths to remove from the filepaths * @param string $addPath Paths to add in front of the filepaths * * @return boolean True on success */ public function addFileList(&$fileList, $removePath = '', $addPath = '') { if ( !is_array($fileList)) { $this->setWarning('addFileList called without a file list array'); return false; } // @codeCoverageIgnoreStart if (function_exists('mb_internal_encoding')) { $mb_encoding = mb_internal_encoding(); mb_internal_encoding('ISO-8859-1'); } // @codeCoverageIgnoreEnd foreach ($fileList as $file) { $storedName = $this->_addRemovePaths($file, $removePath, $addPath); $ret = $this->_addFile(false, $file, $storedName); } // @codeCoverageIgnoreStart if (function_exists('mb_internal_encoding')) { mb_internal_encoding($mb_encoding); } // @codeCoverageIgnoreEnd return true; } /** * Adds a single file in the archive * * @param string $file The absolute path to the file to add * @param string $removePath Path to remove from $file * @param string $addPath Path to prepend to $file * * @return boolean */ public function addFile($file, $removePath = '', $addPath = '') { if (function_exists('mb_internal_encoding')) { $mb_encoding = mb_internal_encoding(); mb_internal_encoding('ISO-8859-1'); } $storedName = $this->_addRemovePaths($file, $removePath, $addPath); $ret = $this->_addFile(false, $file, $storedName); if (function_exists('mb_internal_encoding')) { mb_internal_encoding($mb_encoding); } return $ret; } /** * Adds a file to the archive, with a name that's different from the source * filename * * @param string $sourceFile Absolute path to the source file * @param string $targetFile Relative filename to store in archive * * @return boolean */ public function addFileRenamed($sourceFile, $targetFile) { if (function_exists('mb_internal_encoding')) { $mb_encoding = mb_internal_encoding(); mb_internal_encoding('ISO-8859-1'); } $ret = $this->_addFile(false, $sourceFile, $targetFile); if (function_exists('mb_internal_encoding')) { mb_internal_encoding($mb_encoding); } return $ret; } /** * Adds a file to the archive, given the stored name and its contents * * @param string $fileName The base file name * @param string $addPath The relative path to prepend to file name * @param string $virtualContent The contents of the file to be archived * * @return boolean */ public function addVirtualFile($fileName, $addPath = '', &$virtualContent) { $storedName = $this->_addRemovePaths($fileName, '', $addPath); if (function_exists('mb_internal_encoding')) { $mb_encoding = mb_internal_encoding(); mb_internal_encoding('ISO-8859-1'); } $ret = $this->_addFile(true, $virtualContent, $storedName); if (function_exists('mb_internal_encoding')) { mb_internal_encoding($mb_encoding); } return $ret; } /** * Initialises the archiver class, creating the archive from an existent * installer's JPA archive. MUST BE OVERRIDEN BY CHILDREN CLASSES. * * @param string $targetArchivePath Absolute path to the generated archive * @param array $options A named key array of options (optional) * * @return void */ abstract public function initialize($targetArchivePath, $options = array()); /** * Makes whatever finalization is needed for the archive to be considered * complete and useful (or, generally, clean up) * * @return void */ public function finalize() { } /** * Returns a string with the extension (including the dot) of the files produced * by this class. * * @return string */ abstract public function getExtension(); /** * The most basic file transaction: add a single entry (file or directory) to * the archive. * * @param boolean $isVirtual If true, the next parameter contains file data instead of a file name * @param string $sourceNameOrData Absolute file name to read data from or the file data itself is $isVirtual is * true * @param string $targetName The (relative) file name under which to store the file in the archive * * @return boolean True on success, false otherwise */ abstract protected function _addFile($isVirtual, &$sourceNameOrData, $targetName); /** * Opens a file, if it's not already open, or returns its cached file pointer if it's already open * * @param string $file The filename to open * @param string $mode File open mode, defaults to binary write * * @return resource */ protected function _fopen($file, $mode = 'wb') { if ( !array_key_exists($file, $this->filePointers)) { //Factory::getLog()->log(LogLevel::DEBUG, "Opening backup archive $file with mode $mode"); $this->filePointers[$file] = @fopen($file, $mode); // If we open a file for append we have to seek to the correct offset if (substr($mode, 0, 1) == 'a') { if (isset($this->fileOffsets[$file])) { //Factory::getLog()->log(LogLevel::DEBUG, "Truncating to " . $this->fileOffsets[$file]); @ftruncate($this->filePointers[$file], $this->fileOffsets[$file]); } fseek($this->filePointers[$file], 0, SEEK_END); } } return $this->filePointers[$file]; } /** * Closes an already open file * * @param resource $fp The file pointer to close * * @return boolean */ protected function _fclose(&$fp) { $offset = array_search($fp, $this->filePointers, true); $result = @fclose($fp); if ($offset !== false) { unset($this->filePointers[$offset]); } $fp = null; return $result; } /** * Closes all open files known to this archiver object * * @return void */ protected function _closeAllFiles() { if ( !empty($this->filePointers)) { foreach ($this->filePointers as $file => $fp) { @fclose($fp); unset($this->filePointers[$file]); } } } /** * Write to file, defeating magic_quotes_runtime settings (pure binary write) * * @param resource $fp Handle to a file * @param string $data The data to write to the file * @param integer $p_len Maximum length of data to write * * @return boolean */ protected function _fwrite($fp, $data, $p_len = null) { static $lastFp = null; static $filename = null; if ($fp !== $lastFp) { $lastFp = $fp; $filename = array_search($fp, $this->filePointers, true); } $len = is_null($p_len) ? (function_exists('mb_strlen') ? mb_strlen($data, '8bit') : strlen($data)) : $p_len; $ret = fwrite($fp, $data, $len); if (($ret === false) || (abs(($ret - $len)) >= 1)) { // Log debug information about the archive file's existence and current size. This helps us figure out if // there is a server-imposed maximum file size limit. clearstatcache(); $fileExists = @file_exists($filename) ? 'exists' : 'does NOT exist'; $currentSize = @filesize($filename); Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . "::_fwrite() ERROR!! Cannot write to archive file $filename. The file $fileExists. File size $currentSize bytes after writing $ret of $len bytes. Please check the output directory permissions and make sure you have enough disk space available. If this does not help, please set up a Part Size for Split Archives LOWER than this size and retry backing up."); $this->setError('Couldn\'t write to the archive file; check the output directory permissions and make sure you have enough disk space available.' . "[len=$ret / $len]"); return false; } if ($filename !== false) { $this->fileOffsets[$filename] = @ftell($fp); } return true; } /** * Removes a file path from the list of resumable offsets * * @param $filename */ protected function _removeFromOffsetsList($filename) { if (isset($this->fileOffsets[$filename])) { unset($this->fileOffsets[$filename]); } } /** * Converts a human formatted size to integer representation of bytes, * e.g. 1M to 1024768 * * @param string $val The value in human readable format, e.g. "1M" * * @return integer The value in bytes */ protected function _return_bytes($val) { $val = trim($val); $last = strtolower($val{strlen($val) - 1}); switch ($last) { // The 'G' modifier is available since PHP 5.1.0 case 'g': $val *= 1024; case 'm': $val *= 1024; case 'k': $val *= 1024; } return $val; } /** * Removes the $p_remove_dir from $p_filename, while prepending it with $p_add_dir. * Largely based on code from the pclZip library. * * @param string $p_filename The absolute file name to treat * @param string $p_remove_dir The path to remove * @param string $p_add_dir The path to prefix the treated file name with * * @return string The treated file name */ protected function _addRemovePaths($p_filename, $p_remove_dir, $p_add_dir) { $p_filename = $this->fsUtils->TranslateWinPath($p_filename); $p_remove_dir = ($p_remove_dir == '') ? '' : $this->fsUtils->TranslateWinPath($p_remove_dir); //should fix corrupt backups, fix by nicholas $v_stored_filename = $p_filename; if ( !($p_remove_dir == "")) { if (substr($p_remove_dir, -1) != '/') { $p_remove_dir .= "/"; } if ((substr($p_filename, 0, 2) == "./") || (substr($p_remove_dir, 0, 2) == "./")) { if ((substr($p_filename, 0, 2) == "./") && (substr($p_remove_dir, 0, 2) != "./")) { $p_remove_dir = "./" . $p_remove_dir; } if ((substr($p_filename, 0, 2) != "./") && (substr($p_remove_dir, 0, 2) == "./")) { $p_remove_dir = substr($p_remove_dir, 2); } } $v_compare = $this->_PathInclusion($p_remove_dir, $p_filename); if ($v_compare > 0) { if ($v_compare == 2) { $v_stored_filename = ""; } else { $v_stored_filename = substr($p_filename, (function_exists('mb_strlen') ? mb_strlen($p_remove_dir, '8bit') : strlen($p_remove_dir))); } } } else { $v_stored_filename = $p_filename; } if ( !($p_add_dir == "")) { if (substr($p_add_dir, -1) == "/") { $v_stored_filename = $p_add_dir . $v_stored_filename; } else { $v_stored_filename = $p_add_dir . "/" . $v_stored_filename; } } return $v_stored_filename; } /** * This function indicates if the path $p_path is under the $p_dir tree. Or, * said in an other way, if the file or sub-dir $p_path is inside the dir * $p_dir. * The function indicates also if the path is exactly the same as the dir. * This function supports path with duplicated '/' like '//', but does not * support '.' or '..' statements. * * Copied verbatim from pclZip library * * @codeCoverageIgnore * * @param string $p_dir Source tree * @param string $p_path Check if this is part of $p_dir * * @return integer 0 if $p_path is not inside directory $p_dir, * 1 if $p_path is inside directory $p_dir * 2 if $p_path is exactly the same as $p_dir */ protected function _PathInclusion($p_dir, $p_path) { $v_result = 1; // ----- Explode dir and path by directory separator $v_list_dir = explode("/", $p_dir); $v_list_dir_size = sizeof($v_list_dir); $v_list_path = explode("/", $p_path); $v_list_path_size = sizeof($v_list_path); // ----- Study directories paths $i = 0; $j = 0; while (($i < $v_list_dir_size) && ($j < $v_list_path_size) && ($v_result)) { // ----- Look for empty dir (path reduction) if ($v_list_dir[$i] == '') { $i++; continue; } if ($v_list_path[$j] == '') { $j++; continue; } // ----- Compare the items if (($v_list_dir[$i] != $v_list_path[$j]) && ($v_list_dir[$i] != '') && ($v_list_path[$j] != '')) { $v_result = 0; } // ----- Next items $i++; $j++; } // ----- Look if everything seems to be the same if ($v_result) { // ----- Skip all the empty items while (($j < $v_list_path_size) && ($v_list_path[$j] == '')) { $j++; } while (($i < $v_list_dir_size) && ($v_list_dir[$i] == '')) { $i++; } if (($i >= $v_list_dir_size) && ($j >= $v_list_path_size)) { // ----- There are exactly the same $v_result = 2; } else if ($i < $v_list_dir_size) { // ----- The path is shorter than the dir $v_result = 0; } } // ----- Return return $v_result; } /** * Transforms a JPA archive (containing an installer) to the native archive format * of the class. It actually extracts the source JPA in memory and instructs the * class to include each extracted file. * * @codeCoverageIgnore * * @param integer $index The index in the source JPA archive's list currently in use * @param integer $offset The source JPA archive's offset to use * * @return boolean False if an error occurred, true otherwise */ public function transformJPA($index, $offset) { static $totalSize = 0; // Do we have to open the file? if ( !$this->_xform_fp) { // Get the source path $registry = Factory::getConfiguration(); $embedded_installer = $registry->get('akeeba.advanced.embedded_installer'); // Fetch the name of the installer image $installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList(); $xform_source = Platform::getInstance()->get_installer_images_path() . '/foobar.jpa'; // We need this as a "safe fallback" // Try to find a sane default if we are not given a valid embedded installer if (!array_key_exists($embedded_installer, $installerDescriptors)) { $embedded_installer = 'angie'; if (!array_key_exists($embedded_installer, $installerDescriptors)) { $allInstallers = array_keys($installerDescriptors); foreach ($allInstallers as $anInstaller) { if ($anInstaller == 'none') { continue; } $embedded_installer = $anInstaller; break; } } } if (array_key_exists($embedded_installer, $installerDescriptors)) { $packages = $installerDescriptors[$embedded_installer]['package']; if (empty($packages)) { // No installer package specified. Pretend we are done! $retArray = array( "filename" => '', // File name extracted "data" => '', // File data "index" => 0, // How many source JPA files I have "offset" => 0, // Offset in JPA file "skip" => false, // Skip this? "done" => true, // Are we done yet? "filesize" => 0 ); return $retArray; } $packages = explode(',', $packages); $totalSize = 0; $pathPrefix = Platform::getInstance()->get_installer_images_path() . '/'; foreach ($packages as $package) { $filePath = $pathPrefix . $package; $totalSize += (int)@filesize($filePath); } if (count($packages) < $index) { $this->setError(__CLASS__ . ":: Installer package index $index not found for embedded installer $embedded_installer"); return false; } $package = $packages[$index]; // A package is specified, use it! $xform_source = $pathPrefix . $package; } // 2.3: Try to use sane default if the indicated installer doesn't exist if ( !file_exists($xform_source) && (basename($xform_source) != 'angie.jpa')) { $this->setError(__CLASS__ . ":: Installer package $xform_source of embedded installer $embedded_installer not found. Please go to the configuration page, select an Embedded Installer, save the configuration and try backing up again."); return false; } // Try opening the file if (file_exists($xform_source)) { $this->_xform_fp = @fopen($xform_source, 'r'); if ($this->_xform_fp === false) { $this->setError(__CLASS__ . ":: Can't seed archive with installer package " . $xform_source); return false; } } else { $this->setError(__CLASS__ . ":: Installer package " . $xform_source . " does not exist!"); return false; } } $headerDataLength = 0; if ( !$offset) { // First run detected! Factory::getLog()->log(LogLevel::DEBUG, 'Initializing with JPA package ' . $xform_source); // Skip over the header and check no problem exists $offset = $this->_xformReadHeader(); if ($offset === false) { $this->setError('JPA package file was not read'); return false; // Oops! The package file doesn't exist or is corrupt } $headerDataLength = $offset; } $ret = $this->_xformExtract($offset); $ret['index'] = $index; if (is_array($ret)) { $ret['chunkProcessed'] = $headerDataLength + $ret['offset'] - $offset; $offset = $ret['offset']; if ( !$ret['skip'] && !$ret['done']) { Factory::getLog()->log(LogLevel::DEBUG, ' Adding ' . $ret['filename'] . '; Next offset:' . $offset); $this->addVirtualFile($ret['filename'], '', $ret['data']); if ($this->getError()) { return false; } } elseif ($ret['done']) { $registry = Factory::getConfiguration(); $embedded_installer = $registry->get('akeeba.advanced.embedded_installer'); $installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList(); $packages = $installerDescriptors[$embedded_installer]['package']; $packages = explode(',', $packages); Factory::getLog()->log(LogLevel::DEBUG, ' Done with package ' . $packages[$index]); if (count($packages) > ($index + 1)) { $ret['done'] = false; $ret['index'] = $index + 1; $ret['offset'] = 0; $this->_xform_fp = null; } else { Factory::getLog()->log(LogLevel::DEBUG, ' Done with installer seeding.'); } } else { $reason = ' Skipping ' . $ret['filename']; Factory::getLog()->log(LogLevel::DEBUG, $reason); } } else { $this->setError('JPA extraction returned FALSE. The installer image is corrupt.'); return false; } if ($ret['done']) { // We are finished! Close the file fclose($this->_xform_fp); Factory::getLog()->log(LogLevel::DEBUG, 'Initializing with JPA package has finished'); } $ret['filesize'] = $totalSize; return $ret; } /** * Extracts a file from the JPA archive and returns an in-memory array containing it * and its file data. The data returned is an array, consisting of the following keys: * "filename" => relative file path stored in the archive * "data" => file data * "offset" => next offset to use * "skip" => if this is not a file, just skip it... * "done" => No more files left in archive * * @codeCoverageIgnore * * @param integer $offset The absolute data offset from archive's header * * @return array See description for more information */ protected function &_xformExtract($offset) { $false = false; // Used to return false values in case an error occurs // Generate a return array $retArray = array( "filename" => '', // File name extracted "data" => '', // File data "offset" => 0, // Offset in ZIP file "skip" => false, // Skip this? "done" => false // Are we done yet? ); // If we can't open the file, return an error condition if ($this->_xform_fp === false) { return $false; } // Go to the offset specified if ( !fseek($this->_xform_fp, $offset) == 0) { return $false; } // Get and decode Entity Description Block $signature = fread($this->_xform_fp, 3); // Check signature if ($signature == 'JPF') { // This a JPA Entity Block. Process the header. // Read length of EDB and of the Entity Path Data $length_array = unpack('vblocksize/vpathsize', fread($this->_xform_fp, 4)); // Read the path data $file = fread($this->_xform_fp, $length_array['pathsize']); // Read and parse the known data portion $bin_data = fread($this->_xform_fp, 14); $header_data = unpack('Ctype/Ccompression/Vcompsize/Vuncompsize/Vperms', $bin_data); // Read any unknwon data $restBytes = $length_array['blocksize'] - (21 + $length_array['pathsize']); if ($restBytes > 0) { $junk = fread($this->_xform_fp, $restBytes); } $compressionType = $header_data['compression']; // Populate the return array $retArray['filename'] = $file; $retArray['skip'] = ($header_data['compsize'] == 0); // Skip over directories switch ($header_data['type']) { case 0: // directory break; case 1: // file switch ($compressionType) { case 0: // No compression if ($header_data['compsize'] > 0) // 0 byte files do not have data to be read { $retArray['data'] = fread($this->_xform_fp, $header_data['compsize']); } break; case 1: // GZip compression $zipData = fread($this->_xform_fp, $header_data['compsize']); $retArray['data'] = gzinflate($zipData); break; case 2: // BZip2 compression $zipData = fread($this->_xform_fp, $header_data['compsize']); $retArray['data'] = bzdecompress($zipData); break; } break; } } else { // This is not a file header. This means we are done. $retArray['done'] = true; } $retArray['offset'] = ftell($this->_xform_fp); return $retArray; } /** * Skips over the JPA header entry and returns the offset file data starts from * * @codeCoverageIgnore * * @return boolean|integer False on failure, offset otherwise */ protected function _xformReadHeader() { // Fail for unreadable files if ($this->_xform_fp === false) { return false; } // Go to the beggining of the file rewind($this->_xform_fp); // Read the signature $sig = fread($this->_xform_fp, 3); // Not a JPA Archive? if ($sig != 'JPA') { return false; } // Read and parse header length $header_length_array = unpack('v', fread($this->_xform_fp, 2)); $header_length = $header_length_array[1]; // Read and parse the known portion of header data (14 bytes) $bin_data = fread($this->_xform_fp, 14); $header_data = unpack('Cmajor/Cminor/Vcount/Vuncsize/Vcsize', $bin_data); // Load any remaining header data (forward compatibility) $rest_length = $header_length - 19; if ($rest_length > 0) { $junk = fread($this->_xform_fp, $rest_length); } return ftell($this->_xform_fp); } }