芝麻web文件管理V1.00
编辑当前文件:/home2/sdektunc/.trash/administrator/components/com_joomlaupdate/restore.php
|'), array('*' => '.*', '?' => '.?')) . '$/i', $string ); } } // Unicode-safe binary data length function if (!function_exists('akstringlen')) { if (function_exists('mb_strlen')) { function akstringlen($string) { return mb_strlen($string, '8bit'); } } else { function akstringlen($string) { return strlen($string); } } } if (!function_exists('aksubstr')) { if (function_exists('mb_strlen')) { function aksubstr($string, $start, $length = null) { return mb_substr($string, $start, $length, '8bit'); } } else { function aksubstr($string, $start, $length = null) { return substr($string, $start, $length); } } } /** * Gets a query parameter from GET or POST data * * @param $key * @param $default */ function getQueryParam($key, $default = null) { $value = $default; if (array_key_exists($key, $_REQUEST)) { $value = $_REQUEST[$key]; } return $value; } // Debugging function function debugMsg($msg) { if (!defined('KSDEBUG')) { return; } $fp = fopen('debug.txt', 'at'); fwrite($fp, $msg . "\n"); fclose($fp); // Echo to stdout if KSDEBUGCLI is defined if (defined('KSDEBUGCLI')) { echo $msg . "\n"; } } /** * The base class of Akeeba Engine objects. Allows for error and warnings logging * and propagation. Largely based on the Joomla! 1.5 JObject class. */ abstract class AKAbstractObject { /** @var array The queue size of the $_errors array. Set to 0 for infinite size. */ protected $_errors_queue_size = 0; /** @var array The queue size of the $_warnings array. Set to 0 for infinite size. */ protected $_warnings_queue_size = 0; /** @var array An array of errors */ private $_errors = array(); /** @var array An array of warnings */ private $_warnings = array(); /** * Get the most recent error message * * @param integer $i Optional error index * * @return string Error message */ public function getError($i = null) { return $this->getItemFromArray($this->_errors, $i); } /** * Returns the last item of a LIFO string message queue, or a specific item * if so specified. * * @param array $array An array of strings, holding messages * @param int $i Optional message index * * @return mixed The message string, or false if the key doesn't exist */ private function getItemFromArray($array, $i = null) { // Find the item if ($i === null) { // Default, return the last item $item = end($array); } else if (!array_key_exists($i, $array)) { // If $i has been specified but does not exist, return false return false; } else { $item = $array[$i]; } return $item; } /** * Return all errors, if any * * @return array Array of error messages */ public function getErrors() { return $this->_errors; } /** * Resets all error messages */ public function resetErrors() { $this->_errors = array(); } /** * Get the most recent warning message * * @param integer $i Optional warning index * * @return string Error message */ public function getWarning($i = null) { return $this->getItemFromArray($this->_warnings, $i); } /** * Return all warnings, if any * * @return array Array of error messages */ public function getWarnings() { return $this->_warnings; } /** * Resets all warning messages */ public function resetWarnings() { $this->_warnings = array(); } /** * Propagates errors and warnings to a foreign object. The foreign object SHOULD * implement the setError() and/or setWarning() methods but DOESN'T HAVE TO be of * AKAbstractObject type. For example, this can even be used to propagate to a * JObject instance in Joomla!. Propagated items will be removed from ourselves. * * @param object $object The object to propagate errors and warnings to. */ public function propagateToObject(&$object) { // Skip non-objects if (!is_object($object)) { return; } if (method_exists($object, 'setError')) { if (!empty($this->_errors)) { foreach ($this->_errors as $error) { $object->setError($error); } $this->_errors = array(); } } if (method_exists($object, 'setWarning')) { if (!empty($this->_warnings)) { foreach ($this->_warnings as $warning) { $object->setWarning($warning); } $this->_warnings = array(); } } } /** * Propagates errors and warnings from a foreign object. Each propagated list is * then cleared on the foreign object, as long as it implements resetErrors() and/or * resetWarnings() methods. * * @param object $object The object to propagate errors and warnings from */ public function propagateFromObject(&$object) { if (method_exists($object, 'getErrors')) { $errors = $object->getErrors(); if (!empty($errors)) { foreach ($errors as $error) { $this->setError($error); } } if (method_exists($object, 'resetErrors')) { $object->resetErrors(); } } if (method_exists($object, 'getWarnings')) { $warnings = $object->getWarnings(); if (!empty($warnings)) { foreach ($warnings as $warning) { $this->setWarning($warning); } } if (method_exists($object, 'resetWarnings')) { $object->resetWarnings(); } } } /** * Add an error message * * @param string $error Error message */ public function setError($error) { if ($this->_errors_queue_size > 0) { if (count($this->_errors) >= $this->_errors_queue_size) { array_shift($this->_errors); } } $this->_errors[] = $error; } /** * Add an error message * * @param string $error Error message */ public function setWarning($warning) { if ($this->_warnings_queue_size > 0) { if (count($this->_warnings) >= $this->_warnings_queue_size) { array_shift($this->_warnings); } } $this->_warnings[] = $warning; } /** * Sets the size of the error queue (acts like a LIFO buffer) * * @param int $newSize The new queue size. Set to 0 for infinite length. */ protected function setErrorsQueueSize($newSize = 0) { $this->_errors_queue_size = (int) $newSize; } /** * Sets the size of the warnings queue (acts like a LIFO buffer) * * @param int $newSize The new queue size. Set to 0 for infinite length. */ protected function setWarningsQueueSize($newSize = 0) { $this->_warnings_queue_size = (int) $newSize; } } /** * 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 */ protected $isPrepared = false; /** * Indicates whether this part has more work to do (it's in running state) * * @var boolean */ protected $isRunning = false; /** * Indicates whether this part has finished its finalization cycle * * @var boolean */ protected $isFinished = false; /** * Indicates whether this part has finished its run cycle * * @var boolean */ protected $hasRan = false; /** * The name of the engine part (a.k.a. Domain), used in return table * generation. * * @var string */ protected $active_domain = ""; /** * 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 = ""; /** * 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 = ""; /** * Any configuration variables, in the form of an array. * * @var array */ protected $_parametersArray = array(); /** @var string The database root key */ protected $databaseRoot = array(); /** @var array An array of observers */ protected $observers = array(); /** @var int Last reported warnings's position in array */ private $warnings_pointer = -1; /** * 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 response array. * * @return array A Response 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": case "running": $this->_run(); break; case "postrun": $this->_finalize(); break; } // Send a Return Table back to the caller $out = $this->_makeReturnTable(); return $out; } /** * 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"; } if (!($this->isPrepared)) { return "init"; } if (!($this->isFinished) && !($this->isRunning) && !($this->hasRun) && ($this->isPrepared)) { return "prepared"; } if (!($this->isFinished) && $this->isRunning && !($this->hasRun)) { return "running"; } if (!($this->isFinished) && !($this->isRunning) && $this->hasRun) { return "postrun"; } if ($this->isFinished) { return "finished"; } } /** * Runs the preparation for this part. Should set _isPrepared * to true */ abstract protected function _prepare(); /** * 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(); /** * Runs the finalisation process for this part. Should set * _isFinished to true. */ abstract protected function _finalize(); /** * Constructs a Response Array based on the engine part's state. * * @return array The Response Array for the current state */ final protected function _makeReturnTable() { // 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) { 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); } } $out = array( 'HasRun' => (!($this->isFinished)), 'Domain' => $this->active_domain, 'Step' => $this->active_step, 'Substep' => $this->active_substep, 'Error' => $this->getError(), 'Warnings' => $warnings ); 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', "Can't modify configuration after the preparation of " . $this->active_domain); } else { $this->_parametersArray = $parametersArray; if (array_key_exists('root', $parametersArray)) { $this->databaseRoot = $parametersArray['root']; } } } /** * 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; case 'prepared': $this->isPrepared = true; $this->isRunning = false; $this->isFinished = false; $this->hasRun = false; break; case 'running': $this->isPrepared = true; $this->isRunning = true; $this->isFinished = false; $this->hasRun = false; break; case 'postrun': $this->isPrepared = true; $this->isRunning = false; $this->isFinished = false; $this->hasRun = true; break; case 'finished': $this->isPrepared = true; $this->isRunning = false; $this->isFinished = true; $this->hasRun = false; break; case 'error': default: $this->setError($errorMessage); break; } } final public function getDomain() { return $this->active_domain; } final public function getStep() { return $this->active_step; } final public function getSubstep() { return $this->active_substep; } /** * Attaches an observer object * * @param AKAbstractPartObserver $obs */ function attach(AKAbstractPartObserver $obs) { $this->observers["$obs"] = $obs; } /** * Detaches an observer object * * @param AKAbstractPartObserver $obs */ function detach(AKAbstractPartObserver $obs) { delete($this->observers["$obs"]); } /** * 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); } final protected function setDomain($new_domain) { $this->active_domain = $new_domain; } final protected function setStep($new_step) { $this->active_step = $new_step; } final protected function setSubstep($new_substep) { $this->active_substep = $new_substep; } /** * 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); } } } /** * The base class of unarchiver classes */ abstract class AKAbstractUnarchiver extends AKAbstractPart { /** @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 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 string Archive filename */ protected $filename = null; /** @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 string Absolute path to remove from extracted files */ protected $removePath = ''; /** @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(); /** * Public constructor */ public function __construct() { parent::__construct(); } /** * Wakeup function, called whenever the class is unserialized */ public function __wakeup() { if ($this->currentPartNumber >= 0 && !empty($this->archiveList[$this->currentPartNumber])) { $this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb'); if ((is_resource($this->fp)) && ($this->currentPartOffset > 0)) { @fseek($this->fp, $this->currentPartOffset); } } } /** * Sleep function, called whenever the class is serialized */ public function shutdown() { if (is_resource($this->fp)) { $this->currentPartOffset = @ftell($this->fp); @fclose($this->fp); } } /** * 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 */ public function isIgnoredDirectory($shortFilename) { // return false; if (substr($shortFilename, -1) == '/') { $check = rtrim($shortFilename, '/'); } else { $check = dirname($shortFilename); } return in_array($check, $this->ignoreDirectories); } /** * Implements the abstract _prepare() method */ final protected function _prepare() { parent::__construct(); if (count($this->_parametersArray) > 0) { 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; // Path to remove from the beginning case 'remove_path': $this->removePath = $value; $this->removePath = str_replace('\\', '/', $this->removePath); $this->removePath = rtrim($this->removePath, '/'); if (!empty($this->removePath)) { $this->removePath .= '/'; } 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; } } } $this->scanArchives(); $this->readArchiveHeader(); $errMessage = $this->getError(); if (!empty($errMessage)) { $this->setState('error', $errMessage); } else { $this->runState = AK_STATE_NOFILE; $this->setState('prepared'); } } /** * 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); } /** * 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; } /** * Concrete classes are supposed to use this method in order to read the archive's header and * prepare themselves to the point of being ready to extract the first file. */ protected abstract function readArchiveHeader(); protected function _run() { 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) { // Send start of file notification $message = new stdClass; $message->type = 'startfile'; $message->content = new stdClass; $message->content->realfile = $this->fileHeader->file; $message->content->file = $this->fileHeader->file; $message->content->uncompressed = $this->fileHeader->uncompressed; if (array_key_exists('realfile', get_object_vars($this->fileHeader))) { $message->content->realfile = $this->fileHeader->realFile; } if (array_key_exists('compressed', get_object_vars($this->fileHeader))) { $message->content->compressed = $this->fileHeader->compressed; } else { $message->content->compressed = 0; } debugMsg(__CLASS__ . '::_run() - Preparing to extract ' . $message->content->realfile); $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; break; } } $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); } } /** * Concrete classes must use this method to read the file header * * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive */ protected abstract function readFileHeader(); /** * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when * it's finished processing the file data. * * @return bool True if processing the file data was successful, false if an error occurred */ protected abstract function processFileData(); protected function _finalize() { // Nothing to do $this->setState('finished'); } /** * 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'); $this->setError(AKText::sprintf('ERR_COULD_NOT_OPEN_ARCHIVE_PART', $this->archiveList[$this->currentPartNumber])); } 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_writable($directory)) { $this->postProcEngine->chmod($directory, 0755); } } $this->postProcEngine->chmod($path, 0644); } /** * Reads data from the archive and notifies the observer with the 'reading' message * * @param $fp * @param $length */ protected function fread($fp, $length = null) { if (is_numeric($length)) { if ($length > 0) { $data = fread($fp, $length); } else { $data = fread($fp, PHP_INT_MAX); } } else { $data = fread($fp, PHP_INT_MAX); } if ($data === false) { $data = ''; } // Send start of file notification $message = new stdClass; $message->type = 'reading'; $message->content = new stdClass; $message->content->length = strlen($data); $this->notify($message); return $data; } /** * Removes the configured $removePath from the path $path * * @param string $path The path to reduce * * @return string The reduced path */ protected function removePath($path) { if (empty($this->removePath)) { return $path; } if (strpos($path, $this->removePath) === 0) { $path = substr($path, strlen($this->removePath)); $path = ltrim($path, '/\\'); } return $path; } } /** * File post processor engines base class */ abstract class AKAbstractPostproc extends AKAbstractObject { /** @var int The UNIX timestamp of the file's desired modification date */ public $timestamp = 0; /** @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; /** * Processes the current file, e.g. moves it from temp to final location by FTP */ abstract public function process(); /** * 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 */ abstract public function processFilename($filename, $perms = 0755); /** * 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 */ 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); } /** * 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); } /** * 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)) { $this->clearFileInOPCache($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) { $this->clearFileInOPCache($file); return @unlink($file); } public function rmdir($directory) { return @rmdir($directory); } public function rename($from, $to) { $this->clearFileInOPCache($from); $ret = @rename($from, $to); $this->clearFileInOPCache($to); return $ret; } public function clearFileInOPCache($file){ if (ini_get('opcache.enable') && function_exists('opcache_invalidate') && (!ini_get('opcache.restrict_api') || stripos(realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')) === 0)) { \opcache_invalidate($file, true); } } } /** * JPA archive extraction class */ class AKUnarchiverJPA extends AKAbstractUnarchiver { 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) { debugMsg('Could not open the first part'); return false; } // Read the signature $sig = fread($this->fp, 3); if ($sig != 'JPA') { // Not a JPA file debugMsg('Invalid archive signature'); $this->setError(AKText::_('ERR_NOT_A_JPA_FILE')); return false; } // Read and parse header length $header_length_array = unpack('v', fread($this->fp, 2)); $header_length = $header_length_array[1]; // Read and parse the known portion of header data (14 bytes) $bin_data = fread($this->fp, 14); $header_data = unpack('Cmajor/Cminor/Vcount/Vuncsize/Vcsize', $bin_data); // Load any remaining header data (forward compatibility) $rest_length = $header_length - 19; if ($rest_length > 0) { $junk = fread($this->fp, $rest_length); } else { $junk = ''; } // Temporary array with all the data we read $temp = array( 'signature' => $sig, 'length' => $header_length, 'major' => $header_data['major'], 'minor' => $header_data['minor'], 'filecount' => $header_data['count'], 'uncompressedsize' => $header_data['uncsize'], 'compressedsize' => $header_data['csize'], 'unknowndata' => $junk ); // Array-to-object conversion foreach ($temp as $key => $value) { $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; return true; } /** * Concrete classes must use this method to read the file header * * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive */ 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(); } $this->currentPartOffset = ftell($this->fp); debugMsg("Reading file signature; part $this->currentPartNumber, offset $this->currentPartOffset"); // Get and decode Entity Description Block $signature = fread($this->fp, 3); $this->fileHeader = new stdClass(); $this->fileHeader->timestamp = 0; // Check signature if ($signature != 'JPF') { if ($this->isEOF(true)) { // This file is finished; make sure it's the last one $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; } // We're just finished return false; } else { $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. $isBannedFile = false; // Read length of EDB and of the Entity Path Data $length_array = unpack('vblocksize/vpathsize', fread($this->fp, 4)); // Read the path data if ($length_array['pathsize'] > 0) { $file = fread($this->fp, $length_array['pathsize']); } else { $file = ''; } // Handle file renaming $isRenamed = false; if (is_array($this->renameFiles) && (count($this->renameFiles) > 0)) { if (array_key_exists($file, $this->renameFiles)) { $file = $this->renameFiles[$file]; $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); $isRenamed = true; $isDirRenamed = true; } } // Read and parse the known data portion $bin_data = fread($this->fp, 14); $header_data = unpack('Ctype/Ccompression/Vcompsize/Vuncompsize/Vperms', $bin_data); // Read any unknown data $restBytes = $length_array['blocksize'] - (21 + $length_array['pathsize']); if ($restBytes > 0) { // Start reading the extra fields while ($restBytes >= 4) { $extra_header_data = fread($this->fp, 4); $extra_header = unpack('vsignature/vlength', $extra_header_data); $restBytes -= 4; $extra_header['length'] -= 4; switch ($extra_header['signature']) { case 256: // File modified timestamp if ($extra_header['length'] > 0) { $bindata = fread($this->fp, $extra_header['length']); $restBytes -= $extra_header['length']; $timestamps = unpack('Vmodified', substr($bindata, 0, 4)); $filectime = $timestamps['modified']; $this->fileHeader->timestamp = $filectime; } break; default: // Unknown field if ($extra_header['length'] > 0) { $junk = fread($this->fp, $extra_header['length']); $restBytes -= $extra_header['length']; } break; } } if ($restBytes > 0) { $junk = fread($this->fp, $restBytes); } } $compressionType = $header_data['compression']; // Populate the return array $this->fileHeader->file = $file; $this->fileHeader->compressed = $header_data['compsize']; $this->fileHeader->uncompressed = $header_data['uncompsize']; switch ($header_data['type']) { case 0: $this->fileHeader->type = 'dir'; break; case 1: $this->fileHeader->type = 'file'; break; case 2: $this->fileHeader->type = 'link'; break; } switch ($compressionType) { case 0: $this->fileHeader->compression = 'none'; break; case 1: $this->fileHeader->compression = 'gzip'; break; case 2: $this->fileHeader->compression = 'bzip2'; break; } $this->fileHeader->permissions = $header_data['perms']; // Find hard-coded banned files if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) { $isBannedFile = true; } // Also try to find banned files passed in class configuration if ((count($this->skipFiles) > 0) && (!$isRenamed)) { if (in_array($this->fileHeader->file, $this->skipFiles)) { $isBannedFile = true; } } // 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) { // Ensure that we can seek past archive part boundaries $curSize = @filesize($this->archiveList[$this->currentPartNumber]); $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; } // Remove the removePath, if any $this->fileHeader->file = $this->removePath($this->fileHeader->file); // 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 $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); if ($this->fileHeader->type == 'file') { // Regular file; ask the postproc engine to process its filename if ($restorePerms) { $this->fileHeader->realFile = $this->postProcEngine->processFilename($this->fileHeader->file, $this->fileHeader->permissions); } else { $this->fileHeader->realFile = $this->postProcEngine->processFilename($this->fileHeader->file); } } elseif ($this->fileHeader->type == 'dir') { $dir = $this->fileHeader->file; // Directory; just create it if ($restorePerms) { $this->postProcEngine->createDirRecursive($this->fileHeader->file, $this->fileHeader->permissions); } else { $this->postProcEngine->createDirRecursive($this->fileHeader->file, 0755); } $this->postProcEngine->processFilename(null); } else { // Symlink; do not post-process $this->postProcEngine->processFilename(null); } $this->createDirectory(); // Header is read $this->runState = AK_STATE_HEADER; $this->dataReadLength = 0; 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; } /** * 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? 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 ? $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; } else { return true; } } /** * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when * it's finished processing the file data. * * @return bool True if processing the file data was successful, false if an error occurred */ protected function processFileData() { switch ($this->fileHeader->type) { case 'dir': return $this->processTypeDir(); break; case 'link': return $this->processTypeLink(); break; case 'file': switch ($this->fileHeader->compression) { case 'none': return $this->processTypeFileUncompressed(); break; case 'gzip': case 'bzip2': return $this->processTypeFileCompressedSimple(); break; } break; default: debugMsg('Unknown file type ' . $this->fileHeader->type); break; } } /** * 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; } /** * Process the file data of a link entry * * @return bool */ private function processTypeLink() { $readBytes = 0; $toReadBytes = 0; $leftBytes = $this->fileHeader->compressed; $data = ''; while ($leftBytes > 0) { $toReadBytes = ($leftBytes > $this->chunkSize) ? $this->chunkSize : $leftBytes; $mydata = $this->fread($this->fp, $toReadBytes); $reallyReadBytes = akstringlen($mydata); $data .= $mydata; $leftBytes -= $reallyReadBytes; if ($reallyReadBytes < $toReadBytes) { // We read less than requested! Why? Did we hit local EOF? if ($this->isEOF(true) && !$this->isEOF(false)) { // Yeap. Let's go to the next file $this->nextFile(); } 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; } } } $filename = $this->fileHeader->realFile ?? $this->fileHeader->file; if (!AKFactory::get('kickstart.setup.dryrun', '0')) { // Try to remove an existing file or directory by the same name if (file_exists($filename)) { @unlink($filename); @rmdir($filename); } // Remove any trailing slash if (substr($filename, -1) == '/') { $filename = substr($filename, 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 :( @symlink($data, $filename); } $this->runState = AK_STATE_DATAREAD; return true; // No matter if the link was created! } private function processTypeFileUncompressed() { // Uncompressed files are being processed in small chunks, to avoid timeouts if (($this->dataReadLength == 0) && !AKFactory::get('kickstart.setup.dryrun', '0')) { // Before processing file data, ensure permissions are adequate $this->setCorrectPermissions($this->fileHeader->file); } // Open the output file if (!AKFactory::get('kickstart.setup.dryrun', '0')) { $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); if ($this->dataReadLength == 0) { $outfp = @fopen($this->fileHeader->realFile, 'wb'); } else { $outfp = @fopen($this->fileHeader->realFile, 'ab'); } // Can we write to the file? if (($outfp === false) && (!$ignore)) { // An error occurred debugMsg('Could not write to output file'); $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); return false; } } // Does the file have any data, at all? if ($this->fileHeader->compressed == 0) { // No file data! if (!AKFactory::get('kickstart.setup.dryrun', '0') && is_resource($outfp)) { @fclose($outfp); } $this->runState = AK_STATE_DATAREAD; return true; } // Reference to the global timer $timer = AKFactory::getTimer(); $toReadBytes = 0; $leftBytes = $this->fileHeader->compressed - $this->dataReadLength; // Loop while there's data to read and enough time to do it while (($leftBytes > 0) && ($timer->getTimeLeft() > 0)) { $toReadBytes = ($leftBytes > $this->chunkSize) ? $this->chunkSize : $leftBytes; $data = $this->fread($this->fp, $toReadBytes); $reallyReadBytes = akstringlen($data); $leftBytes -= $reallyReadBytes; $this->dataReadLength += $reallyReadBytes; if ($reallyReadBytes < $toReadBytes) { // We read less than requested! Why? Did we hit local EOF? if ($this->isEOF(true) && !$this->isEOF(false)) { // Yeap. Let's go to the next file $this->nextFile(); } 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; } } if (!AKFactory::get('kickstart.setup.dryrun', '0')) { if (is_resource($outfp)) { @fwrite($outfp, $data); } } } // Close the file pointer if (!AKFactory::get('kickstart.setup.dryrun', '0')) { if (is_resource($outfp)) { @fclose($outfp); } } // Was this a pre-timeout bail out? if ($leftBytes > 0) { $this->runState = AK_STATE_DATA; } else { // Oh! We just finished! $this->runState = AK_STATE_DATAREAD; $this->dataReadLength = 0; } return true; } private function processTypeFileCompressedSimple() { if (!AKFactory::get('kickstart.setup.dryrun', '0')) { // Before processing file data, ensure permissions are adequate $this->setCorrectPermissions($this->fileHeader->file); // Open the output file $outfp = @fopen($this->fileHeader->realFile, 'wb'); // Can we write to the file? $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); if (($outfp === false) && (!$ignore)) { // An error occurred debugMsg('Could not write to output file'); $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); return false; } } // Does the file have any data, at all? if ($this->fileHeader->compressed == 0) { // No file data! if (!AKFactory::get('kickstart.setup.dryrun', '0')) { if (is_resource($outfp)) { @fclose($outfp); } } $this->runState = AK_STATE_DATAREAD; return true; } // Simple compressed files are processed as a whole; we can't do chunk processing $zipData = $this->fread($this->fp, $this->fileHeader->compressed); while (akstringlen($zipData) < $this->fileHeader->compressed) { // End of local file before reading all data, but have more archive parts? if ($this->isEOF(true) && !$this->isEOF(false)) { // Yeap. Read from the next file $this->nextFile(); $bytes_left = $this->fileHeader->compressed - akstringlen($zipData); $zipData .= $this->fread($this->fp, $bytes_left); } 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; } } if ($this->fileHeader->compression == 'gzip') { $unzipData = gzinflate($zipData); } elseif ($this->fileHeader->compression == 'bzip2') { $unzipData = bzdecompress($zipData); } unset($zipData); // Write to the file. if (!AKFactory::get('kickstart.setup.dryrun', '0') && is_resource($outfp)) { @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed); @fclose($outfp); } unset($unzipData); $this->runState = AK_STATE_DATAREAD; return true; } } /** * ZIP archive extraction class * * Since the file data portion of ZIP and JPA are similarly structured (it's empty for dirs, * linked node name for symlinks, dumped binary data for no compressions and dumped gzipped * binary data for gzip compression) we just have to subclass AKUnarchiverJPA and change the * header reading bits. Reusable code ;) */ class AKUnarchiverZIP extends AKUnarchiverJPA { var $expectDataDescriptor = false; 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) { 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) { debugMsg('The archive is not multipart'); fseek($this->fp, -4, SEEK_CUR); } else { debugMsg('The archive is multipart'); } $multiPartSigs = array( 0x08074b50, // Multi-part ZIP 0x30304b50, // Multi-part ZIP (alternate) 0x04034b50 // Single file ); 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; return true; } /** * Concrete classes must use this method to read the file header * * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive */ 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(); } $this->currentPartOffset = ftell($this->fp); if ($this->expectDataDescriptor) { // The last file had bit 3 of the general purpose bit flag set. This means that we have a // 12 byte data descriptor we need to skip. To make things worse, there might also be a 4 // byte optional data descriptor header (0x08074b50). $junk = @fread($this->fp, 4); $junk = unpack('Vsig', $junk); if ($junk['sig'] == 0x08074b50) { // Yes, there was a signature $junk = @fread($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); debugMsg('Data descriptor (w/out header) skipped at ' . (ftell($this->fp) - 8)); } // And check for EOF, too if ($this->isEOF(true)) { debugMsg('EOF before reading header'); $this->nextFile(); } } // Get and decode Local File Header $headerBinary = fread($this->fp, 30); $headerData = unpack('Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/Vuncomp/vfnamelen/veflen', $headerBinary); // Check signature if (!($headerData['sig'] == 0x04034b50)) { 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) { 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 return false; } else { debugMsg('Invalid signature ' . dechex($headerData['sig']) . ' at ' . ftell($this->fp)); $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); return false; } } // If bit 3 of the bitflag is set, expectDataDescriptor is true $this->expectDataDescriptor = ($headerData['bitflag'] & 4) == 4; $this->fileHeader = new stdClass(); $this->fileHeader->timestamp = 0; // 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']; $this->fileHeader->uncompressed = $headerData['uncomp']; $nameFieldLength = $headerData['fnamelen']; $extraFieldLength = $headerData['eflen']; // Read filename field $this->fileHeader->file = fread($this->fp, $nameFieldLength); // Handle file renaming $isRenamed = false; if (is_array($this->renameFiles) && (count($this->renameFiles) > 0)) { if (array_key_exists($this->fileHeader->file, $this->renameFiles)) { $this->fileHeader->file = $this->renameFiles[$this->fileHeader->file]; $isRenamed = true; } } // Handle directory renaming $isDirRenamed = false; if (is_array($this->renameDirs) && (count($this->renameDirs) > 0)) { 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; } } // Read extra field if present if ($extraFieldLength > 0) { $extrafield = fread($this->fp, $extraFieldLength); } debugMsg('*' . ftell($this->fp) . ' IS START OF ' . $this->fileHeader->file . ' (' . $this->fileHeader->compressed . ' bytes)'); // Decide filetype -- Check for directories $this->fileHeader->type = 'file'; if (strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1) { $this->fileHeader->type = 'dir'; } // Decide filetype -- Check for symbolic links if (($headerData['ver1'] == 10) && ($headerData['ver2'] == 3)) { $this->fileHeader->type = 'link'; } switch ($headerData['compmethod']) { case 0: $this->fileHeader->compression = 'none'; break; case 8: $this->fileHeader->compression = 'gzip'; break; } // Find hard-coded banned files if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) { $isBannedFile = true; } // Also try to find banned files passed in class configuration if ((count($this->skipFiles) > 0) && (!$isRenamed)) { if (in_array($this->fileHeader->file, $this->skipFiles)) { $isBannedFile = true; } } // If we have a banned file, let's skip it if ($isBannedFile) { // Advance the file pointer, skipping exactly the size of the compressed data $seekleft = $this->fileHeader->compressed; while ($seekleft > 0) { // Ensure that we can seek past archive part boundaries $curSize = @filesize($this->archiveList[$this->currentPartNumber]); $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; } // Remove the removePath, if any $this->fileHeader->file = $this->removePath($this->fileHeader->file); // 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 */ 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--; // 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) { $this->max_exec_time = $php_max_exec_time; } else { $this->max_exec_time = $config_max_exec_time; } } /** * Returns the current timestampt in decimal seconds */ private function microtime_float() { list($usec, $sec) = explode(" ", microtime()); return ((float) $usec + (float) $sec); } /** * 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; } /** * 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 { $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(); } /** * @param int $max_exec_time */ public function setMaxExecTime($max_exec_time) { $this->max_exec_time = $max_exec_time; } } /** * A filesystem scanner which uses opendir() */ class AKUtilsLister extends AKAbstractObject { public function &getFiles($folder, $pattern = '*') { // Initialize variables $arr = array(); $false = false; if (!is_dir($folder)) { return $false; } $handle = @opendir($folder); // If directory is not accessible, just return FALSE if ($handle === false) { $this->setWarning('Unreadable directory ' . $folder); return $false; } while (($file = @readdir($handle)) !== false) { if (!fnmatch($pattern, $file)) { continue; } if (($file != '.') && ($file != '..')) { $ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR; $dir = $folder . $ds . $file; $isDir = is_dir($dir); if (!$isDir) { $arr[] = $dir; } } } @closedir($handle); return $arr; } public function &getFolders($folder, $pattern = '*') { // Initialize variables $arr = array(); $false = false; if (!is_dir($folder)) { return $false; } $handle = @opendir($folder); // If directory is not accessible, just return FALSE if ($handle === false) { $this->setWarning('Unreadable directory ' . $folder); return $false; } while (($file = @readdir($handle)) !== false) { if (!fnmatch($pattern, $file)) { continue; } if (($file != '.') && ($file != '..')) { $ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR; $dir = $folder . $ds . $file; $isDir = is_dir($dir); if ($isDir) { $arr[] = $dir; } } } @closedir($handle); return $arr; } } /** * A simple INI-based i18n engine */ class AKText extends AKAbstractObject { /** * The default (en_GB) translation used when no other translation is available * * @var array */ private $default_translation = [ 'ERR_NOT_A_JPA_FILE' => 'The file is not a JPA archive', 'ERR_CORRUPT_ARCHIVE' => 'The archive file is corrupt, truncated or archive parts are missing', 'ERR_INVALID_LOGIN' => 'Invalid login', 'COULDNT_CREATE_DIR' => 'Could not create %s folder', 'COULDNT_WRITE_FILE' => 'Could not open %s for writing.', 'INVALID_FILE_HEADER' => 'Invalid header in archive file, part %s, offset %s', 'ERR_COULD_NOT_OPEN_ARCHIVE_PART' => 'Could not open archive part file %s for reading. Check that the file exists, is readable by the web server and is not in a directory made out of reach by chroot, open_basedir restrictions or any other restriction put in place by your host.', ]; /** * The array holding the translation keys * * @var array */ private $strings; /** * The currently detected language (ISO code) * * @var string */ private $language; /* * Initializes the translation engine * @return AKText */ public function __construct() { // Start with the default translation $this->strings = $this->default_translation; // Try loading the translation file in English, if it exists $this->loadTranslation('en-GB'); // Try loading the translation file in the browser's preferred language, if it exists $this->getBrowserLanguage(); if (!is_null($this->language)) { $this->loadTranslation(); } } private function loadTranslation($lang = null) { 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; if (!@file_exists($translationFilename) && ($basename != 'kickstart.ini')) { $basename = 'kickstart.ini'; $translationFilename = $dirname . DIRECTORY_SEPARATOR . $lang . '.' . $basename; } if (!@file_exists($translationFilename)) { return; } $temp = self::parse_ini_file($translationFilename, false); if (!is_array($this->strings)) { $this->strings = array(); } if (empty($temp)) { $this->strings = array_merge($this->default_translation, $this->strings); } else { $this->strings = array_merge($this->strings, $temp); } } /** * A PHP based INI file parser. * * Thanks to asohn ~at~ aircanopy ~dot~ net for posting this handy function on * the parse_ini_file page on http://gr.php.net/parse_ini_file * * @param string $file Filename to process * @param bool $process_sections True to also process INI sections * * @return array An associative array of sections, keys and values * @access private */ public static function parse_ini_file($file, $process_sections = false, $raw_data = false) { $process_sections = ($process_sections !== true) ? false : true; if (!$raw_data) { $ini = @file($file); } else { $ini = $file; } if (count($ini) == 0) { return array(); } $sections = array(); $values = array(); $result = array(); $globals = array(); $i = 0; if (!empty($ini)) { foreach ($ini as $line) { $line = trim($line); $line = str_replace("\t", " ", $line); // Comments if (!preg_match('/^[a-zA-Z0-9[]/', $line)) { continue; } // Sections if ($line[0] == '[') { $tmp = explode(']', $line); $sections[] = trim(substr($tmp[0], 1)); $i++; continue; } // Key-value pair list($key, $value) = explode('=', $line, 2); $key = trim($key); $value = trim($value); if (strstr($value, ";")) { $tmp = explode(';', $value); if (count($tmp) == 2) { if ((($value[0] != '"') && ($value[0] != "'")) || preg_match('/^".*"\s*;/', $value) || preg_match('/^".*;[^"]*$/', $value) || preg_match("/^'.*'\s*;/", $value) || preg_match("/^'.*;[^']*$/", $value) ) { $value = $tmp[0]; } } else { if ($value[0] == '"') { $value = preg_replace('/^"(.*)".*/', '$1', $value); } elseif ($value[0] == "'") { $value = preg_replace("/^'(.*)'.*/", '$1', $value); } else { $value = $tmp[0]; } } } $value = trim($value); $value = trim($value, "'\""); if ($i == 0) { if (substr($line, -1, 2) == '[]') { $globals[$key][] = $value; } else { $globals[$key] = $value; } } else { if (substr($line, -1, 2) == '[]') { $values[$i - 1][$key][] = $value; } else { $values[$i - 1][$key] = $value; } } } } for ($j = 0; $j < $i; $j++) { if ($process_sections === true) { $result[$sections[$j]] = $values[$j]; } else { $result[] = $values[$j]; } } return $result + $globals; } public function getBrowserLanguage() { // Detection code from Full Operating system language detection, by Harald Hope // Retrieved from http://techpatterns.com/downloads/php_language_detection.php $user_languages = array(); //check to see if language is set if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) { $languages = strtolower($_SERVER["HTTP_ACCEPT_LANGUAGE"]); // $languages = ' fr-ch;q=0.3, da, en-us;q=0.8, en;q=0.5, fr;q=0.3'; // need to remove spaces from strings to avoid error $languages = str_replace(' ', '', $languages); $languages = explode(",", $languages); foreach ($languages as $language_list) { // pull out the language, place languages into array of full and primary // string structure: $temp_array = array(); // slice out the part before ; on first step, the part before - on second, place into array $temp_array[0] = substr($language_list, 0, strcspn($language_list, ';'));//full language $temp_array[1] = substr($language_list, 0, 2);// cut out primary language if ((strlen($temp_array[0]) == 5) && ((substr($temp_array[0], 2, 1) == '-') || (substr($temp_array[0], 2, 1) == '_'))) { $langLocation = strtoupper(substr($temp_array[0], 3, 2)); $temp_array[0] = $temp_array[1] . '-' . $langLocation; } //place this array into main $user_languages language array $user_languages[] = $temp_array; } } else// if no languages found { $user_languages[0] = array('', ''); //return blank array. } $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. 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(KSROOTDIR, $languageStruct[1] . '-??.' . $basename); if (count($iniFiles) > 0) { $filename = $iniFiles[0]; $filename = substr($filename, strlen(KSROOTDIR) + 1); $this->language = substr($filename, 0, 5); } else { $this->language = null; } } } } if (is_null($this->language)) { // Try to find a full language match foreach ($user_languages as $languageStruct) { if (@file_exists($languageStruct[0] . '.' . $basename) && is_null($this->language)) { $this->language = $languageStruct[0]; } else { } } } else { // Do we have an exact match? foreach ($user_languages as $languageStruct) { if (substr($this->language, 0, strlen($languageStruct[1])) == $languageStruct[1]) { if (file_exists($languageStruct[0] . '.' . $basename)) { $this->language = $languageStruct[0]; } } } } // Now, scan for full language based on the partial match } public static function sprintf($key) { $text = self::getInstance(); $args = func_get_args(); if (count($args) > 0) { $args[0] = $text->_($args[0]); return @call_user_func_array('sprintf', $args); } return ''; } /** * Singleton pattern for Language * * @return AKText The global AKText instance */ public static function &getInstance() { static $instance; if (!is_object($instance)) { $instance = new AKText(); } return $instance; } public static function _($string) { $text = self::getInstance(); $key = strtoupper($string); $key = substr($key, 0, 1) == '_' ? substr($key, 1) : $key; if (isset ($text->strings[$key])) { $string = $text->strings[$key]; } else { if (defined($string)) { $string = constant($string); } } return $string; } public function dumpLanguage() { $out = ''; foreach ($this->strings as $key => $value) { $out .= "$key=$value\n"; } return $out; } public function asJavascript() { $out = ''; foreach ($this->strings as $key => $value) { $key = addcslashes($key, '\\\'"'); $value = addcslashes($value, '\\\'"'); if (!empty($out)) { $out .= ",\n"; } $out .= "'$key':\t'$value'"; } return $out; } public function resetTranslation() { $this->strings = $this->default_translation; } public function addDefaultLanguageStrings($stringList = array()) { if (!is_array($stringList)) { return; } if (empty($stringList)) { return; } $this->strings = array_merge($stringList, $this->strings); } } /** * The Akeeba Kickstart Factory class * This class is reponssible for instanciating all Akeeba Kicsktart classes */ class AKFactory { /** @var array A list of instantiated objects */ private $objectlist = array(); /** @var array Simple hash data storage */ private $varlist = array(); /** @var self Static instance */ private static $instance = null; /** Private constructor makes sure we can't directly instantiate the class */ private function __construct() { } /** * Gets a serialized snapshot of the Factory for safekeeping (hibernate) * * @return string The serialized snapshot of the Factory */ public static function serialize() { $engine = self::getUnarchiver(); $engine->shutdown(); $serialized = serialize(self::getInstance()); if (function_exists('base64_encode') && function_exists('base64_decode')) { $serialized = base64_encode($serialized); } return $serialized; } /** * Gets the unarchiver engine */ public static function &getUnarchiver($configOverride = null) { static $class_name; if (!empty($configOverride)) { if ($configOverride['reset']) { $class_name = null; } } if (empty($class_name)) { $filetype = self::get('kickstart.setup.filetype', null); if (empty($filetype)) { $filename = self::get('kickstart.setup.sourcefile', null); $basename = basename($filename); $baseextension = strtoupper(substr($basename, -3)); switch ($baseextension) { case 'JPA': $filetype = 'JPA'; break; case 'JPS': $filetype = 'JPS'; break; case 'ZIP': $filetype = 'ZIP'; break; default: die('Invalid archive type or extension in file ' . $filename); break; } } $class_name = 'AKUnarchiver' . ucfirst($filetype); } $destdir = self::get('kickstart.setup.destdir', null); if (empty($destdir)) { $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 –– Any change here MUST be reflected to echoHeadJavascript (default values) $config = array( 'filename' => $sourceFile, 'restore_permissions' => self::get('kickstart.setup.restoreperms', 0), 'post_proc' => self::get('kickstart.procengine', 'direct'), 'add_path' => self::get('kickstart.setup.targetpath', $destdir), 'remove_path' => self::get('kickstart.setup.removepath', ''), 'rename_files' => self::get('kickstart.setup.renamefiles', array( '.htaccess' => 'htaccess.bak', 'php.ini' => 'php.ini.bak', 'web.config' => 'web.config.bak', '.user.ini' => '.user.ini.bak' )), 'skip_files' => self::get('kickstart.setup.skipfiles', array( basename(__FILE__), 'kickstart.php', 'abiautomation.ini', 'htaccess.bak', 'php.ini.bak', 'cacert.pem' )), 'ignoredirectories' => self::get('kickstart.setup.ignoredirectories', array( 'tmp', 'log', 'logs' )), ); if (!defined('KICKSTART')) { // In restore.php mode we have to exclude the restoration.php files $moreSkippedFiles = array( // Akeeba Backup for Joomla! 'administrator/components/com_akeeba/restoration.php', // Joomla! Update 'administrator/components/com_joomlaupdate/restoration.php', // Akeeba Backup for WordPress 'wp-content/plugins/akeebabackupwp/app/restoration.php', 'wp-content/plugins/akeebabackupcorewp/app/restoration.php', 'wp-content/plugins/akeebabackup/app/restoration.php', 'wp-content/plugins/akeebabackupwpcore/app/restoration.php', // Akeeba Solo 'app/restoration.php', ); $config['skip_files'] = array_merge($config['skip_files'], $moreSkippedFiles); } if (!empty($configOverride)) { $config = array_merge($config, $configOverride); } $object->setup($config); } return $object; } // ======================================================================== // Public factory interface // ======================================================================== public static function get($key, $default = null) { $self = self::getInstance(); if (array_key_exists($key, $self->varlist)) { return $self->varlist[$key]; } else { return $default; } } /** * Gets a single, internally used instance of the Factory * * @param string $serialized_data [optional] Serialized data to spawn the instance from * * @return AKFactory A reference to the unique Factory object instance */ protected static function &getInstance($serialized_data = null) { if (!is_object(self::$instance) || !is_null($serialized_data)) { if (!is_null($serialized_data)) { self::$instance = unserialize($serialized_data); } else { self::$instance = new self(); } } return self::$instance; } /** * Internal function which instanciates a class named $class_name. * The autoloader * * @param string $class_name * * @return object */ protected static function &getClassInstance($class_name) { $self = self::getInstance(); if (!isset($self->objectlist[$class_name])) { $self->objectlist[$class_name] = new $class_name; } return $self->objectlist[$class_name]; } // ======================================================================== // Public hash data storage interface // ======================================================================== /** * Regenerates the full Factory state from a serialized snapshot (resume) * * @param string $serialized_data The serialized snapshot to resume from */ public static function unserialize($serialized_data) { if (function_exists('base64_encode') && function_exists('base64_decode')) { $serialized_data = base64_decode($serialized_data); } self::getInstance($serialized_data); } /** * Reset the internal factory state, freeing all previously created objects */ public static function nuke() { self::$instance = null; } // ======================================================================== // Akeeba Kickstart classes // ======================================================================== public static function set($key, $value) { $self = self::getInstance(); $self->varlist[$key] = $value; } /** * Gets the post processing engine * * @param string $proc_engine */ public static function &getPostProc($proc_engine = null) { static $class_name; if (empty($class_name)) { if (empty($proc_engine)) { $proc_engine = self::get('kickstart.procengine', 'direct'); } $class_name = 'AKPostproc' . ucfirst($proc_engine); } return self::getClassInstance($class_name); } /** * Get the a reference to the Akeeba Engine's timer * * @return AKCoreTimer */ public static function &getTimer() { return self::getClassInstance('AKCoreTimer'); } } /** * Interface for AES encryption adapters */ interface AKEncryptionAESAdapterInterface { /** * Decrypts a string. Returns the raw binary ciphertext, zero-padded. * * @param string $plainText The plaintext to encrypt * @param string $key The raw binary key (will be zero-padded or chopped if its size is different than the block size) * * @return string The raw encrypted binary string. */ public function decrypt($plainText, $key); /** * Returns the encryption block size in bytes * * @return int */ public function getBlockSize(); /** * Is this adapter supported? * * @return bool */ public function isSupported(); } /** * Abstract AES encryption class */ abstract class AKEncryptionAESAdapterAbstract { /** * Trims or zero-pads a key / IV * * @param string $key The key or IV to treat * @param int $size The block size of the currently used algorithm * * @return null|string Null if $key is null, treated string of $size byte length otherwise */ public function resizeKey($key, $size) { if (empty($key)) { return null; } $keyLength = strlen($key); if (function_exists('mb_strlen')) { $keyLength = mb_strlen($key, 'ASCII'); } if ($keyLength == $size) { return $key; } if ($keyLength > $size) { if (function_exists('mb_substr')) { return mb_substr($key, 0, $size, 'ASCII'); } return substr($key, 0, $size); } return $key . str_repeat("\0", ($size - $keyLength)); } /** * Returns null bytes to append to the string so that it's zero padded to the specified block size * * @param string $string The binary string which will be zero padded * @param int $blockSize The block size * * @return string The zero bytes to append to the string to zero pad it to $blockSize */ protected function getZeroPadding($string, $blockSize) { $stringSize = strlen($string); if (function_exists('mb_strlen')) { $stringSize = mb_strlen($string, 'ASCII'); } if ($stringSize == $blockSize) { return ''; } if ($stringSize < $blockSize) { return str_repeat("\0", $blockSize - $stringSize); } $paddingBytes = $stringSize % $blockSize; return str_repeat("\0", $blockSize - $paddingBytes); } } class Mcrypt extends AKEncryptionAESAdapterAbstract implements AKEncryptionAESAdapterInterface { protected $cipherType = MCRYPT_RIJNDAEL_128; protected $cipherMode = MCRYPT_MODE_CBC; public function decrypt($cipherText, $key) { $iv_size = $this->getBlockSize(); $key = $this->resizeKey($key, $iv_size); $iv = substr($cipherText, 0, $iv_size); $cipherText = substr($cipherText, $iv_size); $plainText = mcrypt_decrypt($this->cipherType, $key, $cipherText, $this->cipherMode, $iv); return $plainText; } public function isSupported() { if (!function_exists('mcrypt_get_key_size')) { return false; } if (!function_exists('mcrypt_get_iv_size')) { return false; } if (!function_exists('mcrypt_create_iv')) { return false; } if (!function_exists('mcrypt_encrypt')) { return false; } if (!function_exists('mcrypt_decrypt')) { return false; } if (!function_exists('mcrypt_list_algorithms')) { return false; } if (!function_exists('hash')) { return false; } if (!function_exists('hash_algos')) { return false; } $algorightms = mcrypt_list_algorithms(); if (!in_array('rijndael-128', $algorightms)) { return false; } if (!in_array('rijndael-192', $algorightms)) { return false; } if (!in_array('rijndael-256', $algorightms)) { return false; } $algorightms = hash_algos(); if (!in_array('sha256', $algorightms)) { return false; } return true; } public function getBlockSize() { return mcrypt_get_iv_size($this->cipherType, $this->cipherMode); } } class OpenSSL extends AKEncryptionAESAdapterAbstract implements AKEncryptionAESAdapterInterface { /** * The OpenSSL options for encryption / decryption * * @var int */ protected $openSSLOptions = 0; /** * The encryption method to use * * @var string */ protected $method = 'aes-128-cbc'; public function __construct() { $this->openSSLOptions = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING; } public function decrypt($cipherText, $key) { $iv_size = $this->getBlockSize(); $key = $this->resizeKey($key, $iv_size); $iv = substr($cipherText, 0, $iv_size); $cipherText = substr($cipherText, $iv_size); $plainText = openssl_decrypt($cipherText, $this->method, $key, $this->openSSLOptions, $iv); return $plainText; } public function isSupported() { if (!function_exists('openssl_get_cipher_methods')) { return false; } if (!function_exists('openssl_random_pseudo_bytes')) { return false; } if (!function_exists('openssl_cipher_iv_length')) { return false; } if (!function_exists('openssl_encrypt')) { return false; } if (!function_exists('openssl_decrypt')) { return false; } if (!function_exists('hash')) { return false; } if (!function_exists('hash_algos')) { return false; } $algorightms = openssl_get_cipher_methods(); if (!in_array('aes-128-cbc', $algorightms)) { return false; } $algorightms = hash_algos(); if (!in_array('sha256', $algorightms)) { return false; } return true; } /** * @return int */ public function getBlockSize() { return openssl_cipher_iv_length($this->method); } } /** * AES implementation in PHP (c) Chris Veness 2005-2016. * Right to use and adapt is granted for under a simple creative commons attribution * licence. No warranty of any form is offered. * * Heavily modified for Akeeba Backup by Nicholas K. Dionysopoulos * Also added AES-128 CBC mode (with mcrypt and OpenSSL) on top of AES CTR */ class AKEncryptionAES { // 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, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 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] protected static $Rcon = array( array(0x00, 0x00, 0x00, 0x00), array(0x01, 0x00, 0x00, 0x00), array(0x02, 0x00, 0x00, 0x00), array(0x04, 0x00, 0x00, 0x00), array(0x08, 0x00, 0x00, 0x00), array(0x10, 0x00, 0x00, 0x00), array(0x20, 0x00, 0x00, 0x00), array(0x40, 0x00, 0x00, 0x00), array(0x80, 0x00, 0x00, 0x00), array(0x1b, 0x00, 0x00, 0x00), array(0x36, 0x00, 0x00, 0x00)); protected static $passwords = array(); /** * The algorithm to use for PBKDF2. Must be a supported hash_hmac algorithm. Default: sha1 * * @var string */ private static $pbkdf2Algorithm = 'sha1'; /** * Number of iterations to use for PBKDF2 * * @var int */ private static $pbkdf2Iterations = 1000; /** * Should we use a static salt for PBKDF2? * * @var int */ private static $pbkdf2UseStaticSalt = 0; /** * The static salt to use for PBKDF2 * * @var string */ private static $pbkdf2StaticSalt = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; /** * Encrypt a text using AES encryption in Counter mode of operation * - see http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf * * Unicode multi-byte character safe * * @param string $plaintext Source text to be encrypted * @param string $password The password to use to generate a key * @param int $nBits Number of bits to be used in the key (128, 192, or 256) * * @return string Encrypted text */ public static function AESEncryptCtr($plaintext, $password, $nBits) { $blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES if (!($nBits == 128 || $nBits == 192 || $nBits == 256)) { return ''; } // standard allows 128/192/256 bit keys // note PHP (5) gives us plaintext and password in UTF8 encoding! // use AES itself to encrypt password to get cipher key (using plain password as source for // key expansion) - gives us well encrypted key $nBytes = $nBits / 8; // no bytes in key $pwBytes = array(); for ($i = 0; $i < $nBytes; $i++) { $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; } $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 // 1st 8 bytes, block counter in 2nd 8 bytes $counterBlock = array(); $nonce = floor(microtime(true) * 1000); // timestamp: milliseconds since 1-Jan-1970 $nonceSec = floor($nonce / 1000); $nonceMs = $nonce % 1000; // encode nonce with seconds in 1st 4 bytes, and (repeated) ms part filling 2nd 4 bytes for ($i = 0; $i < 4; $i++) { $counterBlock[$i] = self::urs($nonceSec, $i * 8) & 0xff; } for ($i = 0; $i < 4; $i++) { $counterBlock[$i + 4] = $nonceMs & 0xff; } // and convert it to a string to go on the front of the ciphertext $ctrTxt = ''; for ($i = 0; $i < 8; $i++) { $ctrTxt .= chr($counterBlock[$i]); } // generate key schedule - an expansion of the key into distinct Key Rounds for each round $keySchedule = self::KeyExpansion($key); $blockCount = ceil(strlen($plaintext) / $blockSize); $ciphertxt = array(); // ciphertext as array of strings for ($b = 0; $b < $blockCount; $b++) { // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes) // done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB) for ($c = 0; $c < 4; $c++) { $counterBlock[15 - $c] = self::urs($b, $c * 8) & 0xff; } for ($c = 0; $c < 4; $c++) { $counterBlock[15 - $c - 4] = self::urs($b / 0x100000000, $c * 8); } $cipherCntr = self::Cipher($counterBlock, $keySchedule); // -- encrypt counter block -- // block size is reduced on final block $blockLength = $b < $blockCount - 1 ? $blockSize : (strlen($plaintext) - 1) % $blockSize + 1; $cipherByte = array(); for ($i = 0; $i < $blockLength; $i++) { // -- xor plaintext with ciphered counter byte-by-byte -- $cipherByte[$i] = $cipherCntr[$i] ^ ord(substr($plaintext, $b * $blockSize + $i, 1)); $cipherByte[$i] = chr($cipherByte[$i]); } $ciphertxt[$b] = implode('', $cipherByte); // escape troublesome characters in ciphertext } // implode is more efficient than repeated string concatenation $ciphertext = $ctrTxt . implode('', $ciphertxt); $ciphertext = base64_encode($ciphertext); return $ciphertext; } /** * AES Cipher function: encrypt 'input' with Rijndael algorithm * * @param array $input Message as byte-array (16 bytes) * @param array $w key schedule as 2D byte-array (Nr+1 x Nb bytes) - * generated from the cipher key by KeyExpansion() * * @return string Ciphertext as byte-array (16 bytes) */ 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] for ($i = 0; $i < 4 * $Nb; $i++) { $state[$i % 4][floor($i / 4)] = $input[$i]; } $state = self::AddRoundKey($state, $w, 0, $Nb); for ($round = 1; $round < $Nr; $round++) { // apply Nr rounds $state = self::SubBytes($state, $Nb); $state = self::ShiftRows($state, $Nb); $state = self::MixColumns($state); $state = self::AddRoundKey($state, $w, $round, $Nb); } $state = self::SubBytes($state, $Nb); $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] 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] 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] 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] $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 for ($c = 0; $c < 4; $c++) { $s[$r][$c] = $t[$c]; } // and copy back } // note that this will work for Nb=4,5,6, but not 7,8 (always 4 for AES): return $s; // see fp.gladman.plus.com/cryptography_technology/rijndael/aes.spec.311.pdf } protected static function MixColumns($s) { // 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) 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) $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 $s[3][$c] = $a[0] ^ $b[0] ^ $a[1] ^ $a[2] ^ $b[3]; // 3*a0 + a1 + a2 + 2*a3 } return $s; } /** * Key expansion for Rijndael Cipher(): performs key expansion on cipher key * to generate a key schedule * * @param array $key Cipher key byte-array (16 bytes) * * @return array Key schedule as 2D byte-array (Nr+1 x Nb bytes) */ protected static function KeyExpansion($key) { // generate Key Schedule from Cipher Key [�5.2] // block size (in words): no of columns in state (fixed at 4 for AES) $Nb = 4; // key length (in words): 4/6/8 for 128/192/256-bit keys $Nk = (int) (count($key) / 4); // no of rounds: 10/12/14 for 128/192/256-bit keys $Nr = $Nk + 6; $w = array(); $temp = array(); for ($i = 0; $i < $Nk; $i++) { $r = array($key[4 * $i], $key[4 * $i + 1], $key[4 * $i + 2], $key[4 * $i + 3]); $w[$i] = $r; } for ($i = $Nk; $i < ($Nb * ($Nr + 1)); $i++) { $w[$i] = array(); for ($t = 0; $t < 4; $t++) { $temp[$t] = $w[$i - 1][$t]; } if ($i % $Nk == 0) { $temp = self::SubWord(self::RotWord($temp)); for ($t = 0; $t < 4; $t++) { $rConIndex = (int) ($i / $Nk); $temp[$t] ^= self::$Rcon[$rConIndex][$t]; } } else if ($Nk > 6 && $i % $Nk == 4) { $temp = self::SubWord($temp); } for ($t = 0; $t < 4; $t++) { $w[$i][$t] = $w[$i - $Nk][$t] ^ $temp[$t]; } } return $w; } protected static function SubWord($w) { // apply SBox to 4-byte word w for ($i = 0; $i < 4; $i++) { $w[$i] = self::$Sbox[$w[$i]]; } return $w; } /* * Unsigned right shift function, since PHP has neither >>> operator nor unsigned ints * * @param a number to be shifted (32-bit integer) * @param b number of bits to shift a to the right (0..31) * @return a right-shifted and zero-filled by b bits */ protected static function RotWord($w) { // rotate 4-byte word w left by one byte $tmp = $w[0]; for ($i = 0; $i < 3; $i++) { $w[$i] = $w[$i + 1]; } $w[3] = $tmp; return $w; } protected static function urs($a, $b) { $a &= 0xffffffff; $b &= 0x1f; // (bounds check) if ($a & 0x80000000 && $b > 0) { // if left-most bit set $a = ($a >> 1) & 0x7fffffff; // right-shift one bit & clear left-most bit $a = $a >> ($b - 1); // remaining right-shifts } else { // otherwise $a = ($a >> $b); // use normal right-shift } return $a; } /** * Decrypt a text encrypted by AES in counter mode of operation * * @param string $ciphertext Source text to be decrypted * @param string $password The password to use to generate a key * @param int $nBits Number of bits to be used in the key (128, 192, or 256) * * @return string Decrypted text */ public static function AESDecryptCtr($ciphertext, $password, $nBits) { $blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES if (!($nBits == 128 || $nBits == 192 || $nBits == 256)) { return ''; } // standard allows 128/192/256 bit keys $ciphertext = base64_decode($ciphertext); // use AES to encrypt password (mirroring encrypt routine) $nBytes = $nBits / 8; // no bytes in key $pwBytes = array(); for ($i = 0; $i < $nBytes; $i++) { $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; } $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 // recover nonce from 1st element of ciphertext $counterBlock = array(); $ctrTxt = substr($ciphertext, 0, 8); for ($i = 0; $i < 8; $i++) { $counterBlock[$i] = ord(substr($ctrTxt, $i, 1)); } // generate key schedule $keySchedule = self::KeyExpansion($key); // separate ciphertext into blocks (skipping past initial 8 bytes) $nBlocks = ceil((strlen($ciphertext) - 8) / $blockSize); $ct = array(); for ($b = 0; $b < $nBlocks; $b++) { $ct[$b] = substr($ciphertext, 8 + $b * $blockSize, 16); } $ciphertext = $ct; // ciphertext is now array of block-length strings // plaintext will get generated block-by-block into array of block-length strings $plaintxt = array(); for ($b = 0; $b < $nBlocks; $b++) { // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes) for ($c = 0; $c < 4; $c++) { $counterBlock[15 - $c] = self::urs($b, $c * 8) & 0xff; } for ($c = 0; $c < 4; $c++) { $counterBlock[15 - $c - 4] = self::urs(($b + 1) / 0x100000000 - 1, $c * 8) & 0xff; } $cipherCntr = self::Cipher($counterBlock, $keySchedule); // encrypt counter block $plaintxtByte = array(); for ($i = 0; $i < strlen($ciphertext[$b]); $i++) { // -- xor plaintext with ciphered counter byte-by-byte -- $plaintxtByte[$i] = $cipherCntr[$i] ^ ord(substr($ciphertext[$b], $i, 1)); $plaintxtByte[$i] = chr($plaintxtByte[$i]); } $plaintxt[$b] = implode('', $plaintxtByte); } // join array of blocks into single plaintext string $plaintext = implode('', $plaintxt); return $plaintext; } /** * AES decryption in CBC mode. This is the standard mode (the CTR methods * actually use Rijndael-128 in CTR mode, which - technically - isn't AES). * * It supports AES-128 only. It assumes that the last 4 bytes * contain a little-endian unsigned long integer representing the unpadded * data length. * * @since 3.0.1 * @author Nicholas K. Dionysopoulos * * @param string $ciphertext The data to encrypt * @param string $password Encryption password * * @return string The plaintext */ public static function AESDecryptCBC($ciphertext, $password) { $adapter = self::getAdapter(); if (!$adapter->isSupported()) { return false; } // Read the data size $data_size = unpack('V', substr($ciphertext, -4)); // Do I have a PBKDF2 salt? $salt = substr($ciphertext, -92, 68); $rightStringLimit = -4; $params = self::getKeyDerivationParameters(); $keySizeBytes = $params['keySize']; $algorithm = $params['algorithm']; $iterations = $params['iterations']; $useStaticSalt = $params['useStaticSalt']; if (substr($salt, 0, 4) == 'JPST') { // We have a stored salt. Retrieve it and tell decrypt to process the string minus the last 44 bytes // (4 bytes for JPST, 16 bytes for the salt, 4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the // uncompressed string length - note that using PBKDF2 means we're also using a randomized IV per the // format specification). $salt = substr($salt, 4); $rightStringLimit -= 68; $key = self::pbkdf2($password, $salt, $algorithm, $iterations, $keySizeBytes); } elseif ($useStaticSalt) { // We have a static salt. Use it for PBKDF2. $key = self::getStaticSaltExpandedKey($password); } else { // Get the expanded key from the password. THIS USES THE OLD, INSECURE METHOD. $key = self::expandKey($password); } // Try to get the IV from the data $iv = substr($ciphertext, -24, 20); if (substr($iv, 0, 4) == 'JPIV') { // We have a stored IV. Retrieve it and tell mdecrypt to process the string minus the last 24 bytes // (4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the uncompressed string length) $iv = substr($iv, 4); $rightStringLimit -= 20; } else { // No stored IV. Do it the dumb way. $iv = self::createTheWrongIV($password); } // Decrypt $plaintext = $adapter->decrypt($iv . substr($ciphertext, 0, $rightStringLimit), $key); // Trim padding, if necessary if (strlen($plaintext) > $data_size) { $plaintext = substr($plaintext, 0, $data_size); } return $plaintext; } /** * That's the old way of creating an IV that's definitely not cryptographically sound. * * DO NOT USE, EVER, UNLESS YOU WANT TO DECRYPT LEGACY DATA * * @param string $password The raw password from which we create an IV in a super bozo way * * @return string A 16-byte IV string */ public static function createTheWrongIV($password) { static $ivs = array(); $key = md5($password); if (!isset($ivs[$key])) { $nBytes = 16; // AES uses a 128 -bit (16 byte) block size, hence the IV size is always 16 bytes $pwBytes = array(); for ($i = 0; $i < $nBytes; $i++) { $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; } $iv = self::Cipher($pwBytes, self::KeyExpansion($pwBytes)); $newIV = ''; foreach ($iv as $int) { $newIV .= chr($int); } $ivs[$key] = $newIV; } return $ivs[$key]; } /** * Expand the password to an appropriate 128-bit encryption key * * @param string $password * * @return string * * @since 5.2.0 * @author Nicholas K. Dionysopoulos */ public static function expandKey($password) { // Try to fetch cached key or create it if it doesn't exist $nBits = 128; $lookupKey = md5($password . '-' . $nBits); if (array_key_exists($lookupKey, self::$passwords)) { $key = self::$passwords[$lookupKey]; return $key; } // use AES itself to encrypt password to get cipher key (using plain password as source for // key expansion) - gives us well encrypted key. $nBytes = $nBits / 8; // Number of bytes in key $pwBytes = array(); for ($i = 0; $i < $nBytes; $i++) { $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; } $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 $newKey = ''; foreach ($key as $int) { $newKey .= chr($int); } $key = $newKey; self::$passwords[$lookupKey] = $key; return $key; } /** * Returns the correct AES-128 CBC encryption adapter * * @return AKEncryptionAESAdapterInterface * * @since 5.2.0 * @author Nicholas K. Dionysopoulos */ public static function getAdapter() { static $adapter = null; if (is_object($adapter) && ($adapter instanceof AKEncryptionAESAdapterInterface)) { return $adapter; } $adapter = new OpenSSL(); if (!$adapter->isSupported()) { $adapter = new Mcrypt(); } return $adapter; } /** * @return string */ public static function getPbkdf2Algorithm() { return self::$pbkdf2Algorithm; } /** * @param string $pbkdf2Algorithm * @return void */ public static function setPbkdf2Algorithm($pbkdf2Algorithm) { self::$pbkdf2Algorithm = $pbkdf2Algorithm; } /** * @return int */ public static function getPbkdf2Iterations() { return self::$pbkdf2Iterations; } /** * @param int $pbkdf2Iterations * @return void */ public static function setPbkdf2Iterations($pbkdf2Iterations) { self::$pbkdf2Iterations = $pbkdf2Iterations; } /** * @return int */ public static function getPbkdf2UseStaticSalt() { return self::$pbkdf2UseStaticSalt; } /** * @param int $pbkdf2UseStaticSalt * @return void */ public static function setPbkdf2UseStaticSalt($pbkdf2UseStaticSalt) { self::$pbkdf2UseStaticSalt = $pbkdf2UseStaticSalt; } /** * @return string */ public static function getPbkdf2StaticSalt() { return self::$pbkdf2StaticSalt; } /** * @param string $pbkdf2StaticSalt * @return void */ public static function setPbkdf2StaticSalt($pbkdf2StaticSalt) { self::$pbkdf2StaticSalt = $pbkdf2StaticSalt; } /** * Get the parameters fed into PBKDF2 to expand the user password into an encryption key. These are the static * parameters (key size, hashing algorithm and number of iterations). A new salt is used for each encryption block * to minimize the risk of attacks against the password. * * @return array */ public static function getKeyDerivationParameters() { return array( 'keySize' => 16, 'algorithm' => self::$pbkdf2Algorithm, 'iterations' => self::$pbkdf2Iterations, 'useStaticSalt' => self::$pbkdf2UseStaticSalt, 'staticSalt' => self::$pbkdf2StaticSalt, ); } /** * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt * * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt * * This implementation of PBKDF2 was originally created by https://defuse.ca * With improvements by http://www.variations-of-shadow.com * Modified for Akeeba Engine by Akeeba Ltd (removed unnecessary checks to make it faster) * * @param string $password The password. * @param string $salt A salt that is unique to the password. * @param string $algorithm The hash algorithm to use. Default is sha1. * @param int $count Iteration count. Higher is better, but slower. Default: 1000. * @param int $key_length The length of the derived key in bytes. * * @return string A string of $key_length bytes */ public static function pbkdf2($password, $salt, $algorithm = 'sha1', $count = 1000, $key_length = 16) { if (function_exists("hash_pbkdf2")) { return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, true); } $hash_length = akstringlen(hash($algorithm, "", true)); $block_count = ceil($key_length / $hash_length); $output = ""; for ($i = 1; $i <= $block_count; $i++) { // $i encoded as 4 bytes, big endian. $last = $salt . pack("N", $i); // First iteration $xorResult = hash_hmac($algorithm, $last, $password, true); $last = $xorResult; // Perform the other $count - 1 iterations for ($j = 1; $j < $count; $j++) { $last = hash_hmac($algorithm, $last, $password, true); $xorResult ^= $last; } $output .= $xorResult; } return aksubstr($output, 0, $key_length); } /** * Get the expanded key from the user supplied password using a static salt. The results are cached for performance * reasons. * * @param string $password The user-supplied password, UTF-8 encoded. * * @return string The expanded key */ private static function getStaticSaltExpandedKey($password) { $params = self::getKeyDerivationParameters(); $keySizeBytes = $params['keySize']; $algorithm = $params['algorithm']; $iterations = $params['iterations']; $staticSalt = $params['staticSalt']; $lookupKey = "PBKDF2-$algorithm-$iterations-" . md5($password . $staticSalt); if (!array_key_exists($lookupKey, self::$passwords)) { self::$passwords[$lookupKey] = self::pbkdf2($password, $staticSalt, $algorithm, $iterations, $keySizeBytes); } return self::$passwords[$lookupKey]; } } /** * 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() { // ------------------------------------------------------------ // 1. Import basic setup parameters // ------------------------------------------------------------ $ini_data = null; // In restore.php mode, require restoration.php or fail 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)) { 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)) { // No parameters fetched. Darn, how am I supposed to work like that?! AKFactory::set('kickstart.enabled', false); return false; } AKFactory::set('kickstart.enabled', true); } else { // Maybe we have $restoration_setup defined in the head of kickstart.php global $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)) { $ini_data = $restoration_setup; } } // Import any data from $restoration_setup if (!empty($ini_data)) { foreach ($ini_data as $key => $value) { AKFactory::set($key, $value); } AKFactory::set('kickstart.enabled', true); } // Reinitialize $ini_data $ini_data = null; // ------------------------------------------------------------ // 2. Explode JSON parameters into $_REQUEST scope // ------------------------------------------------------------ // Detect a JSON string in the request variable and store it. $json = getQueryParam('json', null); // Remove everything from the request, post and get arrays if (!empty($_REQUEST)) { 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 $password = AKFactory::get('kickstart.security.password', null); if (!empty($json)) { 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); if (!empty($password) && (empty($raw))) { die('###{"status":false,"message":"Invalid login"}###'); } // Pass all JSON data to the request array if (!empty($raw)) { 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)) { // Get the serialized factory AKFactory::unserialize($serialized); AKFactory::set('kickstart.enabled', true); return true; } // ------------------------------------------------------------ // 4. Try the configuration variable for Kickstart // ------------------------------------------------------------ if (defined('KICKSTART')) { $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); } // Import any INI data we might have from other sources if (!empty($ini_data)) { foreach ($ini_data as $key => $value) { AKFactory::set($key, $value); } AKFactory::set('kickstart.enabled', true); return true; } } } // Mini-controller for restore.php if (!defined('KICKSTART')) { // The observer class, used to report number of files and bytes processed class RestorationObserver extends AKAbstractPartObserver { public $compressedTotal = 0; public $uncompressedTotal = 0; public $filesProcessed = 0; public function update($object, $message) { if (!is_object($message)) { return; } if (!array_key_exists('type', get_object_vars($message))) { return; } if ($message->type == 'startfile') { $this->filesProcessed++; $this->compressedTotal += $message->content->compressed; $this->uncompressedTotal += $message->content->uncompressed; } } public function __toString() { return __CLASS__; } } // Import configuration masterSetup(); $retArray = array( 'status' => true, 'message' => null ); $enabled = AKFactory::get('kickstart.enabled', false); if ($enabled) { $task = getQueryParam('task'); switch ($task) { case 'ping': // ping task - really does nothing! $timer = AKFactory::getTimer(); $timer->enforce_min_exec_time(); break; /** * There are two separate steps here since we were using an inefficient restoration intialization method in * the past. Now both startRestore and stepRestore are identical. The difference in behavior depends * exclusively on the calling Javascript. If no serialized factory was passed in the request then we start a * new restoration. If a serialized factory was passed in the request then the restoration is resumed. For * this reason we should NEVER call AKFactory::nuke() in startRestore anymore: that would simply reset the * extraction engine configuration which was done in masterSetup() leading to an error about the file being * invalid (since no file is found). */ case 'startRestore': case 'stepRestore': $engine = AKFactory::getUnarchiver(); // Get the engine $observer = new RestorationObserver(); // Create a new observer $engine->attach($observer); // Attach the observer $engine->tick(); $ret = $engine->getStatusArray(); if ($ret['Error'] != '') { $retArray['status'] = false; $retArray['done'] = true; $retArray['message'] = $ret['Error']; } elseif (!$ret['HasRun']) { $retArray['files'] = $observer->filesProcessed; $retArray['bytesIn'] = $observer->compressedTotal; $retArray['bytesOut'] = $observer->uncompressedTotal; $retArray['status'] = true; $retArray['done'] = true; } else { $retArray['files'] = $observer->filesProcessed; $retArray['bytesIn'] = $observer->compressedTotal; $retArray['bytesOut'] = $observer->uncompressedTotal; $retArray['status'] = true; $retArray['done'] = false; $retArray['factory'] = AKFactory::serialize(); } break; case 'finalizeRestore': $root = AKFactory::get('kickstart.setup.destdir'); // Remove the installation directory recursive_remove_directory($root . '/installation'); $postproc = AKFactory::getPostProc(); /** * Should I rename the htaccess.bak and web.config.bak files back to their live filenames...? */ $renameFiles = AKFactory::get('kickstart.setup.postrenamefiles', true); if ($renameFiles) { // Rename htaccess.bak to .htaccess if (file_exists($root . '/htaccess.bak')) { if (file_exists($root . '/.htaccess')) { $postproc->unlink($root . '/.htaccess'); } $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 = KSROOTDIR; $basepath = rtrim(str_replace('\\', '/', $basepath), '/'); if (!empty($basepath)) { $basepath .= '/'; } $postproc->unlink($basepath . 'restoration.php'); // Import a custom finalisation file $filename = dirname(__FILE__) . '/restore_finalisation.php'; if (file_exists($filename)) { // We cannot use the Filesystem API here. if (ini_get('opcache.enable') && function_exists('opcache_invalidate') && (!ini_get('opcache.restrict_api') || stripos(realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')) === 0) ) { \opcache_invalidate($filename, true); } if (function_exists('apc_compile_file')) { \apc_compile_file($filename); } if (function_exists('wincache_refresh_if_changed')) { \wincache_refresh_if_changed(array($filename)); } if (function_exists('xcache_asm')) { xcache_asm($filename); } include_once $filename; } // Run a custom finalisation script if (function_exists('finalizeRestore')) { finalizeRestore($root, $basepath); } break; default: // Invalid task! $enabled = false; break; } } // Maybe we weren't authorized or the task was invalid? if (!$enabled) { // Maybe the user failed to enter any information $retArray['status'] = false; $retArray['message'] = AKText::_('ERR_INVALID_LOGIN'); } // JSON encode the message $json = json_encode($retArray); // Do I have to encrypt? $password = AKFactory::get('kickstart.security.password', null); if (!empty($password)) { $json = AKEncryptionAES::AESEncryptCtr($json, $password, 128); } // Return the message echo "###$json###"; } // ------------ lixlpixel recursive PHP functions ------------- // recursive_remove_directory( directory to delete, empty ) // expects path to directory and optional TRUE / FALSE to empty // of course PHP has to have the rights to delete the directory // you specify and all files and folders inside the directory // ------------------------------------------------------------ function recursive_remove_directory($directory) { // if the path has a slash at the end we remove it here if (substr($directory, -1) == '/') { $directory = substr($directory, 0, -1); } // if the path is not valid or is not a directory ... if (!file_exists($directory) || !is_dir($directory)) { // ... we return false and exit the function return false; // ... if the path is not readable } elseif (!is_readable($directory)) { // ... we return false and exit the function return false; // ... else if the path is readable } else { // we open the directory $handle = opendir($directory); $postproc = AKFactory::getPostProc(); // and scan through the items inside while (false !== ($item = readdir($handle))) { // if the filepointer is not the current directory // or the parent directory if ($item != '.' && $item != '..') { // we build the new path to delete $path = $directory . '/' . $item; // if the new path is a directory if (is_dir($path)) { // we call this function with the new path recursive_remove_directory($path); // if the new path is a file } else { // we remove the file $postproc->unlink($path); } } } // close the directory closedir($handle); // try to delete the now empty directory if (!$postproc->rmdir($directory)) { // return false if not possible return false; } // return success return true; } }