diff --git a/administrator/components/com_admin/sql/updates/mysql/3.3.6-2014-09-30.sql b/administrator/components/com_admin/sql/updates/mysql/3.3.6-2014-09-30.sql new file mode 100644 index 0000000000000..73b7c1ba0ed3f --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/3.3.6-2014-09-30.sql @@ -0,0 +1,5 @@ +INSERT INTO `#__update_sites` (`name`, `type`, `location`, `enabled`) VALUES +('Joomla! Update Component Update Site', 'extension', 'http://update.joomla.org/core/extensions/com_joomlaupdate.xml', 1); + +INSERT INTO `#__update_sites_extensions` (`update_site_id`, `extension_id`) VALUES +((SELECT `update_site_id` FROM `#__update_sites` WHERE `name` = 'Joomla! Update Component Update Site'), (SELECT `extension_id` FROM `#__extensions` WHERE `name` = 'com_joomlaupdate')); diff --git a/administrator/components/com_admin/sql/updates/postgresql/3.3.6-2014-09-30.sql b/administrator/components/com_admin/sql/updates/postgresql/3.3.6-2014-09-30.sql new file mode 100644 index 0000000000000..fae9c20d45522 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/3.3.6-2014-09-30.sql @@ -0,0 +1,5 @@ +INSERT INTO "#__update_sites" ("name", "type", "location", "enabled") VALUES +('Joomla! Update Component Update Site', 'extension', 'http://update.joomla.org/core/extensions/com_joomlaupdate.xml', 1); + +INSERT INTO "#__update_sites_extensions" ("update_site_id", "extension_id") VALUES +((SELECT "update_site_id" FROM "#__update_sites" WHERE "name" = 'Joomla! Update Component Update Site'), (SELECT "extension_id" FROM "#__extensions" WHERE "name" = 'com_joomlaupdate')); diff --git a/administrator/components/com_admin/sql/updates/sqlazure/3.3.6-2014-09-30.sql b/administrator/components/com_admin/sql/updates/sqlazure/3.3.6-2014-09-30.sql new file mode 100644 index 0000000000000..309ade5b7c50a --- /dev/null +++ b/administrator/components/com_admin/sql/updates/sqlazure/3.3.6-2014-09-30.sql @@ -0,0 +1,5 @@ +INSERT INTO [#__update_sites] ([name], [type], [location], [enabled]) +SELECT 'Joomla! Update Component Update Site', 'extension', 'http://update.joomla.org/core/extensions/com_joomlaupdate.xml', 1; + +INSERT INTO [#__update_sites_extensions] ([update_site_id], [extension_id]) +SELECT (SELECT [update_site_id] FROM [#__update_sites] WHERE [name] = 'Joomla! Update Component Update Site'), (SELECT [extension_id] FROM [#__extensions] WHERE [name] = 'com_joomlaupdate'); diff --git a/administrator/components/com_categories/views/categories/tmpl/modal.php b/administrator/components/com_categories/views/categories/tmpl/modal.php index 8060242933793..c91bf088da6a8 100644 --- a/administrator/components/com_categories/views/categories/tmpl/modal.php +++ b/administrator/components/com_categories/views/categories/tmpl/modal.php @@ -144,7 +144,7 @@ - id; ?> + id; ?> diff --git a/administrator/components/com_contact/tables/contact.php b/administrator/components/com_contact/tables/contact.php index cc0c8b7590d78..140b7b09221a2 100644 --- a/administrator/components/com_contact/tables/contact.php +++ b/administrator/components/com_contact/tables/contact.php @@ -21,7 +21,7 @@ class ContactTableContact extends JTable * @var array * @since 3.3 */ - protected $jsonEncode = array('params', 'metadata'); + protected $_jsonEncode = array('params', 'metadata'); /** * Constructor diff --git a/administrator/components/com_joomlaupdate/joomlaupdate.xml b/administrator/components/com_joomlaupdate/joomlaupdate.xml index 003bb1d792616..919cfe5e170fc 100644 --- a/administrator/components/com_joomlaupdate/joomlaupdate.xml +++ b/administrator/components/com_joomlaupdate/joomlaupdate.xml @@ -26,5 +26,8 @@ language/en-GB.com_joomlaupdate.sys.ini + + http://update.joomla.org/core/extensions/com_joomlaupdate.xml + diff --git a/administrator/components/com_joomlaupdate/restore.php b/administrator/components/com_joomlaupdate/restore.php index 3d47bf6c96d80..83e74c2878dad 100644 --- a/administrator/components/com_joomlaupdate/restore.php +++ b/administrator/components/com_joomlaupdate/restore.php @@ -1,82 +1,134 @@ |'), - array('*' => '.*', '?' => '.?')) . '$/i', $string + array('*' => '.*', '?' => '.?')) . '$/i', $string ); } } // Unicode-safe binary data length function -if(function_exists('mb_strlen')) { - function akstringlen($string) { return mb_strlen($string,'8bit'); } -} else { - function akstringlen($string) { return strlen($string); } +if (!function_exists('akstringlen')) +{ + if (function_exists('mb_strlen')) + { + function akstringlen($string) + { + return mb_strlen($string, '8bit'); + } + } + else + { + function akstringlen($string) + { + return strlen($string); + } + } } /** * Gets a query parameter from GET or POST data + * * @param $key * @param $default */ -function getQueryParam( $key, $default = null ) +function getQueryParam($key, $default = null) { - $value = null; + $value = $default; - if(array_key_exists($key, $_REQUEST)) { + if (array_key_exists($key, $_REQUEST)) + { $value = $_REQUEST[$key]; - } elseif(array_key_exists($key, $_POST)) { - $value = $_POST[$key]; - } elseif(array_key_exists($key, $_GET)) { - $value = $_GET[$key]; - } else { - return $default; } - if(get_magic_quotes_gpc() && !is_null($value)) $value=stripslashes($value); + if (get_magic_quotes_gpc() && !is_null($value)) + { + $value = stripslashes($value); + } return $value; } +// Debugging function +function debugMsg($msg) +{ + if (!defined('KSDEBUG')) + { + return; + } + + $fp = fopen('debug.txt', 'at'); + + fwrite($fp, $msg . "\n"); + fclose($fp); +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + /** * Akeeba Backup's JSON compatibility layer * @@ -900,6 +952,16 @@ function json_decode($value, $assoc = false) } } +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + /** * The base class of Akeeba Engine objects. Allows for error and warnings logging * and propagation. Largely based on the Joomla! 1.5 JObject class. @@ -1154,438 +1216,823 @@ private function getItemFromArray($array, $i = null) } /** - * File post processor engines base class + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart */ -abstract class AKAbstractPostproc extends AKAbstractObject -{ - /** @var string The current (real) file path we'll have to process */ - protected $filename = null; - - /** @var int The requested permissions */ - protected $perms = 0755; - - /** @var string The temporary file path we gave to the unarchiver engine */ - protected $tempFilename = null; - - /** @var int The UNIX timestamp of the file's desired modification date */ - public $timestamp = 0; +/** + * The superclass of all Akeeba Kickstart parts. The "parts" are intelligent stateful + * classes which perform a single procedure and have preparation, running and + * finalization phases. The transition between phases is handled automatically by + * this superclass' tick() final public method, which should be the ONLY public API + * exposed to the rest of the Akeeba Engine. + */ +abstract class AKAbstractPart extends AKAbstractObject +{ /** - * Processes the current file, e.g. moves it from temp to final location by FTP + * Indicates whether this part has finished its initialisation cycle + * @var boolean */ - abstract public function process(); + protected $isPrepared = false; /** - * The unarchiver tells us the path to the filename it wants to extract and we give it - * a different path instead. - * @param string $filename The path to the real file - * @param int $perms The permissions we need the file to have - * @return string The path to the temporary file + * Indicates whether this part has more work to do (it's in running state) + * @var boolean */ - abstract public function processFilename($filename, $perms = 0755); + protected $isRunning = false; /** - * Recursively creates a directory if it doesn't exist - * @param string $dirName The directory to create - * @param int $perms The permissions to give to that directory + * Indicates whether this part has finished its finalization cycle + * @var boolean */ - abstract public function createDirRecursive( $dirName, $perms ); - - abstract public function chmod( $file, $perms ); - - abstract public function unlink( $file ); - - abstract public function rmdir( $directory ); - - abstract public function rename( $from, $to ); -} - -/** - * The base class of unarchiver classes - */ -abstract class AKAbstractUnarchiver extends AKAbstractPart -{ - /** @var string Archive filename */ - protected $filename = null; - - /** @var array List of the names of all archive parts */ - public $archiveList = array(); - - /** @var int The total size of all archive parts */ - public $totalSize = array(); - - /** @var integer Current archive part number */ - protected $currentPartNumber = -1; - - /** @var integer The offset inside the current part */ - protected $currentPartOffset = 0; - - /** @var bool Should I restore permissions? */ - protected $flagRestorePermissions = false; - - /** @var AKAbstractPostproc Post processing class */ - protected $postProcEngine = null; + protected $isFinished = false; - /** @var string Absolute path to prepend to extracted files */ - protected $addPath = ''; + /** + * Indicates whether this part has finished its run cycle + * @var boolean + */ + protected $hasRan = false; - /** @var array Which files to rename */ - public $renameFiles = array(); + /** + * The name of the engine part (a.k.a. Domain), used in return table + * generation. + * @var string + */ + protected $active_domain = ""; - /** @var array Which directories to rename */ - public $renameDirs = array(); + /** + * The step this engine part is in. Used verbatim in return table and + * should be set by the code in the _run() method. + * @var string + */ + protected $active_step = ""; - /** @var array Which files to skip */ - public $skipFiles = array(); + /** + * A more detailed description of the step this engine part is in. Used + * verbatim in return table and should be set by the code in the _run() + * method. + * @var string + */ + protected $active_substep = ""; - /** @var integer Chunk size for processing */ - protected $chunkSize = 524288; + /** + * Any configuration variables, in the form of an array. + * @var array + */ + protected $_parametersArray = array(); - /** @var resource File pointer to the current archive part file */ - protected $fp = null; + /** @var string The database root key */ + protected $databaseRoot = array(); - /** @var int Run state when processing the current archive file */ - protected $runState = null; + /** @var int Last reported warnings's position in array */ + private $warnings_pointer = -1; - /** @var stdClass File header data, as read by the readFileHeader() method */ - protected $fileHeader = null; + /** @var array An array of observers */ + protected $observers = array(); - /** @var int How much of the uncompressed data we've read so far */ - protected $dataReadLength = 0; + /** + * Runs the preparation for this part. Should set _isPrepared + * to true + */ + abstract protected function _prepare(); /** - * Public constructor + * Runs the finalisation process for this part. Should set + * _isFinished to true. */ - public function __construct() - { - parent::__construct(); - } + abstract protected function _finalize(); /** - * Wakeup function, called whenever the class is unserialized + * Runs the main functionality loop for this part. Upon calling, + * should set the _isRunning to true. When it finished, should set + * the _hasRan to true. If an error is encountered, setError should + * be used. */ - public function __wakeup() - { - if($this->currentPartNumber >= 0) - { - $this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb'); - if( (is_resource($this->fp)) && ($this->currentPartOffset > 0) ) - { - @fseek($this->fp, $this->currentPartOffset); - } - } - } + abstract protected function _run(); /** - * Sleep function, called whenever the class is serialized + * Sets the BREAKFLAG, which instructs this engine part that the current step must break immediately, + * in fear of timing out. */ - public function shutdown() + protected function setBreakFlag() { - if(is_resource($this->fp)) - { - $this->currentPartOffset = @ftell($this->fp); - @fclose($this->fp); - } + AKFactory::set('volatile.breakflag', true); } /** - * Implements the abstract _prepare() method + * Sets the engine part's internal state, in an easy to use manner + * + * @param string $state One of init, prepared, running, postrun, finished, error + * @param string $errorMessage The reported error message, should the state be set to error */ - final protected function _prepare() + protected function setState($state = 'init', $errorMessage='Invalid setState argument') { - parent::__construct(); - - if( count($this->_parametersArray) > 0 ) + switch($state) { - foreach($this->_parametersArray as $key => $value) - { - switch($key) - { - case 'filename': // Archive's absolute filename - $this->filename = $value; - break; - - case 'restore_permissions': // Should I restore permissions? - $this->flagRestorePermissions = $value; - break; + case 'init': + $this->isPrepared = false; + $this->isRunning = false; + $this->isFinished = false; + $this->hasRun = false; + break; - case 'post_proc': // Should I use FTP? - $this->postProcEngine = AKFactory::getpostProc($value); - break; + case 'prepared': + $this->isPrepared = true; + $this->isRunning = false; + $this->isFinished = false; + $this->hasRun = false; + break; - case 'add_path': // Path to prepend - $this->addPath = $value; - $this->addPath = str_replace('\\','/',$this->addPath); - $this->addPath = rtrim($this->addPath,'/'); - if(!empty($this->addPath)) $this->addPath .= '/'; - break; + case 'running': + $this->isPrepared = true; + $this->isRunning = true; + $this->isFinished = false; + $this->hasRun = false; + break; - case 'rename_files': // Which files to rename (hash array) - $this->renameFiles = $value; - break; - - case 'rename_dirs': // Which files to rename (hash array) - $this->renameDirs = $value; - break; + case 'postrun': + $this->isPrepared = true; + $this->isRunning = false; + $this->isFinished = false; + $this->hasRun = true; + break; - case 'skip_files': // Which files to skip (indexed array) - $this->skipFiles = $value; - break; - } - } + case 'finished': + $this->isPrepared = true; + $this->isRunning = false; + $this->isFinished = true; + $this->hasRun = false; + break; + + case 'error': + default: + $this->setError($errorMessage); + break; } + } - $this->scanArchives(); + /** + * The public interface to an engine part. This method takes care for + * calling the correct method in order to perform the initialisation - + * run - finalisation cycle of operation and return a proper reponse array. + * @return array A Reponse Array + */ + final public function tick() + { + // Call the right action method, depending on engine part state + switch( $this->getState() ) + { + case "init": + $this->_prepare(); + break; + case "prepared": + $this->_run(); + break; + case "running": + $this->_run(); + break; + case "postrun": + $this->_finalize(); + break; + } - $this->readArchiveHeader(); - $errMessage = $this->getError(); - if(!empty($errMessage)) + // Send a Return Table back to the caller + $out = $this->_makeReturnTable(); + return $out; + } + + /** + * Returns a copy of the class's status array + * @return array + */ + public function getStatusArray() + { + return $this->_makeReturnTable(); + } + + /** + * Sends any kind of setup information to the engine part. Using this, + * we avoid passing parameters to the constructor of the class. These + * parameters should be passed as an indexed array and should be taken + * into account during the preparation process only. This function will + * set the error flag if it's called after the engine part is prepared. + * + * @param array $parametersArray The parameters to be passed to the + * engine part. + */ + final public function setup( $parametersArray ) + { + if( $this->isPrepared ) { - $this->setState('error', $errMessage); + $this->setState('error', "Can't modify configuration after the preparation of " . $this->active_domain); } else { - $this->runState = AK_STATE_NOFILE; - $this->setState('prepared'); + $this->_parametersArray = $parametersArray; + if(array_key_exists('root', $parametersArray)) + { + $this->databaseRoot = $parametersArray['root']; + } } } - protected function _run() + /** + * Returns the state of this engine part. + * + * @return string The state of this engine part. It can be one of + * error, init, prepared, running, postrun, finished. + */ + final public function getState() { - if($this->getState() == 'postrun') return; - - $this->setState('running'); - - $timer = AKFactory::getTimer(); - - $status = true; - while( $status && ($timer->getTimeLeft() > 0) ) + if( $this->getError() ) { - switch( $this->runState ) - { - case AK_STATE_NOFILE: - $status = $this->readFileHeader(); - if($status) - { - // Send start of file notification - $message = new stdClass; - $message->type = 'startfile'; - $message->content = new stdClass; - if( array_key_exists('realfile', get_object_vars($this->fileHeader)) ) { - $message->content->realfile = $this->fileHeader->realFile; - } else { - $message->content->realfile = $this->fileHeader->file; - } - $message->content->file = $this->fileHeader->file; - if( array_key_exists('compressed', get_object_vars($this->fileHeader)) ) { - $message->content->compressed = $this->fileHeader->compressed; - } else { - $message->content->compressed = 0; - } - $message->content->uncompressed = $this->fileHeader->uncompressed; - $this->notify($message); - } - break; - - case AK_STATE_HEADER: - case AK_STATE_DATA: - $status = $this->processFileData(); - break; + return "error"; + } - case AK_STATE_DATAREAD: - case AK_STATE_POSTPROC: - $this->postProcEngine->timestamp = $this->fileHeader->timestamp; - $status = $this->postProcEngine->process(); - $this->propagateFromObject( $this->postProcEngine ); - $this->runState = AK_STATE_DONE; - break; + if( !($this->isPrepared) ) + { + return "init"; + } - case AK_STATE_DONE: - default: - if($status) - { - // Send end of file notification - $message = new stdClass; - $message->type = 'endfile'; - $message->content = new stdClass; - if( array_key_exists('realfile', get_object_vars($this->fileHeader)) ) { - $message->content->realfile = $this->fileHeader->realFile; - } else { - $message->content->realfile = $this->fileHeader->file; - } - $message->content->file = $this->fileHeader->file; - if( array_key_exists('compressed', get_object_vars($this->fileHeader)) ) { - $message->content->compressed = $this->fileHeader->compressed; - } else { - $message->content->compressed = 0; - } - $message->content->uncompressed = $this->fileHeader->uncompressed; - $this->notify($message); - } - $this->runState = AK_STATE_NOFILE; - continue; - } + if( !($this->isFinished) && !($this->isRunning) && !( $this->hasRun ) && ($this->isPrepared) ) + { + return "prepared"; } - $error = $this->getError(); - if( !$status && ($this->runState == AK_STATE_NOFILE) && empty( $error ) ) + if ( !($this->isFinished) && $this->isRunning && !( $this->hasRun ) ) { - // We just finished - $this->setState('postrun'); + return "running"; } - elseif( !empty($error) ) + + if ( !($this->isFinished) && !($this->isRunning) && $this->hasRun ) { - $this->setState( 'error', $error ); + return "postrun"; } - } - protected function _finalize() - { - // Nothing to do - $this->setState('finished'); + if ( $this->isFinished ) + { + return "finished"; + } } /** - * Returns the base extension of the file, e.g. '.jpa' - * @return string + * Constructs a Response Array based on the engine part's state. + * @return array The Response Array for the current state */ - private function getBaseExtension() + final protected function _makeReturnTable() { - static $baseextension; - - if(empty($baseextension)) + // Get a list of warnings + $warnings = $this->getWarnings(); + // Report only new warnings if there is no warnings queue size + if( $this->_warnings_queue_size == 0 ) { - $basename = basename($this->filename); - $lastdot = strrpos($basename,'.'); - $baseextension = substr($basename, $lastdot); + if( ($this->warnings_pointer > 0) && ($this->warnings_pointer < (count($warnings)) ) ) + { + $warnings = array_slice($warnings, $this->warnings_pointer + 1); + $this->warnings_pointer += count($warnings); + } + else + { + $this->warnings_pointer = count($warnings); + } } - return $baseextension; + $out = array( + 'HasRun' => (!($this->isFinished)), + 'Domain' => $this->active_domain, + 'Step' => $this->active_step, + 'Substep' => $this->active_substep, + 'Error' => $this->getError(), + 'Warnings' => $warnings + ); + + return $out; } - /** - * Scans for archive parts - */ - private function scanArchives() + final protected function setDomain($new_domain) { - $privateArchiveList = array(); + $this->active_domain = $new_domain; + } - // Get the components of the archive filename - $dirname = dirname($this->filename); - $base_extension = $this->getBaseExtension(); - $basename = basename($this->filename, $base_extension); - $this->totalSize = 0; + final public function getDomain() + { + return $this->active_domain; + } - // Scan for multiple parts until we don't find any more of them - $count = 0; - $found = true; - $this->archiveList = array(); - while($found) - { - ++$count; - $extension = substr($base_extension, 0, 2).sprintf('%02d', $count); - $filename = $dirname.DIRECTORY_SEPARATOR.$basename.$extension; - $found = file_exists($filename); - if($found) - { - // Add yet another part, with a numeric-appended filename - $this->archiveList[] = $filename; + final protected function setStep($new_step) + { + $this->active_step = $new_step; + } - $filesize = @filesize($filename); - $this->totalSize += $filesize; + final public function getStep() + { + return $this->active_step; + } - $privateArchiveList[] = array($filename, $filesize); - } - else - { - // Add the last part, with the regular extension - $this->archiveList[] = $this->filename; + final protected function setSubstep($new_substep) + { + $this->active_substep = $new_substep; + } - $filename = $this->filename; - $filesize = @filesize($filename); - $this->totalSize += $filesize; + final public function getSubstep() + { + return $this->active_substep; + } - $privateArchiveList[] = array($filename, $filesize); - } - } + /** + * Attaches an observer object + * @param AKAbstractPartObserver $obs + */ + function attach(AKAbstractPartObserver $obs) { + $this->observers["$obs"] = $obs; + } - $this->currentPartNumber = -1; - $this->currentPartOffset = 0; - $this->runState = AK_STATE_NOFILE; + /** + * Dettaches an observer object + * @param AKAbstractPartObserver $obs + */ + function detach(AKAbstractPartObserver $obs) { + delete($this->observers["$obs"]); + } - // Send start of file notification - $message = new stdClass; - $message->type = 'totalsize'; - $message->content = new stdClass; - $message->content->totalsize = $this->totalSize; - $message->content->filelist = $privateArchiveList; - $this->notify($message); - } + /** + * Notifies observers each time something interesting happened to the part + * @param mixed $message The event object + */ + protected function notify($message) { + foreach ($this->observers as $obs) { + $obs->update($this, $message); + } + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * The base class of unarchiver classes + */ +abstract class AKAbstractUnarchiver extends AKAbstractPart +{ + /** @var string Archive filename */ + protected $filename = null; + + /** @var array List of the names of all archive parts */ + public $archiveList = array(); + + /** @var int The total size of all archive parts */ + public $totalSize = array(); + + /** @var integer Current archive part number */ + protected $currentPartNumber = -1; + + /** @var integer The offset inside the current part */ + protected $currentPartOffset = 0; + + /** @var bool Should I restore permissions? */ + protected $flagRestorePermissions = false; + + /** @var AKAbstractPostproc Post processing class */ + protected $postProcEngine = null; + + /** @var string Absolute path to prepend to extracted files */ + protected $addPath = ''; + + /** @var array Which files to rename */ + public $renameFiles = array(); + + /** @var array Which directories to rename */ + public $renameDirs = array(); + + /** @var array Which files to skip */ + public $skipFiles = array(); + + /** @var integer Chunk size for processing */ + protected $chunkSize = 524288; + + /** @var resource File pointer to the current archive part file */ + protected $fp = null; + + /** @var int Run state when processing the current archive file */ + protected $runState = null; + + /** @var stdClass File header data, as read by the readFileHeader() method */ + protected $fileHeader = null; + + /** @var int How much of the uncompressed data we've read so far */ + protected $dataReadLength = 0; + + /** @var array Unwriteable files in these directories are always ignored and do not cause errors when not extracted */ + protected $ignoreDirectories = array(); /** - * Opens the next part file for reading + * Public constructor */ - protected function nextFile() + public function __construct() { - ++$this->currentPartNumber; + parent::__construct(); + } - if( $this->currentPartNumber > (count($this->archiveList) - 1) ) + /** + * Wakeup function, called whenever the class is unserialized + */ + public function __wakeup() + { + if($this->currentPartNumber >= 0) { - $this->setState('postrun'); - return false; + $this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb'); + if( (is_resource($this->fp)) && ($this->currentPartOffset > 0) ) + { + @fseek($this->fp, $this->currentPartOffset); + } } - else + } + + /** + * Sleep function, called whenever the class is serialized + */ + public function shutdown() + { + if(is_resource($this->fp)) { - if( is_resource($this->fp) ) @fclose($this->fp); - $this->fp = @fopen( $this->archiveList[$this->currentPartNumber], 'rb' ); - fseek($this->fp, 0); - $this->currentPartOffset = 0; - return true; + $this->currentPartOffset = @ftell($this->fp); + @fclose($this->fp); } } /** - * Returns true if we have reached the end of file - * @param $local bool True to return EOF of the local file, false (default) to return if we have reached the end of the archive set - * @return bool True if we have reached End Of File + * Implements the abstract _prepare() method */ - protected function isEOF($local = false) + final protected function _prepare() { - $eof = @feof($this->fp); + parent::__construct(); - if(!$eof) + if( count($this->_parametersArray) > 0 ) { - // Border case: right at the part's end (eeeek!!!). For the life of me, I don't understand why - // feof() doesn't report true. It expects the fp to be positioned *beyond* the EOF to report - // true. Incredible! :( - $position = @ftell($this->fp); - $filesize = @filesize( $this->archiveList[$this->currentPartNumber] ); - if( $position >= $filesize ) $eof = true; + foreach($this->_parametersArray as $key => $value) + { + switch($key) + { + // Archive's absolute filename + case 'filename': + $this->filename = $value; + + // Sanity check + if (!empty($value)) + { + $value = strtolower($value); + + if (strlen($value) > 6) + { + if ( + (substr($value, 0, 7) == 'http://') + || (substr($value, 0, 8) == 'https://') + || (substr($value, 0, 6) == 'ftp://') + || (substr($value, 0, 7) == 'ssh2://') + || (substr($value, 0, 6) == 'ssl://') + ) + { + $this->setState('error', 'Invalid archive location'); + } + } + } + + + + break; + + // Should I restore permissions? + case 'restore_permissions': + $this->flagRestorePermissions = $value; + break; + + // Should I use FTP? + case 'post_proc': + $this->postProcEngine = AKFactory::getpostProc($value); + break; + + // Path to add in the beginning + case 'add_path': + $this->addPath = $value; + $this->addPath = str_replace('\\','/',$this->addPath); + $this->addPath = rtrim($this->addPath,'/'); + if(!empty($this->addPath)) $this->addPath .= '/'; + break; + + // Which files to rename (hash array) + case 'rename_files': + $this->renameFiles = $value; + break; + + // Which files to rename (hash array) + case 'rename_dirs': + $this->renameDirs = $value; + break; + + // Which files to skip (indexed array) + case 'skip_files': + $this->skipFiles = $value; + break; + + // Which directories to ignore when we can't write files in them (indexed array) + case 'ignoredirectories': + $this->ignoreDirectories = $value; + break; + } + } } - if($local) + $this->scanArchives(); + + $this->readArchiveHeader(); + $errMessage = $this->getError(); + if(!empty($errMessage)) { - return $eof; + $this->setState('error', $errMessage); } else { - return $eof && ($this->currentPartNumber >= (count($this->archiveList)-1) ); + $this->runState = AK_STATE_NOFILE; + $this->setState('prepared'); } } - /** - * Tries to make a directory user-writable so that we can write a file to it - * @param $path string A path to a file - */ - protected function setCorrectPermissions($path) + protected function _run() { - static $rootDir = null; - - if(is_null($rootDir)) { - $rootDir = rtrim(AKFactory::get('kickstart.setup.destdir',''),'/\\'); - } - - $directory = rtrim(dirname($path),'/\\'); + if($this->getState() == 'postrun') return; + + $this->setState('running'); + + $timer = AKFactory::getTimer(); + + $status = true; + while( $status && ($timer->getTimeLeft() > 0) ) + { + switch( $this->runState ) + { + case AK_STATE_NOFILE: + debugMsg(__CLASS__.'::_run() - Reading file header'); + $status = $this->readFileHeader(); + if($status) + { + debugMsg(__CLASS__.'::_run() - Preparing to extract '.$this->fileHeader->realFile); + // Send start of file notification + $message = new stdClass; + $message->type = 'startfile'; + $message->content = new stdClass; + if( array_key_exists('realfile', get_object_vars($this->fileHeader)) ) { + $message->content->realfile = $this->fileHeader->realFile; + } else { + $message->content->realfile = $this->fileHeader->file; + } + $message->content->file = $this->fileHeader->file; + if( array_key_exists('compressed', get_object_vars($this->fileHeader)) ) { + $message->content->compressed = $this->fileHeader->compressed; + } else { + $message->content->compressed = 0; + } + $message->content->uncompressed = $this->fileHeader->uncompressed; + $this->notify($message); + } else { + debugMsg(__CLASS__.'::_run() - Could not read file header'); + } + break; + + case AK_STATE_HEADER: + case AK_STATE_DATA: + debugMsg(__CLASS__.'::_run() - Processing file data'); + $status = $this->processFileData(); + break; + + case AK_STATE_DATAREAD: + case AK_STATE_POSTPROC: + debugMsg(__CLASS__.'::_run() - Calling post-processing class'); + $this->postProcEngine->timestamp = $this->fileHeader->timestamp; + $status = $this->postProcEngine->process(); + $this->propagateFromObject( $this->postProcEngine ); + $this->runState = AK_STATE_DONE; + break; + + case AK_STATE_DONE: + default: + if($status) + { + debugMsg(__CLASS__.'::_run() - Finished extracting file'); + // Send end of file notification + $message = new stdClass; + $message->type = 'endfile'; + $message->content = new stdClass; + if( array_key_exists('realfile', get_object_vars($this->fileHeader)) ) { + $message->content->realfile = $this->fileHeader->realFile; + } else { + $message->content->realfile = $this->fileHeader->file; + } + $message->content->file = $this->fileHeader->file; + if( array_key_exists('compressed', get_object_vars($this->fileHeader)) ) { + $message->content->compressed = $this->fileHeader->compressed; + } else { + $message->content->compressed = 0; + } + $message->content->uncompressed = $this->fileHeader->uncompressed; + $this->notify($message); + } + $this->runState = AK_STATE_NOFILE; + continue; + } + } + + $error = $this->getError(); + if( !$status && ($this->runState == AK_STATE_NOFILE) && empty( $error ) ) + { + debugMsg(__CLASS__.'::_run() - Just finished'); + // We just finished + $this->setState('postrun'); + } + elseif( !empty($error) ) + { + debugMsg(__CLASS__.'::_run() - Halted with an error:'); + debugMsg($error); + $this->setState( 'error', $error ); + } + } + + protected function _finalize() + { + // Nothing to do + $this->setState('finished'); + } + + /** + * Returns the base extension of the file, e.g. '.jpa' + * @return string + */ + private function getBaseExtension() + { + static $baseextension; + + if(empty($baseextension)) + { + $basename = basename($this->filename); + $lastdot = strrpos($basename,'.'); + $baseextension = substr($basename, $lastdot); + } + + return $baseextension; + } + + /** + * Scans for archive parts + */ + private function scanArchives() + { + if(defined('KSDEBUG')) { + @unlink('debug.txt'); + } + debugMsg('Preparing to scan archives'); + + $privateArchiveList = array(); + + // Get the components of the archive filename + $dirname = dirname($this->filename); + $base_extension = $this->getBaseExtension(); + $basename = basename($this->filename, $base_extension); + $this->totalSize = 0; + + // Scan for multiple parts until we don't find any more of them + $count = 0; + $found = true; + $this->archiveList = array(); + while($found) + { + ++$count; + $extension = substr($base_extension, 0, 2).sprintf('%02d', $count); + $filename = $dirname.DIRECTORY_SEPARATOR.$basename.$extension; + $found = file_exists($filename); + if($found) + { + debugMsg('- Found archive '.$filename); + // Add yet another part, with a numeric-appended filename + $this->archiveList[] = $filename; + + $filesize = @filesize($filename); + $this->totalSize += $filesize; + + $privateArchiveList[] = array($filename, $filesize); + } + else + { + debugMsg('- Found archive '.$this->filename); + // Add the last part, with the regular extension + $this->archiveList[] = $this->filename; + + $filename = $this->filename; + $filesize = @filesize($filename); + $this->totalSize += $filesize; + + $privateArchiveList[] = array($filename, $filesize); + } + } + debugMsg('Total archive parts: '.$count); + + $this->currentPartNumber = -1; + $this->currentPartOffset = 0; + $this->runState = AK_STATE_NOFILE; + + // Send start of file notification + $message = new stdClass; + $message->type = 'totalsize'; + $message->content = new stdClass; + $message->content->totalsize = $this->totalSize; + $message->content->filelist = $privateArchiveList; + $this->notify($message); + } + + /** + * Opens the next part file for reading + */ + protected function nextFile() + { + debugMsg('Current part is '.$this->currentPartNumber.'; opening the next part'); + ++$this->currentPartNumber; + + if( $this->currentPartNumber > (count($this->archiveList) - 1) ) + { + $this->setState('postrun'); + return false; + } + else + { + if( is_resource($this->fp) ) @fclose($this->fp); + debugMsg('Opening file '.$this->archiveList[$this->currentPartNumber]); + $this->fp = @fopen( $this->archiveList[$this->currentPartNumber], 'rb' ); + if($this->fp === false) { + debugMsg('Could not open file - crash imminent'); + } + fseek($this->fp, 0); + $this->currentPartOffset = 0; + return true; + } + } + + /** + * Returns true if we have reached the end of file + * @param $local bool True to return EOF of the local file, false (default) to return if we have reached the end of the archive set + * @return bool True if we have reached End Of File + */ + protected function isEOF($local = false) + { + $eof = @feof($this->fp); + + if(!$eof) + { + // Border case: right at the part's end (eeeek!!!). For the life of me, I don't understand why + // feof() doesn't report true. It expects the fp to be positioned *beyond* the EOF to report + // true. Incredible! :( + $position = @ftell($this->fp); + $filesize = @filesize( $this->archiveList[$this->currentPartNumber] ); + if($filesize <= 0) { + // 2Gb or more files on a 32 bit version of PHP tend to get screwed up. Meh. + $eof = false; + } elseif( $position >= $filesize ) { + $eof = true; + } + } + + if($local) + { + return $eof; + } + else + { + return $eof && ($this->currentPartNumber >= (count($this->archiveList)-1) ); + } + } + + /** + * Tries to make a directory user-writable so that we can write a file to it + * @param $path string A path to a file + */ + protected function setCorrectPermissions($path) + { + static $rootDir = null; + + if(is_null($rootDir)) { + $rootDir = rtrim(AKFactory::get('kickstart.setup.destdir',''),'/\\'); + } + + $directory = rtrim(dirname($path),'/\\'); if($directory != $rootDir) { // Is this an unwritable directory? if(!is_writeable($directory)) { @@ -1626,12 +2073,12 @@ protected function fread($fp, $length = null) if($length > 0) { $data = fread($fp, $length); } else { - $data = fread($fp); + $data = fread($fp, PHP_INT_MAX); } } else { - $data = fread($fp); + $data = fread($fp, PHP_INT_MAX); } if($data === false) $data = ''; @@ -1644,514 +2091,1468 @@ protected function fread($fp, $length = null) return $data; } -} -/** - * The superclass of all Akeeba Kickstart parts. The "parts" are intelligent stateful - * classes which perform a single procedure and have preparation, running and - * finalization phases. The transition between phases is handled automatically by - * this superclass' tick() final public method, which should be the ONLY public API - * exposed to the rest of the Akeeba Engine. - */ -abstract class AKAbstractPart extends AKAbstractObject -{ /** - * Indicates whether this part has finished its initialisation cycle - * @var boolean + * Is this file or directory contained in a directory we've decided to ignore + * write errors for? This is useful to let the extraction work despite write + * errors in the log, logs and tmp directories which MIGHT be used by the system + * on some low quality hosts and Plesk-powered hosts. + * + * @param string $shortFilename The relative path of the file/directory in the package + * + * @return boolean True if it belongs in an ignored directory */ - protected $isPrepared = false; - - /** - * Indicates whether this part has more work to do (it's in running state) - * @var boolean - */ - protected $isRunning = false; + public function isIgnoredDirectory($shortFilename) + { + return false; + if (substr($shortFilename, -1) == '/') + { + $check = substr($shortFilename, 0, -1); + } + else + { + $check = dirname($shortFilename); + } - /** - * Indicates whether this part has finished its finalization cycle - * @var boolean - */ - protected $isFinished = false; + return in_array($check, $this->ignoreDirectories); + } +} - /** - * Indicates whether this part has finished its run cycle - * @var boolean - */ - protected $hasRan = false; +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ - /** - * The name of the engine part (a.k.a. Domain), used in return table - * generation. - * @var string - */ - protected $active_domain = ""; +/** + * File post processor engines base class + */ +abstract class AKAbstractPostproc extends AKAbstractObject +{ + /** @var string The current (real) file path we'll have to process */ + protected $filename = null; + + /** @var int The requested permissions */ + protected $perms = 0755; + + /** @var string The temporary file path we gave to the unarchiver engine */ + protected $tempFilename = null; + + /** @var int The UNIX timestamp of the file's desired modification date */ + public $timestamp = 0; /** - * The step this engine part is in. Used verbatim in return table and - * should be set by the code in the _run() method. - * @var string + * Processes the current file, e.g. moves it from temp to final location by FTP */ - protected $active_step = ""; + abstract public function process(); /** - * A more detailed description of the step this engine part is in. Used - * verbatim in return table and should be set by the code in the _run() - * method. - * @var string + * The unarchiver tells us the path to the filename it wants to extract and we give it + * a different path instead. + * @param string $filename The path to the real file + * @param int $perms The permissions we need the file to have + * @return string The path to the temporary file */ - protected $active_substep = ""; + abstract public function processFilename($filename, $perms = 0755); /** - * Any configuration variables, in the form of an array. - * @var array + * Recursively creates a directory if it doesn't exist + * @param string $dirName The directory to create + * @param int $perms The permissions to give to that directory */ - protected $_parametersArray = array(); + abstract public function createDirRecursive( $dirName, $perms ); + + abstract public function chmod( $file, $perms ); + + abstract public function unlink( $file ); + + abstract public function rmdir( $directory ); + + abstract public function rename( $from, $to ); +} + + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Descendants of this class can be used in the unarchiver's observer methods (attach, detach and notify) + * @author Nicholas + * + */ +abstract class AKAbstractPartObserver +{ + abstract public function update($object, $message); +} + + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Direct file writer + */ +class AKPostprocDirect extends AKAbstractPostproc +{ + public function process() + { + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + if($restorePerms) + { + @chmod($this->filename, $this->perms); + } + else + { + if(@is_file($this->filename)) + { + @chmod($this->filename, 0644); + } + else + { + @chmod($this->filename, 0755); + } + } + if($this->timestamp > 0) + { + @touch($this->filename, $this->timestamp); + } + return true; + } + + public function processFilename($filename, $perms = 0755) + { + $this->perms = $perms; + $this->filename = $filename; + return $filename; + } + + public function createDirRecursive( $dirName, $perms ) + { + if( AKFactory::get('kickstart.setup.dryrun','0') ) return true; + if (@mkdir($dirName, 0755, true)) { + @chmod($dirName, 0755); + return true; + } + + $root = AKFactory::get('kickstart.setup.destdir'); + $root = rtrim(str_replace('\\','/',$root),'/'); + $dir = rtrim(str_replace('\\','/',$dirName),'/'); + if(strpos($dir, $root) === 0) { + $dir = ltrim(substr($dir, strlen($root)), '/'); + $root .= '/'; + } else { + $root = ''; + } + + if(empty($dir)) return true; + + $dirArray = explode('/', $dir); + $path = ''; + foreach( $dirArray as $dir ) + { + $path .= $dir . '/'; + $ret = is_dir($root.$path) ? true : @mkdir($root.$path); + if( !$ret ) { + // Is this a file instead of a directory? + if(is_file($root.$path) ) + { + @unlink($root.$path); + $ret = @mkdir($root.$path); + } + if( !$ret ) { + $this->setError( AKText::sprintf('COULDNT_CREATE_DIR',$path) ); + return false; + } + } + // Try to set new directory permissions to 0755 + @chmod($root.$path, $perms); + } + return true; + } + + public function chmod( $file, $perms ) + { + if( AKFactory::get('kickstart.setup.dryrun','0') ) return true; + + return @chmod( $file, $perms ); + } + + public function unlink( $file ) + { + return @unlink( $file ); + } + + public function rmdir( $directory ) + { + return @rmdir( $directory ); + } + + public function rename( $from, $to ) + { + return @rename($from, $to); + } + +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * FTP file writer + */ +class AKPostprocFTP extends AKAbstractPostproc +{ + /** @var bool Should I use FTP over implicit SSL? */ + public $useSSL = false; + /** @var bool use Passive mode? */ + public $passive = true; + /** @var string FTP host name */ + public $host = ''; + /** @var int FTP port */ + public $port = 21; + /** @var string FTP user name */ + public $user = ''; + /** @var string FTP password */ + public $pass = ''; + /** @var string FTP initial directory */ + public $dir = ''; + /** @var resource The FTP handle */ + private $handle = null; + /** @var string The temporary directory where the data will be stored */ + private $tempDir = ''; + + public function __construct() + { + parent::__construct(); + + $this->useSSL = AKFactory::get('kickstart.ftp.ssl', false); + $this->passive = AKFactory::get('kickstart.ftp.passive', true); + $this->host = AKFactory::get('kickstart.ftp.host', ''); + $this->port = AKFactory::get('kickstart.ftp.port', 21); + if(trim($this->port) == '') $this->port = 21; + $this->user = AKFactory::get('kickstart.ftp.user', ''); + $this->pass = AKFactory::get('kickstart.ftp.pass', ''); + $this->dir = AKFactory::get('kickstart.ftp.dir', ''); + $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); + + $connected = $this->connect(); + + if($connected) + { + if(!empty($this->tempDir)) + { + $tempDir = rtrim($this->tempDir, '/\\').'/'; + $writable = $this->isDirWritable($tempDir); + } + else + { + $tempDir = ''; + $writable = false; + } + + if(!$writable) { + // Default temporary directory is the current root + $tempDir = KSROOTDIR; + if(empty($tempDir)) + { + // Oh, we have no directory reported! + $tempDir = '.'; + } + $absoluteDirToHere = $tempDir; + $tempDir = rtrim(str_replace('\\','/',$tempDir),'/'); + if(!empty($tempDir)) $tempDir .= '/'; + $this->tempDir = $tempDir; + // Is this directory writable? + $writable = $this->isDirWritable($tempDir); + } + + if(!$writable) + { + // Nope. Let's try creating a temporary directory in the site's root. + $tempDir = $absoluteDirToHere.'/kicktemp'; + $this->createDirRecursive($tempDir, 0777); + // Try making it writable... + $this->fixPermissions($tempDir); + $writable = $this->isDirWritable($tempDir); + } + + // Was the new directory writable? + if(!$writable) + { + // Let's see if the user has specified one + $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); + if(!empty($userdir)) + { + // Is it an absolute or a relative directory? + $absolute = false; + $absolute = $absolute || ( substr($userdir,0,1) == '/' ); + $absolute = $absolute || ( substr($userdir,1,1) == ':' ); + $absolute = $absolute || ( substr($userdir,2,1) == ':' ); + if(!$absolute) + { + // Make absolute + $tempDir = $absoluteDirToHere.$userdir; + } + else + { + // it's already absolute + $tempDir = $userdir; + } + // Does the directory exist? + if( is_dir($tempDir) ) + { + // Yeah. Is it writable? + $writable = $this->isDirWritable($tempDir); + } + } + } + $this->tempDir = $tempDir; + + if(!$writable) + { + // No writable directory found!!! + $this->setError(AKText::_('FTP_TEMPDIR_NOT_WRITABLE')); + } + else + { + AKFactory::set('kickstart.ftp.tempdir', $tempDir); + $this->tempDir = $tempDir; + } + } + } + + function __wakeup() + { + $this->connect(); + } + + public function connect() + { + // Connect to server, using SSL if so required + if($this->useSSL) { + $this->handle = @ftp_ssl_connect($this->host, $this->port); + } else { + $this->handle = @ftp_connect($this->host, $this->port); + } + if($this->handle === false) + { + $this->setError(AKText::_('WRONG_FTP_HOST')); + return false; + } + + // Login + if(! @ftp_login($this->handle, $this->user, $this->pass)) + { + $this->setError(AKText::_('WRONG_FTP_USER')); + @ftp_close($this->handle); + return false; + } + + // Change to initial directory + if(! @ftp_chdir($this->handle, $this->dir)) + { + $this->setError(AKText::_('WRONG_FTP_PATH1')); + @ftp_close($this->handle); + return false; + } + + // Enable passive mode if the user requested it + if( $this->passive ) + { + @ftp_pasv($this->handle, true); + } + else + { + @ftp_pasv($this->handle, false); + } + + // Try to download ourselves + $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); + $tempHandle = fopen('php://temp', 'r+'); + if (@ftp_fget($this->handle, $tempHandle, $testFilename, FTP_ASCII, 0) === false) + { + $this->setError(AKText::_('WRONG_FTP_PATH2')); + @ftp_close($this->handle); + fclose($tempHandle); + + return false; + } + fclose($tempHandle); + + return true; + } + + public function process() + { + if( is_null($this->tempFilename) ) + { + // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + return true; + } + + $remotePath = dirname($this->filename); + $removePath = AKFactory::get('kickstart.setup.destdir',''); + if(!empty($removePath)) + { + $removePath = ltrim($removePath, "/"); + $remotePath = ltrim($remotePath, "/"); + $left = substr($remotePath, 0, strlen($removePath)); + if($left == $removePath) + { + $remotePath = substr($remotePath, strlen($removePath)); + } + } + + $absoluteFSPath = dirname($this->filename); + $relativeFTPPath = trim($remotePath, '/'); + $absoluteFTPPath = '/'.trim( $this->dir, '/' ).'/'.trim($remotePath, '/'); + $onlyFilename = basename($this->filename); + + $remoteName = $absoluteFTPPath.'/'.$onlyFilename; + + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); + if($ret === false) + { + $ret = $this->createDirRecursive( $absoluteFSPath, 0755); + if($ret === false) { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + return false; + } + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); + if($ret === false) { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + return false; + } + } + + $ret = @ftp_put($this->handle, $remoteName, $this->tempFilename, FTP_BINARY); + if($ret === false) + { + // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($this->filename); + $this->unlink($this->filename); + + $fp = @fopen($this->tempFilename, 'rb'); + if($fp !== false) + { + $ret = @ftp_fput($this->handle, $remoteName, $fp, FTP_BINARY); + @fclose($fp); + } + else + { + $ret = false; + } + } + @unlink($this->tempFilename); + + if($ret === false) + { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + return false; + } + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + if($restorePerms) + { + @ftp_chmod($this->_handle, $this->perms, $remoteName); + } + else + { + @ftp_chmod($this->_handle, 0644, $remoteName); + } + return true; + } + + public function processFilename($filename, $perms = 0755) + { + // Catch some error conditions... + if($this->getError()) + { + return false; + } + + // If a null filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + if(is_null($filename)) + { + $this->filename = null; + $this->tempFilename = null; + return null; + } + + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir',''); + if(!empty($removePath)) + { + $left = substr($filename, 0, strlen($removePath)); + if($left == $removePath) + { + $filename = substr($filename, strlen($removePath)); + } + } + + // Trim slash on the left + $filename = ltrim($filename, '/'); + + $this->filename = $filename; + $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); + $this->perms = $perms; + + if( empty($this->tempFilename) ) + { + // Oops! Let's try something different + $this->tempFilename = $this->tempDir.'/kickstart-'.time().'.dat'; + } + + return $this->tempFilename; + } + + private function isDirWritable($dir) + { + $fp = @fopen($dir.'/kickstart.dat', 'wb'); + if($fp === false) + { + return false; + } + else + { + @fclose($fp); + unlink($dir.'/kickstart.dat'); + return true; + } + } + + public function createDirRecursive( $dirName, $perms ) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir',''); + if(!empty($removePath)) + { + // UNIXize the paths + $removePath = str_replace('\\','/',$removePath); + $dirName = str_replace('\\','/',$dirName); + // Make sure they both end in a slash + $removePath = rtrim($removePath,'/\\').'/'; + $dirName = rtrim($dirName,'/\\').'/'; + // Process the path removal + $left = substr($dirName, 0, strlen($removePath)); + if($left == $removePath) + { + $dirName = substr($dirName, strlen($removePath)); + } + } + if(empty($dirName)) $dirName = ''; // 'cause the substr() above may return FALSE. + + $check = '/'.trim($this->dir,'/').'/'.trim($dirName, '/'); + if($this->is_dir($check)) return true; + + $alldirs = explode('/', $dirName); + $previousDir = '/'.trim($this->dir); + foreach($alldirs as $curdir) + { + $check = $previousDir.'/'.$curdir; + if(!$this->is_dir($check)) + { + // Proactively try to delete a file by the same name + @ftp_delete($this->handle, $check); + + if(@ftp_mkdir($this->handle, $check) === false) + { + // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($removePath.$check); + if(@ftp_mkdir($this->handle, $check) === false) + { + // Can we fall back to pure PHP mode, sire? + if(!@mkdir($check)) + { + $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); + return false; + } + else + { + // Since the directory was built by PHP, change its permissions + @chmod($check, "0777"); + return true; + } + } + } + @ftp_chmod($this->handle, $perms, $check); + } + $previousDir = $check; + } + + return true; + } + + public function close() + { + @ftp_close($this->handle); + } + + /* + * Tries to fix directory/file permissions in the PHP level, so that + * the FTP operation doesn't fail. + * @param $path string The full path to a directory or file + */ + private function fixPermissions( $path ) + { + // Turn off error reporting + if(!defined('KSDEBUG')) { + $oldErrorReporting = @error_reporting(E_NONE); + } + + // Get UNIX style paths + $relPath = str_replace('\\','/',$path); + $basePath = rtrim(str_replace('\\','/',KSROOTDIR),'/'); + $basePath = rtrim($basePath,'/'); + if(!empty($basePath)) $basePath .= '/'; + // Remove the leading relative root + if( substr($relPath,0,strlen($basePath)) == $basePath ) + $relPath = substr($relPath,strlen($basePath)); + $dirArray = explode('/', $relPath); + $pathBuilt = rtrim($basePath,'/'); + foreach( $dirArray as $dir ) + { + if(empty($dir)) continue; + $oldPath = $pathBuilt; + $pathBuilt .= '/'.$dir; + if(is_dir($oldPath.$dir)) + { + @chmod($oldPath.$dir, 0777); + } + else + { + if(@chmod($oldPath.$dir, 0777) === false) + { + @unlink($oldPath.$dir); + } + } + } + + // Restore error reporting + if(!defined('KSDEBUG')) { + @error_reporting($oldErrorReporting); + } + } + + public function chmod( $file, $perms ) + { + return @ftp_chmod($this->handle, $perms, $file); + } + + private function is_dir( $dir ) + { + return @ftp_chdir( $this->handle, $dir ); + } + + public function unlink( $file ) + { + $removePath = AKFactory::get('kickstart.setup.destdir',''); + if(!empty($removePath)) + { + $left = substr($file, 0, strlen($removePath)); + if($left == $removePath) + { + $file = substr($file, strlen($removePath)); + } + } + + $check = '/'.trim($this->dir,'/').'/'.trim($file, '/'); + + return @ftp_delete( $this->handle, $check ); + } + + public function rmdir( $directory ) + { + $removePath = AKFactory::get('kickstart.setup.destdir',''); + if(!empty($removePath)) + { + $left = substr($directory, 0, strlen($removePath)); + if($left == $removePath) + { + $directory = substr($directory, strlen($removePath)); + } + } + + $check = '/'.trim($this->dir,'/').'/'.trim($directory, '/'); + + return @ftp_rmdir( $this->handle, $check ); + } + + public function rename( $from, $to ) + { + $originalFrom = $from; + $originalTo = $to; + + $removePath = AKFactory::get('kickstart.setup.destdir',''); + if(!empty($removePath)) + { + $left = substr($from, 0, strlen($removePath)); + if($left == $removePath) + { + $from = substr($from, strlen($removePath)); + } + } + $from = '/'.trim($this->dir,'/').'/'.trim($from, '/'); + + if(!empty($removePath)) + { + $left = substr($to, 0, strlen($removePath)); + if($left == $removePath) + { + $to = substr($to, strlen($removePath)); + } + } + $to = '/'.trim($this->dir,'/').'/'.trim($to, '/'); + + $result = @ftp_rename( $this->handle, $from, $to ); + if($result !== true) + { + return @rename($from, $to); + } + else + { + return true; + } + } + +} + + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * FTP file writer + */ +class AKPostprocSFTP extends AKAbstractPostproc +{ + /** @var bool Should I use FTP over implicit SSL? */ + public $useSSL = false; + /** @var bool use Passive mode? */ + public $passive = true; + /** @var string FTP host name */ + public $host = ''; + /** @var int FTP port */ + public $port = 21; + /** @var string FTP user name */ + public $user = ''; + /** @var string FTP password */ + public $pass = ''; + /** @var string FTP initial directory */ + public $dir = ''; + + /** @var resource SFTP resource handle */ + private $handle = null; + + /** @var resource SSH2 connection resource handle */ + private $_connection = null; + + /** @var string Current remote directory, including the remote directory string */ + private $_currentdir; + + /** @var string The temporary directory where the data will be stored */ + private $tempDir = ''; + + public function __construct() + { + parent::__construct(); + + $this->host = AKFactory::get('kickstart.ftp.host', ''); + $this->port = AKFactory::get('kickstart.ftp.port', 22); + + if(trim($this->port) == '') $this->port = 22; + + $this->user = AKFactory::get('kickstart.ftp.user', ''); + $this->pass = AKFactory::get('kickstart.ftp.pass', ''); + $this->dir = AKFactory::get('kickstart.ftp.dir', ''); + $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); + + $connected = $this->connect(); + + if($connected) + { + if(!empty($this->tempDir)) + { + $tempDir = rtrim($this->tempDir, '/\\').'/'; + $writable = $this->isDirWritable($tempDir); + } + else + { + $tempDir = ''; + $writable = false; + } + + if(!$writable) { + // Default temporary directory is the current root + $tempDir = KSROOTDIR; + if(empty($tempDir)) + { + // Oh, we have no directory reported! + $tempDir = '.'; + } + $absoluteDirToHere = $tempDir; + $tempDir = rtrim(str_replace('\\','/',$tempDir),'/'); + if(!empty($tempDir)) $tempDir .= '/'; + $this->tempDir = $tempDir; + // Is this directory writable? + $writable = $this->isDirWritable($tempDir); + } + + if(!$writable) + { + // Nope. Let's try creating a temporary directory in the site's root. + $tempDir = $absoluteDirToHere.'/kicktemp'; + $this->createDirRecursive($tempDir, 0777); + // Try making it writable... + $this->fixPermissions($tempDir); + $writable = $this->isDirWritable($tempDir); + } + + // Was the new directory writable? + if(!$writable) + { + // Let's see if the user has specified one + $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); + if(!empty($userdir)) + { + // Is it an absolute or a relative directory? + $absolute = false; + $absolute = $absolute || ( substr($userdir,0,1) == '/' ); + $absolute = $absolute || ( substr($userdir,1,1) == ':' ); + $absolute = $absolute || ( substr($userdir,2,1) == ':' ); + if(!$absolute) + { + // Make absolute + $tempDir = $absoluteDirToHere.$userdir; + } + else + { + // it's already absolute + $tempDir = $userdir; + } + // Does the directory exist? + if( is_dir($tempDir) ) + { + // Yeah. Is it writable? + $writable = $this->isDirWritable($tempDir); + } + } + } + $this->tempDir = $tempDir; + + if(!$writable) + { + // No writable directory found!!! + $this->setError(AKText::_('SFTP_TEMPDIR_NOT_WRITABLE')); + } + else + { + AKFactory::set('kickstart.ftp.tempdir', $tempDir); + $this->tempDir = $tempDir; + } + } + } + + function __wakeup() + { + $this->connect(); + } + + public function connect() + { + $this->_connection = false; - /** @var string The database root key */ - protected $databaseRoot = array(); + if(!function_exists('ssh2_connect')) + { + $this->setError(AKText::_('SFTP_NO_SSH2')); + return false; + } - /** @var int Last reported warnings's position in array */ - private $warnings_pointer = -1; + $this->_connection = @ssh2_connect($this->host, $this->port); - /** @var array An array of observers */ - protected $observers = array(); + if (!@ssh2_auth_password($this->_connection, $this->user, $this->pass)) + { + $this->setError(AKText::_('SFTP_WRONG_USER')); - /** - * Runs the preparation for this part. Should set _isPrepared - * to true - */ - abstract protected function _prepare(); + $this->_connection = false; - /** - * Runs the finalisation process for this part. Should set - * _isFinished to true. - */ - abstract protected function _finalize(); + return false; + } - /** - * Runs the main functionality loop for this part. Upon calling, - * should set the _isRunning to true. When it finished, should set - * the _hasRan to true. If an error is encountered, setError should - * be used. - */ - abstract protected function _run(); + $this->handle = @ssh2_sftp($this->_connection); - /** - * Sets the BREAKFLAG, which instructs this engine part that the current step must break immediately, - * in fear of timing out. - */ - protected function setBreakFlag() - { - AKFactory::set('volatile.breakflag', true); - } + // I must have an absolute directory + if(!$this->dir) + { + $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); + return false; + } - /** - * Sets the engine part's internal state, in an easy to use manner - * - * @param string $state One of init, prepared, running, postrun, finished, error - * @param string $errorMessage The reported error message, should the state be set to error - */ - protected function setState($state = 'init', $errorMessage='Invalid setState argument') - { - switch($state) - { - case 'init': - $this->isPrepared = false; - $this->isRunning = false; - $this->isFinished = false; - $this->hasRun = false; - break; + // Change to initial directory + if(!$this->sftp_chdir('/')) + { + $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); - case 'prepared': - $this->isPrepared = true; - $this->isRunning = false; - $this->isFinished = false; - $this->hasRun = false; - break; + unset($this->_connection); + unset($this->handle); - case 'running': - $this->isPrepared = true; - $this->isRunning = true; - $this->isFinished = false; - $this->hasRun = false; - break; + return false; + } - case 'postrun': - $this->isPrepared = true; - $this->isRunning = false; - $this->isFinished = false; - $this->hasRun = true; - break; + // Try to download ourselves + $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); + $basePath = '/'.trim($this->dir, '/'); - case 'finished': - $this->isPrepared = true; - $this->isRunning = false; - $this->isFinished = true; - $this->hasRun = false; - break; + if(@fopen("ssh2.sftp://{$this->handle}$basePath/$testFilename",'r+') === false) + { + $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); - case 'error': - default: - $this->setError($errorMessage); - break; - } + unset($this->_connection); + unset($this->handle); + + return false; + } + + return true; } - /** - * The public interface to an engine part. This method takes care for - * calling the correct method in order to perform the initialisation - - * run - finalisation cycle of operation and return a proper reponse array. - * @return array A Reponse Array - */ - final public function tick() + public function process() { - // Call the right action method, depending on engine part state - switch( $this->getState() ) + if( is_null($this->tempFilename) ) { - case "init": - $this->_prepare(); - break; - case "prepared": - $this->_run(); - break; - case "running": - $this->_run(); - break; - case "postrun": - $this->_finalize(); - break; + // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + return true; } - // Send a Return Table back to the caller - $out = $this->_makeReturnTable(); - return $out; - } + $remotePath = dirname($this->filename); + $absoluteFSPath = dirname($this->filename); + $absoluteFTPPath = '/'.trim( $this->dir, '/' ).'/'.trim($remotePath, '/'); + $onlyFilename = basename($this->filename); - /** - * Returns a copy of the class's status array - * @return array - */ - public function getStatusArray() - { - return $this->_makeReturnTable(); - } + $remoteName = $absoluteFTPPath.'/'.$onlyFilename; - /** - * Sends any kind of setup information to the engine part. Using this, - * we avoid passing parameters to the constructor of the class. These - * parameters should be passed as an indexed array and should be taken - * into account during the preparation process only. This function will - * set the error flag if it's called after the engine part is prepared. - * - * @param array $parametersArray The parameters to be passed to the - * engine part. - */ - final public function setup( $parametersArray ) - { - if( $this->isPrepared ) - { - $this->setState('error', "Can't modify configuration after the preparation of " . $this->active_domain); - } - else + $ret = $this->sftp_chdir($absoluteFTPPath); + + if($ret === false) { - $this->_parametersArray = $parametersArray; - if(array_key_exists('root', $parametersArray)) - { - $this->databaseRoot = $parametersArray['root']; + $ret = $this->createDirRecursive( $absoluteFSPath, 0755); + + if($ret === false) + { + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + return false; } - } - } - /** - * Returns the state of this engine part. - * - * @return string The state of this engine part. It can be one of - * error, init, prepared, running, postrun, finished. - */ - final public function getState() - { - if( $this->getError() ) - { - return "error"; - } + $ret = $this->sftp_chdir($absoluteFTPPath); - if( !($this->isPrepared) ) - { - return "init"; + if($ret === false) + { + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + return false; + } } - if( !($this->isFinished) && !($this->isRunning) && !( $this->hasRun ) && ($this->isPrepared) ) + // Create the file + $ret = $this->write($this->tempFilename, $remoteName); + + // If I got a -1 it means that I wasn't able to open the file, so I have to stop here + if($ret === -1) + { + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + return false; + } + + if($ret === false) { - return "prepared"; + // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($this->filename); + $this->unlink($this->filename); + + $ret = $this->write($this->tempFilename, $remoteName); } - if ( !($this->isFinished) && $this->isRunning && !( $this->hasRun ) ) + @unlink($this->tempFilename); + + if($ret === false) { - return "running"; + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + return false; } + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); - if ( !($this->isFinished) && !($this->isRunning) && $this->hasRun ) + if($restorePerms) { - return "postrun"; + $this->chmod($remoteName, $this->perms); } - - if ( $this->isFinished ) + else { - return "finished"; + $this->chmod($remoteName, 0644); } + return true; } - /** - * Constructs a Response Array based on the engine part's state. - * @return array The Response Array for the current state - */ - final protected function _makeReturnTable() + public function processFilename($filename, $perms = 0755) { - // Get a list of warnings - $warnings = $this->getWarnings(); - // Report only new warnings if there is no warnings queue size - if( $this->_warnings_queue_size == 0 ) + // Catch some error conditions... + if($this->getError()) { - if( ($this->warnings_pointer > 0) && ($this->warnings_pointer < (count($warnings)) ) ) - { - $warnings = array_slice($warnings, $this->warnings_pointer + 1); - $this->warnings_pointer += count($warnings); - } - else - { - $this->warnings_pointer = count($warnings); - } + return false; } - $out = array( - 'HasRun' => (!($this->isFinished)), - 'Domain' => $this->active_domain, - 'Step' => $this->active_step, - 'Substep' => $this->active_substep, - 'Error' => $this->getError(), - 'Warnings' => $warnings - ); - - return $out; - } + // If a null filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + if(is_null($filename)) + { + $this->filename = null; + $this->tempFilename = null; + return null; + } - final protected function setDomain($new_domain) - { - $this->active_domain = $new_domain; - } + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir',''); + if(!empty($removePath)) + { + $left = substr($filename, 0, strlen($removePath)); + if($left == $removePath) + { + $filename = substr($filename, strlen($removePath)); + } + } - final public function getDomain() - { - return $this->active_domain; - } + // Trim slash on the left + $filename = ltrim($filename, '/'); - final protected function setStep($new_step) - { - $this->active_step = $new_step; - } + $this->filename = $filename; + $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); + $this->perms = $perms; - final public function getStep() - { - return $this->active_step; - } + if( empty($this->tempFilename) ) + { + // Oops! Let's try something different + $this->tempFilename = $this->tempDir.'/kickstart-'.time().'.dat'; + } - final protected function setSubstep($new_substep) - { - $this->active_substep = $new_substep; + return $this->tempFilename; } - final public function getSubstep() + private function isDirWritable($dir) { - return $this->active_substep; + if(@fopen("ssh2.sftp://{$this->handle}$dir/kickstart.dat",'wb') === false) + { + return false; + } + else + { + @ssh2_sftp_unlink($this->handle, $dir.'/kickstart.dat'); + return true; + } } - /** - * Attaches an observer object - * @param AKAbstractPartObserver $obs - */ - function attach(AKAbstractPartObserver $obs) { - $this->observers["$obs"] = $obs; - } + public function createDirRecursive( $dirName, $perms ) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir',''); + if(!empty($removePath)) + { + // UNIXize the paths + $removePath = str_replace('\\','/',$removePath); + $dirName = str_replace('\\','/',$dirName); + // Make sure they both end in a slash + $removePath = rtrim($removePath,'/\\').'/'; + $dirName = rtrim($dirName,'/\\').'/'; + // Process the path removal + $left = substr($dirName, 0, strlen($removePath)); + if($left == $removePath) + { + $dirName = substr($dirName, strlen($removePath)); + } + } + if(empty($dirName)) $dirName = ''; // 'cause the substr() above may return FALSE. - /** - * Dettaches an observer object - * @param AKAbstractPartObserver $obs - */ - function detach(AKAbstractPartObserver $obs) { - delete($this->observers["$obs"]); - } + $check = '/'.trim($this->dir,'/ ').'/'.trim($dirName, '/'); - /** - * Notifies observers each time something interesting happened to the part - * @param mixed $message The event object - */ - protected function notify($message) { - foreach ($this->observers as $obs) { - $obs->update($this, $message); + if($this->is_dir($check)) + { + return true; } - } -} -/** - * Descendants of this class can be used in the unarchiver's observer methods (attach, detach and notify) - * @author Nicholas - * - */ -abstract class AKAbstractPartObserver -{ - abstract public function update($object, $message); -} + $alldirs = explode('/', $dirName); + $previousDir = '/'.trim($this->dir, '/ '); -/** - * Direct file writer - */ -class AKPostprocDirect extends AKAbstractPostproc -{ - public function process() - { - $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); - if($restorePerms) - { - @chmod($this->filename, $this->perms); - } - else + foreach($alldirs as $curdir) { - if(@is_file($this->filename)) - { - @chmod($this->filename, 0644); - } - else + if(!$curdir) + { + continue; + } + + $check = $previousDir.'/'.$curdir; + + if(!$this->is_dir($check)) { - @chmod($this->filename, 0755); + // Proactively try to delete a file by the same name + @ssh2_sftp_unlink($this->handle, $check); + + if(@ssh2_sftp_mkdir($this->handle, $check) === false) + { + // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($check); + + if(@ssh2_sftp_mkdir($this->handle, $check) === false) + { + // Can we fall back to pure PHP mode, sire? + if(!@mkdir($check)) + { + $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); + return false; + } + else + { + // Since the directory was built by PHP, change its permissions + @chmod($check, "0777"); + return true; + } + } + } + + @ssh2_sftp_chmod($this->handle, $check, $perms); } + + $previousDir = $check; } - if($this->timestamp > 0) - { - @touch($this->filename, $this->timestamp); - } + return true; } - public function processFilename($filename, $perms = 0755) + public function close() { - $this->perms = $perms; - $this->filename = $filename; - return $filename; + unset($this->_connection); + unset($this->handle); } - public function createDirRecursive( $dirName, $perms ) + /* + * Tries to fix directory/file permissions in the PHP level, so that + * the FTP operation doesn't fail. + * @param $path string The full path to a directory or file + */ + private function fixPermissions( $path ) { - if( AKFactory::get('kickstart.setup.dryrun','0') ) return true; - if (@mkdir($dirName, 0755, true)) { - @chmod($dirName, 0755); - return true; + // Turn off error reporting + if(!defined('KSDEBUG')) { + $oldErrorReporting = @error_reporting(E_NONE); } - $root = AKFactory::get('kickstart.setup.destdir'); - $root = rtrim(str_replace('\\','/',$root),'/'); - $dir = rtrim(str_replace('\\','/',$dirName),'/'); - if(strpos($dir, $root) === 0) { - $dir = ltrim(substr($dir, strlen($root)), '/'); - $root .= '/'; - } else { - $root = ''; - } - - if(empty($dir)) return true; + // Get UNIX style paths + $relPath = str_replace('\\','/',$path); + $basePath = rtrim(str_replace('\\','/',KSROOTDIR),'/'); + $basePath = rtrim($basePath,'/'); + + if(!empty($basePath)) + { + $basePath .= '/'; + } + + // Remove the leading relative root + if( substr($relPath,0,strlen($basePath)) == $basePath ) + { + $relPath = substr($relPath,strlen($basePath)); + } + + $dirArray = explode('/', $relPath); + $pathBuilt = rtrim($basePath,'/'); - $dirArray = explode('/', $dir); - $path = ''; foreach( $dirArray as $dir ) { - $path .= $dir . '/'; - $ret = is_dir($root.$path) ? true : @mkdir($root.$path); - if( !$ret ) { - // Is this a file instead of a directory? - if(is_file($root.$path) ) + if(empty($dir)) + { + continue; + } + + $oldPath = $pathBuilt; + $pathBuilt .= '/'.$dir; + + if(is_dir($oldPath.'/'.$dir)) + { + @chmod($oldPath.'/'.$dir, 0777); + } + else + { + if(@chmod($oldPath.'/'.$dir, 0777) === false) { - @unlink($root.$path); - $ret = @mkdir($root.$path); - } - if( !$ret ) { - $this->setError( AKText::sprintf('COULDNT_CREATE_DIR',$path) ); - return false; + @unlink($oldPath.$dir); } } - // Try to set new directory permissions to 0755 - @chmod($root.$path, $perms); } - return true; + + // Restore error reporting + if(!defined('KSDEBUG')) { + @error_reporting($oldErrorReporting); + } } public function chmod( $file, $perms ) { - if( AKFactory::get('kickstart.setup.dryrun','0') ) return true; + return @ssh2_sftp_chmod($this->handle, $file, $perms); + } - return @chmod( $file, $perms ); + private function is_dir( $dir ) + { + return $this->sftp_chdir($dir); } + private function write($local, $remote) + { + $fp = @fopen("ssh2.sftp://{$this->handle}$remote",'w'); + $localfp = @fopen($local,'rb'); + + if($fp === false) + { + return -1; + } + + if($localfp === false) + { + @fclose($fp); + return -1; + } + + $res = true; + + while(!feof($localfp) && ($res !== false)) + { + $buffer = @fread($localfp, 65567); + $res = @fwrite($fp, $buffer); + } + + @fclose($fp); + @fclose($localfp); + + return $res; + } + public function unlink( $file ) { - return @unlink( $file ); + $check = '/'.trim($this->dir,'/').'/'.trim($file, '/'); + + return @ssh2_sftp_unlink($this->handle, $check); } public function rmdir( $directory ) { - return @rmdir( $directory ); + $check = '/'.trim($this->dir,'/').'/'.trim($directory, '/'); + + return @ssh2_sftp_rmdir( $this->handle, $check); } public function rename( $from, $to ) { - return @rename($from, $to); + $from = '/'.trim($this->dir,'/').'/'.trim($from, '/'); + $to = '/'.trim($this->dir,'/').'/'.trim($to, '/'); + + $result = @ssh2_sftp_rename($this->handle, $from, $to); + + if($result !== true) + { + return @rename($from, $to); + } + else + { + return true; + } } + /** + * Changes to the requested directory in the remote server. You give only the + * path relative to the initial directory and it does all the rest by itself, + * including doing nothing if the remote directory is the one we want. + * + * @param string $dir The (realtive) remote directory + * + * @return bool True if successful, false otherwise. + */ + private function sftp_chdir($dir) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir',''); + if(!empty($removePath)) + { + // UNIXize the paths + $removePath = str_replace('\\','/',$removePath); + $dir = str_replace('\\','/',$dir); + + // Make sure they both end in a slash + $removePath = rtrim($removePath,'/\\').'/'; + $dir = rtrim($dir,'/\\').'/'; + + // Process the path removal + $left = substr($dir, 0, strlen($removePath)); + + if($left == $removePath) + { + $dir = substr($dir, strlen($removePath)); + } + } + + if(empty($dir)) + { + // Because the substr() above may return FALSE. + $dir = ''; + } + + // Calculate "real" (absolute) SFTP path + $realdir = substr($this->dir, -1) == '/' ? substr($this->dir, 0, strlen($this->dir) - 1) : $this->dir; + $realdir .= '/'.$dir; + $realdir = substr($realdir, 0, 1) == '/' ? $realdir : '/'.$realdir; + + if($this->_currentdir == $realdir) + { + // Already there, do nothing + return true; + } + + $result = @ssh2_sftp_stat($this->handle, $realdir); + + if($result === false) + { + return false; + } + else + { + // Update the private "current remote directory" variable + $this->_currentdir = $realdir; + + return true; + } + } + } + /** - * FTP file writer + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart */ -class AKPostprocFTP extends AKAbstractPostproc + +/** + * Hybrid direct / FTP mode file writer + */ +class AKPostprocHybrid extends AKAbstractPostproc { + + /** @var bool Should I use the FTP layer? */ + public $useFTP = false; + /** @var bool Should I use FTP over implicit SSL? */ public $useSSL = false; + /** @var bool use Passive mode? */ public $passive = true; + /** @var string FTP host name */ public $host = ''; + /** @var int FTP port */ public $port = 21; + /** @var string FTP user name */ public $user = ''; + /** @var string FTP password */ public $pass = ''; + /** @var string FTP initial directory */ public $dir = ''; + /** @var resource The FTP handle */ private $handle = null; + /** @var string The temporary directory where the data will be stored */ private $tempDir = ''; + /** @var null The FTP connection handle */ + private $_handle = null; + + /** + * Public constructor. Tries to connect to the FTP server. + */ public function __construct() { parent::__construct(); + $this->useFTP = true; $this->useSSL = AKFactory::get('kickstart.ftp.ssl', false); $this->passive = AKFactory::get('kickstart.ftp.passive', true); $this->host = AKFactory::get('kickstart.ftp.host', ''); $this->port = AKFactory::get('kickstart.ftp.port', 21); - if(trim($this->port) == '') $this->port = 21; $this->user = AKFactory::get('kickstart.ftp.user', ''); $this->pass = AKFactory::get('kickstart.ftp.pass', ''); $this->dir = AKFactory::get('kickstart.ftp.dir', ''); $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); + if (trim($this->port) == '') + { + $this->port = 21; + } + + // If FTP is not configured, skip it altogether + if (empty($this->host) || empty($this->user) || empty($this->pass)) + { + $this->useFTP = false; + } + + // Try to connect to the FTP server $connected = $this->connect(); - if($connected) + // If the connection fails, skip FTP altogether + if (!$connected) { - if(!empty($this->tempDir)) + $this->useFTP = false; + } + + if ($connected) + { + if (!empty($this->tempDir)) { - $tempDir = rtrim($this->tempDir, '/\\').'/'; + $tempDir = rtrim($this->tempDir, '/\\') . '/'; $writable = $this->isDirWritable($tempDir); } else @@ -2160,26 +3561,30 @@ public function __construct() $writable = false; } - if(!$writable) { + if (!$writable) + { // Default temporary directory is the current root - $tempDir = function_exists('getcwd') ? getcwd() : dirname(__FILE__); - if(empty($tempDir)) + $tempDir = KSROOTDIR; + if (empty($tempDir)) { // Oh, we have no directory reported! $tempDir = '.'; } $absoluteDirToHere = $tempDir; - $tempDir = rtrim(str_replace('\\','/',$tempDir),'/'); - if(!empty($tempDir)) $tempDir .= '/'; + $tempDir = rtrim(str_replace('\\', '/', $tempDir), '/'); + if (!empty($tempDir)) + { + $tempDir .= '/'; + } $this->tempDir = $tempDir; // Is this directory writable? $writable = $this->isDirWritable($tempDir); } - if(!$writable) + if (!$writable) { // Nope. Let's try creating a temporary directory in the site's root. - $tempDir = $absoluteDirToHere.'/kicktemp'; + $tempDir = $absoluteDirToHere . '/kicktemp'; $this->createDirRecursive($tempDir, 0777); // Try making it writable... $this->fixPermissions($tempDir); @@ -2187,21 +3592,21 @@ public function __construct() } // Was the new directory writable? - if(!$writable) + if (!$writable) { // Let's see if the user has specified one $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); - if(!empty($userdir)) + if (!empty($userdir)) { // Is it an absolute or a relative directory? $absolute = false; - $absolute = $absolute || ( substr($userdir,0,1) == '/' ); - $absolute = $absolute || ( substr($userdir,1,1) == ':' ); - $absolute = $absolute || ( substr($userdir,2,1) == ':' ); - if(!$absolute) + $absolute = $absolute || (substr($userdir, 0, 1) == '/'); + $absolute = $absolute || (substr($userdir, 1, 1) == ':'); + $absolute = $absolute || (substr($userdir, 2, 1) == ':'); + if (!$absolute) { // Make absolute - $tempDir = $absoluteDirToHere.$userdir; + $tempDir = $absoluteDirToHere . $userdir; } else { @@ -2209,7 +3614,7 @@ public function __construct() $tempDir = $userdir; } // Does the directory exist? - if( is_dir($tempDir) ) + if (is_dir($tempDir)) { // Yeah. Is it writable? $writable = $this->isDirWritable($tempDir); @@ -2218,7 +3623,7 @@ public function __construct() } $this->tempDir = $tempDir; - if(!$writable) + if (!$writable) { // No writable directory found!!! $this->setError(AKText::_('FTP_TEMPDIR_NOT_WRITABLE')); @@ -2231,43 +3636,73 @@ public function __construct() } } + /** + * Called after unserialisation, tries to reconnect to FTP + */ function __wakeup() { - $this->connect(); + if ($this->useFTP) + { + $this->connect(); + } + } + + function __destruct() + { + if (!$this->useFTP) + { + @ftp_close($this->handle); + } } + /** + * Tries to connect to the FTP server + * + * @return bool + */ public function connect() { + if (!$this->useFTP) + { + return false; + } + // Connect to server, using SSL if so required - if($this->useSSL) { + if ($this->useSSL) + { $this->handle = @ftp_ssl_connect($this->host, $this->port); - } else { + } + else + { $this->handle = @ftp_connect($this->host, $this->port); } - if($this->handle === false) + if ($this->handle === false) { $this->setError(AKText::_('WRONG_FTP_HOST')); + return false; } // Login - if(! @ftp_login($this->handle, $this->user, $this->pass)) + if (!@ftp_login($this->handle, $this->user, $this->pass)) { $this->setError(AKText::_('WRONG_FTP_USER')); @ftp_close($this->handle); + return false; } // Change to initial directory - if(! @ftp_chdir($this->handle, $this->dir)) + if (!@ftp_chdir($this->handle, $this->dir)) { $this->setError(AKText::_('WRONG_FTP_PATH1')); @ftp_close($this->handle); + return false; } // Enable passive mode if the user requested it - if( $this->passive ) + if ($this->passive) { @ftp_pasv($this->handle, true); } @@ -2276,12 +3711,32 @@ public function connect() @ftp_pasv($this->handle, false); } + // Try to download ourselves + $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); + $tempHandle = fopen('php://temp', 'r+'); + + if (@ftp_fget($this->handle, $tempHandle, $testFilename, FTP_ASCII, 0) === false) + { + $this->setError(AKText::_('WRONG_FTP_PATH2')); + @ftp_close($this->handle); + fclose($tempHandle); + + return false; + } + + fclose($tempHandle); + return true; } + /** + * Post-process an extracted file, using FTP or direct file writes to move it + * + * @return bool + */ public function process() { - if( is_null($this->tempFilename) ) + if (is_null($this->tempFilename)) { // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. // the entity was a directory or symlink @@ -2289,13 +3744,16 @@ public function process() } $remotePath = dirname($this->filename); - $removePath = AKFactory::get('kickstart.setup.destdir',''); - if(!empty($removePath)) + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + $root = rtrim($removePath, '/\\'); + + if (!empty($removePath)) { $removePath = ltrim($removePath, "/"); $remotePath = ltrim($remotePath, "/"); $left = substr($remotePath, 0, strlen($removePath)); - if($left == $removePath) + + if ($left == $removePath) { $remotePath = substr($remotePath, strlen($removePath)); } @@ -2303,86 +3761,124 @@ public function process() $absoluteFSPath = dirname($this->filename); $relativeFTPPath = trim($remotePath, '/'); - $absoluteFTPPath = '/'.trim( $this->dir, '/' ).'/'.trim($remotePath, '/'); + $absoluteFTPPath = '/' . trim($this->dir, '/') . '/' . trim($remotePath, '/'); $onlyFilename = basename($this->filename); - $remoteName = $absoluteFTPPath.'/'.$onlyFilename; + $remoteName = $absoluteFTPPath . '/' . $onlyFilename; - $ret = @ftp_chdir($this->handle, $absoluteFTPPath); - if($ret === false) + // Does the directory exist? + if (!is_dir($root . '/' . $absoluteFSPath)) { - $ret = $this->createDirRecursive( $absoluteFSPath, 0755); - if($ret === false) { - $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); - return false; + $ret = $this->createDirRecursive($absoluteFSPath, 0755); + + if (($ret === false) && ($this->useFTP)) + { + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); } - $ret = @ftp_chdir($this->handle, $absoluteFTPPath); - if($ret === false) { + + if ($ret === false) + { $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + return false; } } - $ret = @ftp_put($this->handle, $remoteName, $this->tempFilename, FTP_BINARY); - if($ret === false) + if ($this->useFTP) + { + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); + } + + // Try copying directly + $ret = @copy($this->tempFilename, $root . '/' . $this->filename); + + if ($ret === false) { - // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! $this->fixPermissions($this->filename); $this->unlink($this->filename); - $fp = @fopen($this->tempFilename); - if($fp !== false) - { - $ret = @ftp_fput($this->handle, $remoteName, $fp, FTP_BINARY); - @fclose($fp); - } - else + $ret = @copy($this->tempFilename, $root . '/' . $this->filename); + } + + if ($this->useFTP && ($ret === false)) + { + $ret = @ftp_put($this->handle, $remoteName, $this->tempFilename, FTP_BINARY); + + if ($ret === false) { - $ret = false; + // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($this->filename); + $this->unlink($this->filename); + + $fp = @fopen($this->tempFilename, 'rb'); + if ($fp !== false) + { + $ret = @ftp_fput($this->handle, $remoteName, $fp, FTP_BINARY); + @fclose($fp); + } + else + { + $ret = false; + } } } + @unlink($this->tempFilename); - if($ret === false) + if ($ret === false) { $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + return false; } + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); - if($restorePerms) + $perms = $restorePerms ? $this->perms : 0644; + + $ret = @chmod($root . '/' . $this->filename, $perms); + + if ($this->useFTP && ($ret === false)) { @ftp_chmod($this->_handle, $perms, $remoteName); } - else - { - @ftp_chmod($this->_handle, 0644, $remoteName); - } + return true; } + /** + * Create a temporary filename + * + * @param string $filename The original filename + * @param int $perms The file permissions + * + * @return string + */ public function processFilename($filename, $perms = 0755) { // Catch some error conditions... - if($this->getError()) + if ($this->getError()) { return false; } // If a null filename is passed, it means that we shouldn't do any post processing, i.e. // the entity was a directory or symlink - if(is_null($filename)) + if (is_null($filename)) { $this->filename = null; $this->tempFilename = null; + return null; } // Strip absolute filesystem path to website's root - $removePath = AKFactory::get('kickstart.setup.destdir',''); - if(!empty($removePath)) + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + + if (!empty($removePath)) { $left = substr($filename, 0, strlen($removePath)); - if($left == $removePath) + + if ($left == $removePath) { $filename = substr($filename, strlen($removePath)); } @@ -2395,243 +3891,371 @@ public function processFilename($filename, $perms = 0755) $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); $this->perms = $perms; - if( empty($this->tempFilename) ) + if (empty($this->tempFilename)) { // Oops! Let's try something different - $this->tempFilename = $this->tempDir.'/kickstart-'.time().'.dat'; + $this->tempFilename = $this->tempDir . '/kickstart-' . time() . '.dat'; } return $this->tempFilename; } + /** + * Is the directory writeable? + * + * @param string $dir The directory ti check + * + * @return bool + */ private function isDirWritable($dir) { - $fp = @fopen($dir.'/kickstart.dat', 'wb'); - if($fp === false) + $fp = @fopen($dir . '/kickstart.dat', 'wb'); + + if ($fp === false) { return false; } - else - { - @fclose($fp); - unlink($dir.'/kickstart.dat'); - return true; - } + + @fclose($fp); + unlink($dir . '/kickstart.dat'); + + return true; } - public function createDirRecursive( $dirName, $perms ) + /** + * Create a directory, recursively + * + * @param string $dirName The directory to create + * @param int $perms The permissions to give to the directory + * + * @return bool + */ + public function createDirRecursive($dirName, $perms) { // Strip absolute filesystem path to website's root - $removePath = AKFactory::get('kickstart.setup.destdir',''); - if(!empty($removePath)) + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + + if (!empty($removePath)) { // UNIXize the paths - $removePath = str_replace('\\','/',$removePath); - $dirName = str_replace('\\','/',$dirName); + $removePath = str_replace('\\', '/', $removePath); + $dirName = str_replace('\\', '/', $dirName); // Make sure they both end in a slash - $removePath = rtrim($removePath,'/\\').'/'; - $dirName = rtrim($dirName,'/\\').'/'; + $removePath = rtrim($removePath, '/\\') . '/'; + $dirName = rtrim($dirName, '/\\') . '/'; // Process the path removal $left = substr($dirName, 0, strlen($removePath)); - if($left == $removePath) + + if ($left == $removePath) { $dirName = substr($dirName, strlen($removePath)); } } - if(empty($dirName)) $dirName = ''; // 'cause the substr() above may return FALSE. - - $check = '/'.trim($this->dir,'/').'/'.trim($dirName, '/'); - if($this->is_dir($check)) return true; + + // 'cause the substr() above may return FALSE. + if (empty($dirName)) + { + $dirName = ''; + } + + $check = '/' . trim($this->dir, '/') . '/' . trim($dirName, '/'); + $checkFS = $removePath . trim($dirName, '/'); + + if ($this->is_dir($check)) + { + return true; + } $alldirs = explode('/', $dirName); - $previousDir = '/'.trim($this->dir); - foreach($alldirs as $curdir) + $previousDir = '/' . trim($this->dir); + $previousDirFS = rtrim($removePath, '/\\'); + + foreach ($alldirs as $curdir) { - $check = $previousDir.'/'.$curdir; - if(!$this->is_dir($check)) + $check = $previousDir . '/' . $curdir; + $checkFS = $previousDirFS . '/' . $curdir; + + if (!is_dir($checkFS) && !$this->is_dir($check)) { // Proactively try to delete a file by the same name - @ftp_delete($this->handle, $check); + if (!@unlink($checkFS) && $this->useFTP) + { + @ftp_delete($this->handle, $check); + } - if(@ftp_mkdir($this->handle, $check) === false) + $createdDir = @mkdir($checkFS, 0755); + + if (!$createdDir && $this->useFTP) + { + $createdDir = @ftp_mkdir($this->handle, $check); + } + + if ($createdDir === false) { // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! - $this->fixPermissions($removePath.$check); - if(@ftp_mkdir($this->handle, $check) === false) + $this->fixPermissions($checkFS); + + $createdDir = @mkdir($checkFS, 0755); + if (!$createdDir && $this->useFTP) { - // Can we fall back to pure PHP mode, sire? - if(!@mkdir($check)) - { - $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR',$dir)); - return false; - } - else - { - // Since the directory was built by PHP, change its permissions - @chmod($check, "0777"); - return true; - } + $createdDir = @ftp_mkdir($this->handle, $check); + } + + if ($createdDir === false) + { + $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); + + return false; } } - @ftp_chmod($this->handle, $perms, $check); + + if (!@chmod($checkFS, $perms) && $this->useFTP) + { + @ftp_chmod($this->handle, $perms, $check); + } } + $previousDir = $check; + $previousDirFS = $checkFS; } return true; } + /** + * Closes the FTP connection + */ public function close() { - @ftp_close($this->handle); + if (!$this->useFTP) + { + @ftp_close($this->handle); + } } - /* + /** * Tries to fix directory/file permissions in the PHP level, so that * the FTP operation doesn't fail. + * * @param $path string The full path to a directory or file */ - private function fixPermissions( $path ) + private function fixPermissions($path) { // Turn off error reporting - if(!defined('KSDEBUG')) { + if (!defined('KSDEBUG')) + { $oldErrorReporting = @error_reporting(E_NONE); } - // Get UNIX style paths - $relPath = str_replace('\\','/',$path); - $basePath = rtrim(str_replace('\\','/',dirname(__FILE__)),'/'); - $basePath = rtrim($basePath,'/'); - if(!empty($basePath)) $basePath .= '/'; + // Get UNIX style paths + $relPath = str_replace('\\', '/', $path); + $basePath = rtrim(str_replace('\\', '/', KSROOTDIR), '/'); + $basePath = rtrim($basePath, '/'); + + if (!empty($basePath)) + { + $basePath .= '/'; + } + // Remove the leading relative root - if( substr($relPath,0,strlen($basePath)) == $basePath ) - $relPath = substr($relPath,strlen($basePath)); + if (substr($relPath, 0, strlen($basePath)) == $basePath) + { + $relPath = substr($relPath, strlen($basePath)); + } + $dirArray = explode('/', $relPath); - $pathBuilt = rtrim($basePath,'/'); - foreach( $dirArray as $dir ) + $pathBuilt = rtrim($basePath, '/'); + + foreach ($dirArray as $dir) { - if(empty($dir)) continue; + if (empty($dir)) + { + continue; + } + $oldPath = $pathBuilt; - $pathBuilt .= '/'.$dir; - if(is_dir($oldPath.$dir)) + $pathBuilt .= '/' . $dir; + + if (is_dir($oldPath . $dir)) { - @chmod($oldPath.$dir, 0777); + @chmod($oldPath . $dir, 0777); } else { - if(@chmod($oldPath.$dir, 0777) === false) + if (@chmod($oldPath . $dir, 0777) === false) { - @unlink($oldPath.$dir); + @unlink($oldPath . $dir); } } } // Restore error reporting - if(!defined('KSDEBUG')) { + if (!defined('KSDEBUG')) + { @error_reporting($oldErrorReporting); } } - public function chmod( $file, $perms ) + public function chmod($file, $perms) { - return @ftp_chmod($this->handle, $perms, $path); + if (AKFactory::get('kickstart.setup.dryrun', '0')) + { + return true; + } + + $ret = @chmod($file, $perms); + + if (!$ret && $this->useFTP) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + + if (!empty($removePath)) + { + $left = substr($file, 0, strlen($removePath)); + + if ($left == $removePath) + { + $file = substr($file, strlen($removePath)); + } + } + + // Trim slash on the left + $file = ltrim($file, '/'); + + $ret = @ftp_chmod($this->handle, $perms, $file); + } + + return $ret; } - private function is_dir( $dir ) + private function is_dir($dir) { - return @ftp_chdir( $this->handle, $dir ); + if ($this->useFTP) + { + return @ftp_chdir($this->handle, $dir); + } + + return false; } - public function unlink( $file ) + public function unlink($file) { - $removePath = AKFactory::get('kickstart.setup.destdir',''); - if(!empty($removePath)) + $ret = @unlink($file); + + if (!$ret && $this->useFTP) { - $left = substr($file, 0, strlen($removePath)); - if($left == $removePath) + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) { - $file = substr($file, strlen($removePath)); + $left = substr($file, 0, strlen($removePath)); + if ($left == $removePath) + { + $file = substr($file, strlen($removePath)); + } } - } - $check = '/'.trim($this->dir,'/').'/'.trim($file, '/'); + $check = '/' . trim($this->dir, '/') . '/' . trim($file, '/'); - return @ftp_delete( $this->handle, $check ); + $ret = @ftp_delete($this->handle, $check); + } + + return $ret; } - public function rmdir( $directory ) + public function rmdir($directory) { - $removePath = AKFactory::get('kickstart.setup.destdir',''); - if(!empty($removePath)) + $ret = @rmdir($directory); + + if (!$ret && $this->useFTP) { - $left = substr($directory, 0, strlen($removePath)); - if($left == $removePath) + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) { - $directory = substr($directory, strlen($removePath)); + $left = substr($directory, 0, strlen($removePath)); + if ($left == $removePath) + { + $directory = substr($directory, strlen($removePath)); + } } - } - $check = '/'.trim($this->dir,'/').'/'.trim($directory, '/'); + $check = '/' . trim($this->dir, '/') . '/' . trim($directory, '/'); - return @ftp_rmdir( $this->handle, $check ); + $ret = @ftp_rmdir($this->handle, $check); + } + + return $ret; } - public function rename( $from, $to ) + public function rename($from, $to) { - $originalFrom = $from; - $originalTo = $to; + $ret = @rename($from, $to); - $removePath = AKFactory::get('kickstart.setup.destdir',''); - if(!empty($removePath)) + if (!$ret && $this->useFTP) { - $left = substr($from, 0, strlen($removePath)); - if($left == $removePath) + $originalFrom = $from; + $originalTo = $to; + + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) { - $from = substr($from, strlen($removePath)); + $left = substr($from, 0, strlen($removePath)); + if ($left == $removePath) + { + $from = substr($from, strlen($removePath)); + } } - } - $from = '/'.trim($this->dir,'/').'/'.trim($from, '/'); + $from = '/' . trim($this->dir, '/') . '/' . trim($from, '/'); - if(!empty($removePath)) - { - $left = substr($to, 0, strlen($removePath)); - if($left == $removePath) + if (!empty($removePath)) { - $to = substr($to, strlen($removePath)); + $left = substr($to, 0, strlen($removePath)); + if ($left == $removePath) + { + $to = substr($to, strlen($removePath)); + } } - } - $to = '/'.trim($this->dir,'/').'/'.trim($to, '/'); + $to = '/' . trim($this->dir, '/') . '/' . trim($to, '/'); - $result = @ftp_rename( $this->handle, $from, $to ); - if($result !== true) - { - return @rename($from, $to); - } - else - { - return true; + $ret = @ftp_rename($this->handle, $from, $to); } - } + return $ret; + } } +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + /** * JPA archive extraction class */ class AKUnarchiverJPA extends AKAbstractUnarchiver { - private $archiveHeaderData = array(); + protected $archiveHeaderData = array(); protected function readArchiveHeader() { + debugMsg('Preparing to read archive header'); // Initialize header data array $this->archiveHeaderData = new stdClass(); // Open the first part + debugMsg('Opening the first part'); $this->nextFile(); // Fail for unreadable files - if( $this->fp === false ) return false; + if( $this->fp === false ) { + debugMsg('Could not open the first part'); + return false; + } // Read the signature $sig = fread( $this->fp, 3 ); @@ -2639,6 +4263,7 @@ protected function readArchiveHeader() if ($sig != 'JPA') { // Not a JPA file + debugMsg('Invalid archive signature'); $this->setError( AKText::_('ERR_NOT_A_JPA_FILE') ); return false; } @@ -2675,6 +4300,14 @@ protected function readArchiveHeader() $this->archiveHeaderData->{$key} = $value; } + debugMsg('Header data:'); + debugMsg('Length : '.$header_length); + debugMsg('Major : '.$header_data['major']); + debugMsg('Minor : '.$header_data['minor']); + debugMsg('File count : '.$header_data['count']); + debugMsg('Uncompressed size : '.$header_data['uncsize']); + debugMsg('Compressed size : '.$header_data['csize']); + $this->currentPartOffset = @ftell($this->fp); $this->dataReadLength = 0; @@ -2690,9 +4323,11 @@ protected function readFileHeader() { // If the current part is over, proceed to the next part please if( $this->isEOF(true) ) { + debugMsg('Archive part EOF; moving to next file'); $this->nextFile(); } + debugMsg('Reading file signature'); // Get and decode Entity Description Block $signature = fread($this->fp, 3); @@ -2708,6 +4343,7 @@ protected function readFileHeader() $this->nextFile(); if(!$this->isEOF(false)) { + debugMsg('Invalid file signature before end of archive encountered'); $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); return false; } @@ -2716,9 +4352,22 @@ protected function readFileHeader() } else { - // This is not a file block! The archive is corrupt. - $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); - return false; + $screwed = true; + if(AKFactory::get('kickstart.setup.ignoreerrors', false)) { + debugMsg('Invalid file block signature; launching heuristic file block signature scanner'); + $screwed = !$this->heuristicFileHeaderLocator(); + if(!$screwed) { + $signature = 'JPF'; + } else { + debugMsg('Heuristics failed. Brace yourself for the imminent crash.'); + } + } + if($screwed) { + debugMsg('Invalid file block signature'); + // This is not a file block! The archive is corrupt. + $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); + return false; + } } } // This a JPA Entity Block. Process the header. @@ -2744,7 +4393,7 @@ protected function readFileHeader() $isRenamed = true; } } - + // Handle directory renaming $isDirRenamed = false; if(is_array($this->renameDirs) && (count($this->renameDirs) > 0)) { @@ -2847,6 +4496,7 @@ protected function readFileHeader() // If we have a banned file, let's skip it if($isBannedFile) { + debugMsg('Skipping file '.$this->fileHeader->file); // Advance the file pointer, skipping exactly the size of the compressed data $seekleft = $this->fileHeader->compressed; while($seekleft > 0) @@ -2948,6 +4598,10 @@ protected function processFileData() } break; + + default: + debugMsg('Unknown file type '.$this->fileHeader->type); + break; } } @@ -2963,7 +4617,7 @@ private function processTypeFileUncompressed() // Open the output file if( !AKFactory::get('kickstart.setup.dryrun','0') ) { - $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false); + $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); if ($this->dataReadLength == 0) { $outfp = @fopen( $this->fileHeader->realFile, 'wb' ); } else { @@ -2973,6 +4627,7 @@ private function processTypeFileUncompressed() // Can we write to the file? if( ($outfp === false) && (!$ignore) ) { // An error occured + debugMsg('Could not write to output file'); $this->setError( AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile) ); return false; } @@ -3012,6 +4667,7 @@ private function processTypeFileUncompressed() else { // Nope. The archive is corrupt + debugMsg('Not enough data in file. The archive is truncated or corrupt.'); $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') ); return false; } @@ -3050,9 +4706,10 @@ private function processTypeFileCompressedSimple() $outfp = @fopen( $this->fileHeader->realFile, 'wb' ); // Can we write to the file? - $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false); + $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); if( ($outfp === false) && (!$ignore) ) { // An error occured + debugMsg('Could not write to output file'); $this->setError( AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile) ); return false; } @@ -3082,6 +4739,7 @@ private function processTypeFileCompressedSimple() } else { + debugMsg('End of local file before reading all data with no more parts left. The archive is corrupt or truncated.'); $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') ); return false; } @@ -3137,6 +4795,7 @@ private function processTypeLink() } else { + debugMsg('End of local file before reading all data with no more parts left. The archive is corrupt or truncated.'); // Nope. The archive is corrupt $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') ); return false; @@ -3179,8 +4838,8 @@ protected function createDirectory() if(empty($this->fileHeader->realFile)) $this->fileHeader->realFile = $this->fileHeader->file; $lastSlash = strrpos($this->fileHeader->realFile, '/'); $dirName = substr( $this->fileHeader->realFile, 0, $lastSlash); - $perms = $this->flagRestorePermissions ? $retArray['permissions'] : 0755; - $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false); + $perms = $this->flagRestorePermissions ? $this->fileHeader->permissions : 0755; + $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($dirName); if( ($this->postProcEngine->createDirRecursive($dirName, $perms) == false) && (!$ignore) ) { $this->setError( AKText::sprintf('COULDNT_CREATE_DIR', $dirName) ); return false; @@ -3190,8 +4849,53 @@ protected function createDirectory() return true; } } + + protected function heuristicFileHeaderLocator() + { + $ret = false; + $fullEOF = false; + + while(!$ret && !$fullEOF) { + $this->currentPartOffset = @ftell($this->fp); + if($this->isEOF(true)) { + $this->nextFile(); + } + + if($this->isEOF(false)) { + $fullEOF = true; + continue; + } + + // Read 512Kb + $chunk = fread($this->fp, 524288); + $size_read = mb_strlen($chunk,'8bit'); + //$pos = strpos($chunk, 'JPF'); + $pos = mb_strpos($chunk, 'JPF', 0, '8bit'); + if($pos !== false) { + // We found it! + $this->currentPartOffset += $pos + 3; + @fseek($this->fp, $this->currentPartOffset, SEEK_SET); + $ret = true; + } else { + // Not yet found :( + $this->currentPartOffset = @ftell($this->fp); + } + } + + return $ret; + } } +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + /** * ZIP archive extraction class * @@ -3206,21 +4910,31 @@ class AKUnarchiverZIP extends AKUnarchiverJPA protected function readArchiveHeader() { + debugMsg('Preparing to read archive header'); // Initialize header data array $this->archiveHeaderData = new stdClass(); // Open the first part + debugMsg('Opening the first part'); $this->nextFile(); // Fail for unreadable files - if( $this->fp === false ) return false; + if( $this->fp === false ) { + debugMsg('The first part is not readable'); + return false; + } // Read a possible multipart signature $sigBinary = fread( $this->fp, 4 ); $headerData = unpack('Vsig', $sigBinary); // Roll back if it's not a multipart archive - if( $headerData['sig'] == 0x04034b50 ) fseek($this->fp, -4, SEEK_CUR); + if( $headerData['sig'] == 0x04034b50 ) { + debugMsg('The archive is not multipart'); + fseek($this->fp, -4, SEEK_CUR); + } else { + debugMsg('The archive is multipart'); + } $multiPartSigs = array( 0x08074b50, // Multi-part ZIP @@ -3229,11 +4943,13 @@ protected function readArchiveHeader() ); if( !in_array($headerData['sig'], $multiPartSigs) ) { + debugMsg('Invalid header signature '.dechex($headerData['sig'])); $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); return false; } $this->currentPartOffset = @ftell($this->fp); + debugMsg('Current part offset after reading header: '.$this->currentPartOffset); $this->dataReadLength = 0; @@ -3248,6 +4964,7 @@ protected function readFileHeader() { // If the current part is over, proceed to the next part please if( $this->isEOF(true) ) { + debugMsg('Opening next archive part'); $this->nextFile(); } @@ -3261,23 +4978,17 @@ protected function readFileHeader() if($junk['sig'] == 0x08074b50) { // Yes, there was a signature $junk = @fread($this->fp, 12); - if(defined('KSDEBUG')) { - debugMsg('Data descriptor (w/ header) skipped at '.(ftell($this->fp)-12)); - } + debugMsg('Data descriptor (w/ header) skipped at '.(ftell($this->fp)-12)); } else { // No, there was no signature, just read another 8 bytes $junk = @fread($this->fp, 8); - if(defined('KSDEBUG')) { - debugMsg('Data descriptor (w/out header) skipped at '.(ftell($this->fp)-8)); - } + debugMsg('Data descriptor (w/out header) skipped at '.(ftell($this->fp)-8)); } // And check for EOF, too if( $this->isEOF(true) ) { - if(defined('KSDEBUG')) { - debugMsg('EOF before reading header'); - } - + debugMsg('EOF before reading header'); + $this->nextFile(); } } @@ -3289,16 +5000,12 @@ protected function readFileHeader() // Check signature if(!( $headerData['sig'] == 0x04034b50 )) { - if(defined('KSDEBUG')) { - debugMsg('Not a file signature at '.(ftell($this->fp)-4)); - } - + debugMsg('Not a file signature at '.(ftell($this->fp)-4)); + // The signature is not the one used for files. Is this a central directory record (i.e. we're done)? if($headerData['sig'] == 0x02014b50) { - if(defined('KSDEBUG')) { - debugMsg('EOCD signature at '.(ftell($this->fp)-4)); - } + debugMsg('EOCD signature at '.(ftell($this->fp)-4)); // End of ZIP file detected. We'll just skip to the end of file... while( $this->nextFile() ) {}; @fseek($this->fp, 0, SEEK_END); // Go to EOF @@ -3306,9 +5013,7 @@ protected function readFileHeader() } else { - if(defined('KSDEBUG')) { - debugMsg( 'Invalid signature ' . dechex($headerData['sig']) . ' at '.ftell($this->fp) ); - } + debugMsg( 'Invalid signature ' . dechex($headerData['sig']) . ' at '.ftell($this->fp) ); $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); return false; } @@ -3323,23 +5028,23 @@ protected function readFileHeader() // Read the last modified data and time $lastmodtime = $headerData['lastmodtime']; $lastmoddate = $headerData['lastmoddate']; - + if($lastmoddate && $lastmodtime) { // ----- Extract time $v_hour = ($lastmodtime & 0xF800) >> 11; $v_minute = ($lastmodtime & 0x07E0) >> 5; $v_seconde = ($lastmodtime & 0x001F)*2; - + // ----- Extract date $v_year = (($lastmoddate & 0xFE00) >> 9) + 1980; $v_month = ($lastmoddate & 0x01E0) >> 5; $v_day = $lastmoddate & 0x001F; - + // ----- Get UNIX date format $this->fileHeader->timestamp = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year); } - + $isBannedFile = false; $this->fileHeader->compressed = $headerData['compsize']; @@ -3360,12 +5065,12 @@ protected function readFileHeader() $isRenamed = true; } } - + // Handle directory renaming $isDirRenamed = false; if(is_array($this->renameDirs) && (count($this->renameDirs) > 0)) { - if(array_key_exists(dirname($file), $this->renameDirs)) { - $file = rtrim($this->renameDirs[dirname($file)],'/').'/'.basename($file); + if(array_key_exists(dirname($this->fileHeader->file), $this->renameDirs)) { + $file = rtrim($this->renameDirs[dirname($this->fileHeader->file)],'/').'/'.basename($this->fileHeader->file); $isRenamed = true; $isDirRenamed = true; } @@ -3375,11 +5080,9 @@ protected function readFileHeader() if($extraFieldLength > 0) { $extrafield = fread( $this->fp, $extraFieldLength ); } - - if(defined('KSDEBUG')) { - debugMsg( '*'.ftell($this->fp).' IS START OF '.$this->fileHeader->file. ' ('.$this->fileHeader->compressed.' bytes)' ); - } - + + debugMsg( '*'.ftell($this->fp).' IS START OF '.$this->fileHeader->file. ' ('.$this->fileHeader->compressed.' bytes)' ); + // Decide filetype -- Check for directories $this->fileHeader->type = 'file'; @@ -3424,230 +5127,71 @@ protected function readFileHeader() $curPos = @ftell($this->fp); $canSeek = $curSize - $curPos; if($canSeek > $seekleft) $canSeek = $seekleft; - @fseek( $this->fp, $canSeek, SEEK_CUR ); - $seekleft -= $canSeek; - if($seekleft) $this->nextFile(); - } - - $this->currentPartOffset = @ftell($this->fp); - $this->runState = AK_STATE_DONE; - return true; - } - - // Last chance to prepend a path to the filename - if(!empty($this->addPath) && !$isDirRenamed) - { - $this->fileHeader->file = $this->addPath.$this->fileHeader->file; - } - - // Get the translated path name - if($this->fileHeader->type == 'file') - { - $this->fileHeader->realFile = $this->postProcEngine->processFilename( $this->fileHeader->file ); - } - elseif($this->fileHeader->type == 'dir') - { - $this->fileHeader->timestamp = 0; - - $dir = $this->fileHeader->file; - - $this->postProcEngine->createDirRecursive( $this->fileHeader->file, 0755 ); - $this->postProcEngine->processFilename(null); - } - else - { - // Symlink; do not post-process - $this->fileHeader->timestamp = 0; - $this->postProcEngine->processFilename(null); - } - - $this->createDirectory(); - - // Header is read - $this->runState = AK_STATE_HEADER; - - return true; - } - -} - -/** - * Timer class - */ -class AKCoreTimer extends AKAbstractObject -{ - /** @var int Maximum execution time allowance per step */ - private $max_exec_time = null; - - /** @var int Timestamp of execution start */ - private $start_time = null; - - /** - * Public constructor, creates the timer object and calculates the execution time limits - * @return AECoreTimer - */ - public function __construct() - { - parent::__construct(); - - // Initialize start time - $this->start_time = $this->microtime_float(); - - // Get configured max time per step and bias - $config_max_exec_time = AKFactory::get('kickstart.tuning.max_exec_time', 14); - $bias = AKFactory::get('kickstart.tuning.run_time_bias', 75)/100; - - // Get PHP's maximum execution time (our upper limit) - if(@function_exists('ini_get')) - { - $php_max_exec_time = @ini_get("maximum_execution_time"); - if ( (!is_numeric($php_max_exec_time)) || ($php_max_exec_time == 0) ) { - // If we have no time limit, set a hard limit of about 10 seconds - // (safe for Apache and IIS timeouts, verbose enough for users) - $php_max_exec_time = 14; + @fseek( $this->fp, $canSeek, SEEK_CUR ); + $seekleft -= $canSeek; + if($seekleft) $this->nextFile(); } + + $this->currentPartOffset = @ftell($this->fp); + $this->runState = AK_STATE_DONE; + return true; } - else + + // Last chance to prepend a path to the filename + if(!empty($this->addPath) && !$isDirRenamed) { - // If ini_get is not available, use a rough default - $php_max_exec_time = 14; + $this->fileHeader->file = $this->addPath.$this->fileHeader->file; } - // Apply an arbitrary correction to counter CMS load time - $php_max_exec_time--; - - // Apply bias - $php_max_exec_time = $php_max_exec_time * $bias; - $config_max_exec_time = $config_max_exec_time * $bias; - - // Use the most appropriate time limit value - if( $config_max_exec_time > $php_max_exec_time ) + // Get the translated path name + if($this->fileHeader->type == 'file') { - $this->max_exec_time = $php_max_exec_time; + $this->fileHeader->realFile = $this->postProcEngine->processFilename( $this->fileHeader->file ); } - else + elseif($this->fileHeader->type == 'dir') { - $this->max_exec_time = $config_max_exec_time; - } - } - - /** - * Wake-up function to reset internal timer when we get unserialized - */ - public function __wakeup() - { - // Re-initialize start time on wake-up - $this->start_time = $this->microtime_float(); - } - - /** - * Gets the number of seconds left, before we hit the "must break" threshold - * @return float - */ - public function getTimeLeft() - { - return $this->max_exec_time - $this->getRunningTime(); - } - - /** - * Gets the time elapsed since object creation/unserialization, effectively how - * long Akeeba Engine has been processing data - * @return float - */ - public function getRunningTime() - { - return $this->microtime_float() - $this->start_time; - } + $this->fileHeader->timestamp = 0; - /** - * Returns the current timestampt in decimal seconds - */ - private function microtime_float() - { - list($usec, $sec) = explode(" ", microtime()); - return ((float)$usec + (float)$sec); - } + $dir = $this->fileHeader->file; - /** - * Enforce the minimum execution time - */ - public function enforce_min_exec_time() - { - // Try to get a sane value for PHP's maximum_execution_time INI parameter - if(@function_exists('ini_get')) - { - $php_max_exec = @ini_get("maximum_execution_time"); + $this->postProcEngine->createDirRecursive( $this->fileHeader->file, 0755 ); + $this->postProcEngine->processFilename(null); } else { - $php_max_exec = 10; - } - if ( ($php_max_exec == "") || ($php_max_exec == 0) ) { - $php_max_exec = 10; + // Symlink; do not post-process + $this->fileHeader->timestamp = 0; + $this->postProcEngine->processFilename(null); } - // Decrease $php_max_exec time by 500 msec we need (approx.) to tear down - // the application, as well as another 500msec added for rounding - // error purposes. Also make sure this is never gonna be less than 0. - $php_max_exec = max($php_max_exec * 1000 - 1000, 0); - - // Get the "minimum execution time per step" Akeeba Backup configuration variable - $minexectime = AKFactory::get('kickstart.tuning.min_exec_time',0); - if(!is_numeric($minexectime)) $minexectime = 0; - // Make sure we are not over PHP's time limit! - if($minexectime > $php_max_exec) $minexectime = $php_max_exec; + $this->createDirectory(); - // Get current running time - $elapsed_time = $this->getRunningTime() * 1000; + // Header is read + $this->runState = AK_STATE_HEADER; - // Only run a sleep delay if we haven't reached the minexectime execution time - if( ($minexectime > $elapsed_time) && ($elapsed_time > 0) ) - { - $sleep_msec = $minexectime - $elapsed_time; - if(function_exists('usleep')) - { - usleep(1000 * $sleep_msec); - } - elseif(function_exists('time_nanosleep')) - { - $sleep_sec = floor($sleep_msec / 1000); - $sleep_nsec = 1000000 * ($sleep_msec - ($sleep_sec * 1000)); - time_nanosleep($sleep_sec, $sleep_nsec); - } - elseif(function_exists('time_sleep_until')) - { - $until_timestamp = time() + $sleep_msec / 1000; - time_sleep_until($until_timestamp); - } - elseif(function_exists('sleep')) - { - $sleep_sec = ceil($sleep_msec/1000); - sleep( $sleep_sec ); - } - } - elseif( $elapsed_time > 0 ) - { - // No sleep required, even if user configured us to be able to do so. - } + return true; } - /** - * Reset the timer. It should only be used in CLI mode! - */ - public function resetTime() - { - $this->start_time = $this->microtime_float(); - } } +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + /** * JPS archive extraction class */ class AKUnarchiverJPS extends AKUnarchiverJPA { - private $archiveHeaderData = array(); + protected $archiveHeaderData = array(); - private $password = ''; + protected $password = ''; public function __construct() { @@ -3788,7 +5332,7 @@ protected function readFileHeader() $isRenamed = true; } } - + // Handle directory renaming $isDirRenamed = false; if(is_array($this->renameDirs) && (count($this->renameDirs) > 0)) { @@ -3870,7 +5414,7 @@ protected function readFileHeader() else { // Skip forward by the amount of compressed data - $miniHead = unpack('Vencsize/Vdecsize'); + $miniHead = unpack('Vencsize/Vdecsize', $binMiniHead); @fseek($this->fp, $miniHead['encsize'], SEEK_CUR); } } @@ -3978,7 +5522,7 @@ private function processTypeFileUncompressed() // Open the output file if( !AKFactory::get('kickstart.setup.dryrun','0') ) { - $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false); + $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); if ($this->dataReadLength == 0) { $outfp = @fopen( $this->fileHeader->realFile, 'wb' ); } else { @@ -4028,7 +5572,7 @@ private function processTypeFileCompressedSimple() $outfp = @fopen( $this->fileHeader->realFile, 'wb' ); // Can we write to the file? - $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false); + $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); if( ($outfp === false) && (!$ignore) ) { // An error occured $this->setError( AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile) ); @@ -4153,6 +5697,8 @@ private function processTypeFileCompressedSimple() $this->runState = AK_STATE_DATAREAD; $this->dataReadLength = 0; } + + return true; } /** @@ -4227,66 +5773,258 @@ private function processTypeLink() } } - // Decrypt the data - $data = AKEncryptionAES::AESDecryptCBC($data, $this->password, 128); - - // Is the length of the decrypted data less than expected? - $data_length = akstringlen($data); - if($data_length < $miniHeader['decsize']) { - $this->setError(AKText::_('ERR_INVALID_JPS_PASSWORD')); - return false; - } + // Decrypt the data + $data = AKEncryptionAES::AESDecryptCBC($data, $this->password, 128); + + // Is the length of the decrypted data less than expected? + $data_length = akstringlen($data); + if($data_length < $miniHeader['decsize']) { + $this->setError(AKText::_('ERR_INVALID_JPS_PASSWORD')); + return false; + } + + // Trim the data + $data = substr($data,0,$miniHeader['decsize']); + + // Try to remove an existing file or directory by the same name + if(file_exists($this->fileHeader->file)) { @unlink($this->fileHeader->file); @rmdir($this->fileHeader->file); } + // Remove any trailing slash + if(substr($this->fileHeader->file, -1) == '/') $this->fileHeader->file = substr($this->fileHeader->file, 0, -1); + // Create the symlink - only possible within PHP context. There's no support built in the FTP protocol, so no postproc use is possible here :( + + if( !AKFactory::get('kickstart.setup.dryrun','0') ) + { + @symlink($data, $this->fileHeader->file); + } + + $this->runState = AK_STATE_DATAREAD; + + return true; // No matter if the link was created! + } + + /** + * Process the file data of a directory entry + * @return bool + */ + private function processTypeDir() + { + // Directory entries in the JPA do not have file data, therefore we're done processing the entry + $this->runState = AK_STATE_DATAREAD; + return true; + } + + /** + * Creates the directory this file points to + */ + protected function createDirectory() + { + if( AKFactory::get('kickstart.setup.dryrun','0') ) return true; + + // Do we need to create a directory? + $lastSlash = strrpos($this->fileHeader->realFile, '/'); + $dirName = substr( $this->fileHeader->realFile, 0, $lastSlash); + $perms = $this->flagRestorePermissions ? $retArray['permissions'] : 0755; + $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($dirName); + if( ($this->postProcEngine->createDirRecursive($dirName, $perms) == false) && (!$ignore) ) { + $this->setError( AKText::sprintf('COULDNT_CREATE_DIR', $dirName) ); + return false; + } + else + { + return true; + } + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Timer class + */ +class AKCoreTimer extends AKAbstractObject +{ + /** @var int Maximum execution time allowance per step */ + private $max_exec_time = null; + + /** @var int Timestamp of execution start */ + private $start_time = null; + + /** + * Public constructor, creates the timer object and calculates the execution time limits + * @return AECoreTimer + */ + public function __construct() + { + parent::__construct(); + + // Initialize start time + $this->start_time = $this->microtime_float(); + + // Get configured max time per step and bias + $config_max_exec_time = AKFactory::get('kickstart.tuning.max_exec_time', 14); + $bias = AKFactory::get('kickstart.tuning.run_time_bias', 75)/100; + + // Get PHP's maximum execution time (our upper limit) + if(@function_exists('ini_get')) + { + $php_max_exec_time = @ini_get("maximum_execution_time"); + if ( (!is_numeric($php_max_exec_time)) || ($php_max_exec_time == 0) ) { + // If we have no time limit, set a hard limit of about 10 seconds + // (safe for Apache and IIS timeouts, verbose enough for users) + $php_max_exec_time = 14; + } + } + else + { + // If ini_get is not available, use a rough default + $php_max_exec_time = 14; + } + + // Apply an arbitrary correction to counter CMS load time + $php_max_exec_time--; - // Trim the data - $data = substr($data,0,$miniHeader['decsize']); + // Apply bias + $php_max_exec_time = $php_max_exec_time * $bias; + $config_max_exec_time = $config_max_exec_time * $bias; - // Try to remove an existing file or directory by the same name - if(file_exists($this->fileHeader->realFile)) { @unlink($this->fileHeader->realFile); @rmdir($this->fileHeader->realFile); } - // Remove any trailing slash - if(substr($this->fileHeader->realFile, -1) == '/') $this->fileHeader->realFile = substr($this->fileHeader->realFile, 0, -1); - // Create the symlink - only possible within PHP context. There's no support built in the FTP protocol, so no postproc use is possible here :( - if( !AKFactory::get('kickstart.setup.dryrun','0') ) - @symlink($data, $this->fileHeader->realFile); + // Use the most appropriate time limit value + if( $config_max_exec_time > $php_max_exec_time ) + { + $this->max_exec_time = $php_max_exec_time; + } + else + { + $this->max_exec_time = $config_max_exec_time; + } + } - $this->runState = AK_STATE_DATAREAD; + /** + * Wake-up function to reset internal timer when we get unserialized + */ + public function __wakeup() + { + // Re-initialize start time on wake-up + $this->start_time = $this->microtime_float(); + } - return true; // No matter if the link was created! + /** + * Gets the number of seconds left, before we hit the "must break" threshold + * @return float + */ + public function getTimeLeft() + { + return $this->max_exec_time - $this->getRunningTime(); } /** - * Process the file data of a directory entry - * @return bool + * Gets the time elapsed since object creation/unserialization, effectively how + * long Akeeba Engine has been processing data + * @return float */ - private function processTypeDir() + public function getRunningTime() { - // Directory entries in the JPA do not have file data, therefore we're done processing the entry - $this->runState = AK_STATE_DATAREAD; - return true; + return $this->microtime_float() - $this->start_time; } /** - * Creates the directory this file points to + * Returns the current timestampt in decimal seconds */ - protected function createDirectory() + private function microtime_float() { - if( AKFactory::get('kickstart.setup.dryrun','0') ) return true; + list($usec, $sec) = explode(" ", microtime()); + return ((float)$usec + (float)$sec); + } - // Do we need to create a directory? - $lastSlash = strrpos($this->fileHeader->realFile, '/'); - $dirName = substr( $this->fileHeader->realFile, 0, $lastSlash); - $perms = $this->flagRestorePermissions ? $retArray['permissions'] : 0755; - $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false); - if( ($this->postProcEngine->createDirRecursive($dirName, $perms) == false) && (!$ignore) ) { - $this->setError( AKText::sprintf('COULDNT_CREATE_DIR', $dirName) ); - return false; + /** + * Enforce the minimum execution time + */ + public function enforce_min_exec_time() + { + // Try to get a sane value for PHP's maximum_execution_time INI parameter + if(@function_exists('ini_get')) + { + $php_max_exec = @ini_get("maximum_execution_time"); } else { - return true; + $php_max_exec = 10; + } + if ( ($php_max_exec == "") || ($php_max_exec == 0) ) { + $php_max_exec = 10; + } + // Decrease $php_max_exec time by 500 msec we need (approx.) to tear down + // the application, as well as another 500msec added for rounding + // error purposes. Also make sure this is never gonna be less than 0. + $php_max_exec = max($php_max_exec * 1000 - 1000, 0); + + // Get the "minimum execution time per step" Akeeba Backup configuration variable + $minexectime = AKFactory::get('kickstart.tuning.min_exec_time',0); + if(!is_numeric($minexectime)) $minexectime = 0; + + // Make sure we are not over PHP's time limit! + if($minexectime > $php_max_exec) $minexectime = $php_max_exec; + + // Get current running time + $elapsed_time = $this->getRunningTime() * 1000; + + // Only run a sleep delay if we haven't reached the minexectime execution time + if( ($minexectime > $elapsed_time) && ($elapsed_time > 0) ) + { + $sleep_msec = $minexectime - $elapsed_time; + if(function_exists('usleep')) + { + usleep(1000 * $sleep_msec); + } + elseif(function_exists('time_nanosleep')) + { + $sleep_sec = floor($sleep_msec / 1000); + $sleep_nsec = 1000000 * ($sleep_msec - ($sleep_sec * 1000)); + time_nanosleep($sleep_sec, $sleep_nsec); + } + elseif(function_exists('time_sleep_until')) + { + $until_timestamp = time() + $sleep_msec / 1000; + time_sleep_until($until_timestamp); + } + elseif(function_exists('sleep')) + { + $sleep_sec = ceil($sleep_msec/1000); + sleep( $sleep_sec ); + } + } + elseif( $elapsed_time > 0 ) + { + // No sleep required, even if user configured us to be able to do so. } } + + /** + * Reset the timer. It should only be used in CLI mode! + */ + public function resetTime() + { + $this->start_time = $this->microtime_float(); + } } +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + /** * A filesystem scanner which uses opendir() */ @@ -4361,6 +6099,16 @@ public function &getFolders($folder, $pattern = '*') } } +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + /** * A simple INI-based i18n engine */ @@ -4383,6 +6131,7 @@ class AKText extends AKAbstractObject 'WRONG_FTP_PATH1' => 'Wrong FTP initial directory - the directory doesn\'t exist', 'FTP_CANT_CREATE_DIR' => 'Could not create directory %s', 'FTP_TEMPDIR_NOT_WRITABLE' => 'Could not find or create a writable temporary directory', + 'SFTP_TEMPDIR_NOT_WRITABLE' => 'Could not find or create a writable temporary directory', 'FTP_COULDNT_UPLOAD' => 'Could not upload %s', 'THINGS_HEADER' => 'Things you should know about Akeeba Kickstart', 'THINGS_01' => 'Kickstart is not an installer. It is an archive extraction tool. The actual installer was put inside the archive file at backup time.', @@ -4399,23 +6148,45 @@ class AKText extends AKAbstractObject 'ARCHIVE_FILE' => 'Archive file:', 'SELECT_EXTRACTION' => 'Select an extraction method', 'WRITE_TO_FILES' => 'Write to files:', + 'WRITE_HYBRID' => 'Hybrid (use FTP only if needed)', 'WRITE_DIRECTLY' => 'Directly', - 'WRITE_FTP' => 'Use FTP', - 'FTP_HOST' => 'FTP host name:', - 'FTP_PORT' => 'FTP port:', + 'WRITE_FTP' => 'Use FTP for all files', + 'WRITE_SFTP' => 'Use SFTP for all files', + 'FTP_HOST' => '(S)FTP host name:', + 'FTP_PORT' => '(S)FTP port:', 'FTP_FTPS' => 'Use FTP over SSL (FTPS)', 'FTP_PASSIVE' => 'Use FTP Passive Mode', - 'FTP_USER' => 'FTP user name:', - 'FTP_PASS' => 'FTP password:', - 'FTP_DIR' => 'FTP directory:', + 'FTP_USER' => '(S)FTP user name:', + 'FTP_PASS' => '(S)FTP password:', + 'FTP_DIR' => '(S)FTP directory:', 'FTP_TEMPDIR' => 'Temporary directory:', 'FTP_CONNECTION_OK' => 'FTP Connection Established', + 'SFTP_CONNECTION_OK' => 'SFTP Connection Established', 'FTP_CONNECTION_FAILURE' => 'The FTP Connection Failed', + 'SFTP_CONNECTION_FAILURE' => 'The SFTP Connection Failed', 'FTP_TEMPDIR_WRITABLE' => 'The temporary directory is writable.', 'FTP_TEMPDIR_UNWRITABLE' => 'The temporary directory is not writable. Please check the permissions.', + 'FTPBROWSER_ERROR_HOSTNAME' => "Invalid FTP host or port", + 'FTPBROWSER_ERROR_USERPASS' => "Invalid FTP username or password", + 'FTPBROWSER_ERROR_NOACCESS' => "Directory doesn't exist or you don't have enough permissions to access it", + 'FTPBROWSER_ERROR_UNSUPPORTED' => "Sorry, your FTP server doesn't support our FTP directory browser.", + 'FTPBROWSER_LBL_GOPARENT' => "<up one level>", + 'FTPBROWSER_LBL_INSTRUCTIONS' => 'Click on a directory to navigate into it. Click on OK to select that directory, Cancel to abort the procedure.', + 'FTPBROWSER_LBL_ERROR' => 'An error occurred', + 'SFTP_NO_SSH2' => 'Your web server does not have the SSH2 PHP module, therefore can not connect to SFTP servers.', + 'SFTP_NO_FTP_SUPPORT' => 'Your SSH server does not allow SFTP connections', + 'SFTP_WRONG_USER' => 'Wrong SFTP username or password', + 'SFTP_WRONG_STARTING_DIR' => 'You must supply a valid absolute path', + 'SFTPBROWSER_ERROR_NOACCESS' => "Directory doesn't exist or you don't have enough permissions to access it", + 'SFTP_COULDNT_UPLOAD' => 'Could not upload %s', + 'SFTP_CANT_CREATE_DIR' => 'Could not create directory %s', + 'UI-ROOT' => '<root>', + 'CONFIG_UI_FTPBROWSER_TITLE' => 'FTP Directory Browser', + 'FTP_BROWSE' => 'Browse', 'BTN_CHECK' => 'Check', 'BTN_RESET' => 'Reset', 'BTN_TESTFTPCON' => 'Test FTP connection', + 'BTN_TESTSFTPCON' => 'Test SFTP connection', 'BTN_GOTOSTART' => 'Start over', 'FINE_TUNE' => 'Fine tune', 'MIN_EXEC_TIME' => 'Minimum execution time:', @@ -4446,7 +6217,12 @@ class AKText extends AKAbstractObject 'UPDATE_HEADER' => 'An updated version of Akeeba Kickstart (unknown) is available!', 'UPDATE_NOTICE' => 'You are advised to always use the latest version of Akeeba Kickstart available. Older versions may be subject to bugs and will not be supported.', 'UPDATE_DLNOW' => 'Download now', - 'UPDATE_MOREINFO' => 'More information' + 'UPDATE_MOREINFO' => 'More information', + 'IGNORE_MOST_ERRORS' => 'Ignore most errors', + 'WRONG_FTP_PATH2' => 'Wrong FTP initial directory - the directory doesn\'t correspond to your site\'s web root', + 'ARCHIVE_DIRECTORY' => 'Archive directory:', + 'RELOAD_ARCHIVES' => 'Reload', + 'CONFIG_UI_SFTPBROWSER_TITLE' => 'SFTP Directory Browser', ); /** @@ -4481,7 +6257,7 @@ public function __construct() /** * Singleton pattern for Language - * @return Language The global Language instance + * @return AKText The global AKText instance */ public static function &getInstance() { @@ -4594,24 +6370,32 @@ public function getBrowserLanguage() $this->language = null; $basename=basename(__FILE__, '.php') . '.ini'; - + // Try to match main language part of the filename, irrespective of the location, e.g. de_DE will do if de_CH doesn't exist. - $fs = new AKUtilsLister(); - $iniFiles = $fs->getFiles( dirname(__FILE__), '*.'.$basename ); - if(empty($iniFiles) && ($basename != 'kickstart.ini')) { - $basename = 'kickstart.ini'; - $iniFiles = $fs->getFiles( dirname(__FILE__), '*.'.$basename ); + if (class_exists('AKUtilsLister')) + { + $fs = new AKUtilsLister(); + $iniFiles = $fs->getFiles(KSROOTDIR, '*.'.$basename ); + if(empty($iniFiles) && ($basename != 'kickstart.ini')) { + $basename = 'kickstart.ini'; + $iniFiles = $fs->getFiles(KSROOTDIR, '*.'.$basename ); + } + } + else + { + $iniFiles = null; } + if (is_array($iniFiles)) { foreach($user_languages as $languageStruct) { if(is_null($this->language)) { // Get files matching the main lang part - $iniFiles = $fs->getFiles( dirname(__FILE__), $languageStruct[1].'-??.'.$basename ); + $iniFiles = $fs->getFiles(KSROOTDIR, $languageStruct[1].'-??.'.$basename ); if (count($iniFiles) > 0) { $filename = $iniFiles[0]; - $filename = substr($filename, strlen(dirname(__FILE__))+1); + $filename = substr($filename, strlen(KSROOTDIR)+1); $this->language = substr($filename, 0, 5); } else { $this->language = null; @@ -4619,7 +6403,7 @@ public function getBrowserLanguage() } } } - + if(is_null($this->language)) { // Try to find a full language match foreach($user_languages as $languageStruct) @@ -4641,15 +6425,22 @@ public function getBrowserLanguage() } } } - + // Now, scan for full language based on the partial match - + } private function loadTranslation( $lang = null ) { - $dirname = function_exists('getcwd') ? getcwd() : dirname(__FILE__); - $basename=basename(__FILE__, '.php') . '.ini'; + if (defined('KSLANGDIR')) + { + $dirname = KSLANGDIR; + } + else + { + $dirname = KSROOTDIR; + } + $basename = basename(__FILE__, '.php') . '.ini'; if( empty($lang) ) $lang = $this->language; $translationFilename = $dirname.DIRECTORY_SEPARATOR.$lang.'.'.$basename; @@ -4668,6 +6459,14 @@ private function loadTranslation( $lang = null ) } } + public function addDefaultLanguageStrings($stringList = array()) + { + if(!is_array($stringList)) return; + if(empty($stringList)) return; + + $this->strings = array_merge($stringList, $this->strings); + } + /** * A PHP based INI file parser. * @@ -4765,6 +6564,16 @@ public static function parse_ini_file($file, $process_sections = false, $raw_dat } } +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + /** * The Akeeba Kickstart Factory class * This class is reponssible for instanciating all Akeeba Kicsktart classes @@ -4952,20 +6761,29 @@ public static function &getUnarchiver( $configOverride = null ) $destdir = self::get('kickstart.setup.destdir', null); if(empty($destdir)) { - $destdir = function_exists('getcwd') ? getcwd() : dirname(__FILE__); + $destdir = KSROOTDIR; } $object = self::getClassInstance($class_name); if( $object->getState() == 'init') { + $sourcePath = self::get('kickstart.setup.sourcepath', ''); + $sourceFile = self::get('kickstart.setup.sourcefile', ''); + + if (!empty($sourcePath)) + { + $sourceFile = rtrim($sourcePath, '/\\') . '/' . $sourceFile; + } + // Initialize the object $config = array( - 'filename' => self::get('kickstart.setup.sourcefile', ''), + 'filename' => $sourceFile, 'restore_permissions' => self::get('kickstart.setup.restoreperms', 0), 'post_proc' => self::get('kickstart.procengine', 'direct'), - 'add_path' => $destdir, - 'rename_files' => array( '.htaccess' => 'htaccess.bak', 'php.ini' => 'php.ini.bak' ), - 'skip_files' => array( basename(__FILE__), 'kickstart.php', 'abiautomation.ini', 'htaccess.bak', 'php.ini.bak' ) + 'add_path' => self::get('kickstart.setup.targetpath', $destdir), + 'rename_files' => array('.htaccess' => 'htaccess.bak', 'php.ini' => 'php.ini.bak', 'web.config' => 'web.config.bak'), + 'skip_files' => array(basename(__FILE__), 'kickstart.php', 'abiautomation.ini', 'htaccess.bak', 'php.ini.bak', 'cacert.pem'), + 'ignoredirectories' => array('tmp', 'log', 'logs'), ); if(!defined('KICKSTART')) @@ -5001,19 +6819,25 @@ public static function &getTimer() } /** - * AES implementation in PHP (c) Chris Veness 2005-2011 - * (http://www.movable-type.co.uk/scripts/aes-php.html) - * I offer these formulæ & scripts for free use and adaptation as my contribution to the - * open-source info-sphere from which I have received so much. You are welcome to re-use these - * scripts [under a simple attribution license or a GPL licence, without any warranty express or implied] - * provided solely that you retain my copyright notice and a link to this page. + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * AES implementation in PHP (c) Chris Veness 2005-2013. + * Right to use and adapt is granted for under a simple creative commons attribution * licence. No warranty of any form is offered. * * Modified for Akeeba Backup by Nicholas K. Dionysopoulos */ class AKEncryptionAES { - // Sbox is pre-computed multiplicative inverse in GF(2^8) used in SubBytes and KeyExpansion [�5.1.1] + // Sbox is pre-computed multiplicative inverse in GF(2^8) used in SubBytes and KeyExpansion [�5.1.1] protected static $Sbox = array(0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, @@ -5032,7 +6856,7 @@ class AKEncryptionAES 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16); - // Rcon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [�5.2] + // Rcon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [�5.2] protected static $Rcon = array( array(0x00, 0x00, 0x00, 0x00), array(0x01, 0x00, 0x00, 0x00), @@ -5056,11 +6880,11 @@ class AKEncryptionAES * generated from the cipher key by KeyExpansion() * @return ciphertext as byte-array (16 bytes) */ - protected static function Cipher($input, $w) { // main Cipher function [�5.1] + protected static function Cipher($input, $w) { // main Cipher function [�5.1] $Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES) $Nr = count($w)/$Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys - $state = array(); // initialise 4xNb byte-array 'state' with input [�3.4] + $state = array(); // initialise 4xNb byte-array 'state' with input [�3.4] for ($i=0; $i<4*$Nb; $i++) $state[$i%4][floor($i/4)] = $input[$i]; $state = self::AddRoundKey($state, $w, 0, $Nb); @@ -5076,26 +6900,26 @@ protected static function Cipher($input, $w) { // main Cipher function [ï¿ $state = self::ShiftRows($state, $Nb); $state = self::AddRoundKey($state, $w, $Nr, $Nb); - $output = array(4*$Nb); // convert state to 1-d array before returning [�3.4] + $output = array(4*$Nb); // convert state to 1-d array before returning [�3.4] for ($i=0; $i<4*$Nb; $i++) $output[$i] = $state[$i%4][floor($i/4)]; return $output; } - protected static function AddRoundKey($state, $w, $rnd, $Nb) { // xor Round Key into state S [�5.1.4] + protected static function AddRoundKey($state, $w, $rnd, $Nb) { // xor Round Key into state S [�5.1.4] for ($r=0; $r<4; $r++) { for ($c=0; $c<$Nb; $c++) $state[$r][$c] ^= $w[$rnd*4+$c][$r]; } return $state; } - protected static function SubBytes($s, $Nb) { // apply SBox to state S [�5.1.1] + protected static function SubBytes($s, $Nb) { // apply SBox to state S [�5.1.1] for ($r=0; $r<4; $r++) { for ($c=0; $c<$Nb; $c++) $s[$r][$c] = self::$Sbox[$s[$r][$c]]; } return $s; } - protected static function ShiftRows($s, $Nb) { // shift row r of state S left by r bytes [�5.1.2] + protected static function ShiftRows($s, $Nb) { // shift row r of state S left by r bytes [�5.1.2] $t = array(4); for ($r=1; $r<4; $r++) { for ($c=0; $c<4; $c++) $t[$c] = $s[$r][($c+$r)%$Nb]; // shift into temp copy @@ -5104,15 +6928,15 @@ protected static function ShiftRows($s, $Nb) { // shift row r of state S left return $s; // see fp.gladman.plus.com/cryptography_technology/rijndael/aes.spec.311.pdf } - protected static function MixColumns($s, $Nb) { // combine bytes of each col of state S [�5.1.3] + protected static function MixColumns($s, $Nb) { // combine bytes of each col of state S [�5.1.3] for ($c=0; $c<4; $c++) { $a = array(4); // 'a' is a copy of the current column from 's' - $b = array(4); // 'b' is a�{02} in GF(2^8) + $b = array(4); // 'b' is a�{02} in GF(2^8) for ($i=0; $i<4; $i++) { $a[$i] = $s[$i][$c]; $b[$i] = $s[$i][$c]&0x80 ? $s[$i][$c]<<1 ^ 0x011b : $s[$i][$c]<<1; } - // a[n] ^ b[n] is a�{03} in GF(2^8) + // a[n] ^ b[n] is a�{03} in GF(2^8) $s[0][$c] = $b[0] ^ $a[1] ^ $b[1] ^ $a[2] ^ $a[3]; // 2*a0 + 3*a1 + a2 + a3 $s[1][$c] = $a[0] ^ $b[1] ^ $a[2] ^ $b[2] ^ $a[3]; // a0 * 2*a1 + 3*a2 + a3 $s[2][$c] = $a[0] ^ $a[1] ^ $b[2] ^ $a[3] ^ $b[3]; // a0 + a1 + 2*a2 + 3*a3 @@ -5128,7 +6952,7 @@ protected static function MixColumns($s, $Nb) { // combine bytes of each col o * @param key cipher key byte-array (16 bytes) * @return key schedule as 2D byte-array (Nr+1 x Nb bytes) */ - protected static function KeyExpansion($key) { // generate Key Schedule from Cipher Key [�5.2] + protected static function KeyExpansion($key) { // generate Key Schedule from Cipher Key [�5.2] $Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES) $Nk = count($key)/4; // key length (in words): 4/6/8 for 128/192/256-bit keys $Nr = $Nk + 6; // no of rounds: 10/12/14 for 128/192/256-bit keys @@ -5209,7 +7033,7 @@ public static function AESEncryptCtr($plaintext, $password, $nBits) { $key = self::Cipher($pwBytes, self::KeyExpansion($pwBytes)); $key = array_merge($key, array_slice($key, 0, $nBytes-16)); // expand key to 16/24/32 bytes long - // initialise counter block (NIST SP800-38A �B.2): millisecond time-stamp for nonce in + // initialise counter block (NIST SP800-38A �B.2): millisecond time-stamp for nonce in // 1st 8 bytes, block counter in 2nd 8 bytes $counterBlock = array(); $nonce = floor(microtime(true)*1000); // timestamp: milliseconds since 1-Jan-1970 @@ -5387,8 +7211,19 @@ public static function AESDecryptCBC($ciphertext, $password, $nBits = 128) } /** - * The Master Setup will read the configuration parameters from restoration.php, abiautomation.ini, or + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * The Master Setup will read the configuration parameters from restoration.php or * the JSON-encoded "configuration" input variable and return the status. + * * @return bool True if the master configuration was applied to the Factory object */ function masterSetup() @@ -5400,26 +7235,29 @@ function masterSetup() $ini_data = null; // In restore.php mode, require restoration.php or fail - if(!defined('KICKSTART')) + if (!defined('KICKSTART')) { // This is the standalone mode, used by Akeeba Backup Professional. It looks for a restoration.php // file to perform its magic. If the file is not there, we will abort. $setupFile = 'restoration.php'; - if( !file_exists($setupFile) ) + if (!file_exists($setupFile)) { - // Uh oh... Somebody tried to pooh on our back yard. Lock the gates! Don't let the traitor inside! AKFactory::set('kickstart.enabled', false); + return false; } // Load restoration.php. It creates a global variable named $restoration_setup require_once $setupFile; + $ini_data = $restoration_setup; - if(empty($ini_data)) + + if (empty($ini_data)) { // No parameters fetched. Darn, how am I supposed to work like that?! AKFactory::set('kickstart.enabled', false); + return false; } @@ -5429,17 +7267,21 @@ function masterSetup() { // Maybe we have $restoration_setup defined in the head of kickstart.php global $restoration_setup; - if(!empty($restoration_setup) && !is_array($restoration_setup)) { + + if (!empty($restoration_setup) && !is_array($restoration_setup)) + { $ini_data = AKText::parse_ini_file($restoration_setup, false, true); - } elseif(is_array($restoration_setup)) { + } + elseif (is_array($restoration_setup)) + { $ini_data = $restoration_setup; } } // Import any data from $restoration_setup - if(!empty($ini_data)) + if (!empty($ini_data)) { - foreach($ini_data as $key => $value) + foreach ($ini_data as $key => $value) { AKFactory::set($key, $value); } @@ -5456,89 +7298,126 @@ function masterSetup() // Detect a JSON string in the request variable and store it. $json = getQueryParam('json', null); - // Remove everything from the request array - if(!empty($_REQUEST)) + // Remove everything from the request, post and get arrays + if (!empty($_REQUEST)) { - foreach($_REQUEST as $key => $value) + foreach ($_REQUEST as $key => $value) { unset($_REQUEST[$key]); } } + + if (!empty($_POST)) + { + foreach ($_POST as $key => $value) + { + unset($_POST[$key]); + } + } + + if (!empty($_GET)) + { + foreach ($_GET as $key => $value) + { + unset($_GET[$key]); + } + } + // Decrypt a possibly encrypted JSON string - if(!empty($json)) + $password = AKFactory::get('kickstart.security.password', null); + + if (!empty($json)) { - $password = AKFactory::get('kickstart.security.password', null); - if(!empty($password)) + if (!empty($password)) { $json = AKEncryptionAES::AESDecryptCtr($json, $password, 128); + + if (empty($json)) + { + die('###{"status":false,"message":"Invalid login"}###'); + } } // Get the raw data - $raw = json_decode( $json, true ); + $raw = json_decode($json, true); + + if (!empty($password) && (empty($raw))) + { + die('###{"status":false,"message":"Invalid login"}###'); + } + // Pass all JSON data to the request array - if(!empty($raw)) + if (!empty($raw)) { - foreach($raw as $key => $value) + foreach ($raw as $key => $value) { $_REQUEST[$key] = $value; } } } + elseif (!empty($password)) + { + die('###{"status":false,"message":"Invalid login"}###'); + } // ------------------------------------------------------------ // 3. Try the "factory" variable // ------------------------------------------------------------ // A "factory" variable will override all other settings. $serialized = getQueryParam('factory', null); - if( !is_null($serialized) ) + + if (!is_null($serialized)) { // Get the serialized factory AKFactory::unserialize($serialized); AKFactory::set('kickstart.enabled', true); + return true; } // ------------------------------------------------------------ - // 4. Try abiautomation.ini and the configuration variable for Kickstart + // 4. Try the configuration variable for Kickstart // ------------------------------------------------------------ - if(defined('KICKSTART')) + if (defined('KICKSTART')) { - // We are in Kickstart mode. abiautomation.ini has precedence. - $setupFile = 'abiautomation.ini'; - if( file_exists($setupFile) ) + $configuration = getQueryParam('configuration'); + + if (!is_null($configuration)) { - // abiautomation.ini was found - $ini_data = AKText::parse_ini_file('restoration.ini', false); + // Let's decode the configuration from JSON to array + $ini_data = json_decode($configuration, true); } else { - // abiautomation.ini was not found. Let's try input parameters. - $configuration = getQueryParam('configuration'); - if( !is_null($configuration) ) - { - // Let's decode the configuration from JSON to array - $ini_data = json_decode($configuration, true); - } - else - { - // Neither exists. Enable Kickstart's interface anyway. - $ini_data = array('kickstart.enabled'=>true); - } + // Neither exists. Enable Kickstart's interface anyway. + $ini_data = array('kickstart.enabled' => true); } // Import any INI data we might have from other sources - if(!empty($ini_data)) + if (!empty($ini_data)) { - foreach($ini_data as $key => $value) + foreach ($ini_data as $key => $value) { AKFactory::set($key, $value); } + AKFactory::set('kickstart.enabled', true); + return true; } } } +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright 2010-2014 Nicholas K. Dionysopoulos / Akeeba Ltd. + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + // Mini-controller for restore.php if(!defined('KICKSTART')) { @@ -5646,11 +7525,33 @@ public function __toString() $postproc->rename( $root.'/htaccess.bak', $root.'/.htaccess' ); } + // Rename htaccess.bak to .htaccess + if(file_exists($root.'/web.config.bak')) + { + if( file_exists($root.'/web.config') ) + { + $postproc->unlink($root.'/web.config'); + } + $postproc->rename( $root.'/web.config.bak', $root.'/web.config' ); + } + // Remove restoration.php - $basepath = dirname(__FILE__); + $basepath = KSROOTDIR; $basepath = rtrim( str_replace('\\','/',$basepath), '/' ); if(!empty($basepath)) $basepath .= '/'; $postproc->unlink( $basepath.'restoration.php' ); + + // Import a custom finalisation file + if (file_exists(dirname(__FILE__) . '/restore_finalisation.php')) + { + include_once dirname(__FILE__) . '/restore_finalisation.php'; + } + + // Run a custom finalisation script + if (function_exists('finalizeRestore')) + { + finalizeRestore($root, $basepath); + } break; default: @@ -5743,4 +7644,3 @@ function recursive_remove_directory($directory) return TRUE; } } -?> \ No newline at end of file diff --git a/administrator/components/com_media/models/manager.php b/administrator/components/com_media/models/manager.php index 679f54f21687e..b99f2b2bd9b15 100644 --- a/administrator/components/com_media/models/manager.php +++ b/administrator/components/com_media/models/manager.php @@ -90,7 +90,7 @@ function getFolderList($base = null) // so both string and integer are supported. if ($asset == 0) { - $asset = $input->get('asset', 0, 'string'); + $asset = $input->get('asset', 0, 'cmd'); } $author = $input->get('author', 0, 'integer'); diff --git a/administrator/components/com_modules/views/modules/tmpl/default.php b/administrator/components/com_modules/views/modules/tmpl/default.php index 251cb6da206d5..cb58c064c9ed6 100644 --- a/administrator/components/com_modules/views/modules/tmpl/default.php +++ b/administrator/components/com_modules/views/modules/tmpl/default.php @@ -159,7 +159,7 @@
- published, $i, $canChange, 'cb'); ?> + published, $i, 'modules.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> " />
- \ No newline at end of file + diff --git a/administrator/components/com_newsfeeds/tables/newsfeed.php b/administrator/components/com_newsfeeds/tables/newsfeed.php index e6c5515581732..bede2c2139501 100644 --- a/administrator/components/com_newsfeeds/tables/newsfeed.php +++ b/administrator/components/com_newsfeeds/tables/newsfeed.php @@ -21,7 +21,7 @@ class NewsfeedsTableNewsfeed extends JTable * @var array * @since 3.3 */ - protected $jsonEncode = array('params', 'metadata', 'images'); + protected $_jsonEncode = array('params', 'metadata', 'images'); /** * Constructor diff --git a/administrator/components/com_plugins/views/plugins/tmpl/default.php b/administrator/components/com_plugins/views/plugins/tmpl/default.php index eef9d0f6d8dd9..cadba65af4ac4 100644 --- a/administrator/components/com_plugins/views/plugins/tmpl/default.php +++ b/administrator/components/com_plugins/views/plugins/tmpl/default.php @@ -47,7 +47,7 @@
sidebar)) : ?> -