From ae0bcc80b0d84e75be770345b6b653c28d81e6b5 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Thu, 26 Aug 2021 14:56:55 +0300 Subject: [PATCH 01/58] ZIP extraction helper for Joomla Update --- administrator/components/com_admin/script.php | 9 + .../components/com_joomlaupdate/extract.php | 1670 ++++++ ...tore_finalisation.php => finalisation.php} | 112 +- .../com_joomlaupdate/joomlaupdate.xml | 4 +- .../components/com_joomlaupdate/restore.php | 5237 ----------------- .../src/Model/UpdateModel.php | 87 +- .../com_joomlaupdate/tmpl/update/default.php | 4 +- .../com_joomlaupdate/joomla.asset.json | 24 - .../js/admin-update-default.es6.js | 143 +- .../com_joomlaupdate/js/encryption.es5.js | 457 -- .../com_joomlaupdate/js/update.es5.js | 391 -- templates/system/build_incomplete.html | 2 +- templates/system/fatal-error.html | 2 +- 13 files changed, 1960 insertions(+), 6182 deletions(-) create mode 100644 administrator/components/com_joomlaupdate/extract.php rename administrator/components/com_joomlaupdate/{restore_finalisation.php => finalisation.php} (69%) delete mode 100644 administrator/components/com_joomlaupdate/restore.php delete mode 100644 build/media_source/com_joomlaupdate/js/encryption.es5.js delete mode 100644 build/media_source/com_joomlaupdate/js/update.es5.js diff --git a/administrator/components/com_admin/script.php b/administrator/components/com_admin/script.php index 3cc6edb4865d7..85d1a97fb80ff 100644 --- a/administrator/components/com_admin/script.php +++ b/administrator/components/com_admin/script.php @@ -6089,6 +6089,15 @@ public function deleteUnexistingFiles($dryRun = false, $suppressOutput = false) '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/NegotiatorTest.php', '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/TestCase.php', '/libraries/vendor/willdurand/negotiation/tests/bootstrap.php', + // 4.0.0 to 4.0.3 + '/administrator/components/com_joomlaupdate/restore.php', + '/administrator/components/com_joomlaupdate/restore_finalisation.php', + '/media/com_joomlaupdate/js/encryption.js', + '/media/com_joomlaupdate/js/encryption.min.js', + '/media/com_joomlaupdate/js/encryption.min.js.gz', + '/media/com_joomlaupdate/js/update.js', + '/media/com_joomlaupdate/js/update.min.js', + '/media/com_joomlaupdate/js/update.min.js.gz', ); $folders = array( diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php new file mode 100644 index 0000000000000..b1b6e2f0d5d34 --- /dev/null +++ b/administrator/components/com_joomlaupdate/extract.php @@ -0,0 +1,1670 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +define('_JOOMLA_UPDATE', 1); + +/** + * ZIP archive extraction class + * + * @since 4.0.3 + */ +class ZIPExtraction +{ + /** @var int How much data to read at once when processing files */ + private const CHUNK_SIZE = 524288; + + /** + * Maximum execution time (seconds). + * + * Each page load will take at most this much time. Please note that if the ZIP archive contains fairly large, + * compressed files we may overshoot this time since we can't interrupt the decompression. This should not be an + * issue in the context of updating Joomla as the ZIP archive contains fairly small files. + * + * If this is too low it will cause too many requests to hit the server, potentially triggering a DoS protection and + * causing the extraction to fail. If this is too big the extraction will not be as verbose and the user might think + * something is broken. A value between 3 and 7 seconds is, therefore, recommended. + * + * @var int + */ + private const MAX_EXEC_TIME = 4; + + /** + * Run-time execution bias (percentage points). + * + * We evaluate the time remaining on the timer before processing each file on the ZIP archive. If we have already + * consumed at least this much percentage of the MAX_EXEC_TIME we will stop processing the archive in this page + * load, return the result to the client and wait for it to call us again so we can resume the extraction. + * + * This becomes important when the MAX_EXEC_TIME is close the the PHP, PHP-FPM or Apache timeout oon the server + * (whichever is lowest) and there are fairly large files in the backup archive. If we start extracting a large, + * compressed file close to a hard server timeout it's possible that we will overshoot that hard timeout and see the + * extraction failing. + * + * Since Joomla Update is used to extract a ZIP archive with many small files we can keep at a fairly high 90% + * without much fear that something will break. + * + * Example: if MAX_EXEC_TIME is 10 seconds and RUNTIME_BIAS is 80 each page load will take between 80% and 100% of + * the MAX_EXEC_TIME, i.e. anywhere between 8 and 10 seconds. + * + * Lower values make it less overshooting MAX_EXEC_TIME when extracting large files. + * + * @var int + */ + private const RUNTIME_BIAS = 90; + + /** + * Minimum execution time (seconds). + * + * A request cannot take less than this many seconds. If it does, we add “dead time” (sleep) where the script does + * nothing except wait. This is essentially a rate limiting feature to avoid hitting a server-side DoS protection + * which could be triggered if we ended up sending too many requests in a limited amount of time. + * + * This should normally be less than MAX_EXEC * (RUNTIME_BIAS / 100). Values between that and MAX_EXEC_TIME have the + * effect of almost always adding dead time in each request, unless a really large file is being extracted from the + * ZIP archive. Values larger than MAX_EXEC will always add dead time to the request. This can be useful to + * artificially reduce the CPU usage limit. Some servers might kill the request if they see a sustained CPU usage + * spike over a short period of time. + * + * The chosen value of 3 seconds belongs to the first category, essentially making sure that we have a decent rate + * limiting without annoying the user too much but also without catering for the most badly configured of shared + * hosting. It's a happy medium which works for the majority (~90%) of commercial servers out there. + * + * @var int + */ + private const MIN_EXEC_TIME = 3; + + /** @var int Internal state when extracting files: we need to be initialised */ + private const AK_STATE_INITIALIZE = -1; + + /** @var int Internal state when extracting files: no file currently being extracted */ + private const AK_STATE_NOFILE = 0; + + /** @var int Internal state when extracting files: reading the file header */ + private const AK_STATE_HEADER = 1; + + /** @var int Internal state when extracting files: reading file data */ + private const AK_STATE_DATA = 2; + + /** @var int Internal state when extracting files: file data has been read thoroughly */ + private const AK_STATE_DATAREAD = 3; + + /** @var int Internal state when extracting files: post-processing the file */ + private const AK_STATE_POSTPROC = 4; + + /** @var int Internal state when extracting files: done with this file */ + private const AK_STATE_DONE = 5; + + /** @var int Internal state when extracting files: finished extracting the ZIP file */ + private const AK_STATE_FINISHED = 999; + + /** @var null|self Singleton isntance */ + private static $instance = null; + + /** @var integer The total size of the ZIP archive */ + public $totalSize = []; + + /** @var array Which files to skip */ + public $skipFiles = []; + + /** @var integer Current tally of compressed size read */ + public $compressedTotal = 0; + + /** @var integer Current tally of bytes written to disk */ + public $uncompressedTotal = 0; + + /** @var integer Current tally of files extracted */ + public $filesProcessed = 0; + + /** @var integer Maximum execution time allowance per step */ + private $maxExecTime = null; + + /** @var integer Timestamp of execution start */ + private $startTime = null; + + /** @var string|null The last error message */ + private $lastErrorMessage = null; + + /** @var string Archive filename */ + private $filename = null; + + /** @var boolean Current archive part number */ + private $archiveFileIsBeingRead = false; + + /** @var integer The offset inside the current part */ + private $currentOffset = 0; + + /** @var string Absolute path to prepend to extracted files */ + private $addPath = ''; + + /** @var resource File pointer to the current archive part file */ + private $fp = null; + + /** @var integer Run state when processing the current archive file */ + private $runState = self::AK_STATE_INITIALIZE; + + /** @var stdClass File header data, as read by the readFileHeader() method */ + private $fileHeader = null; + + /** @var integer How much of the uncompressed data we've read so far */ + private $dataReadLength = 0; + + /** @var array Unwritable files in these directories are always ignored and do not cause errors when not extracted */ + private $ignoreDirectories = []; + + /** @var boolean Internal flag, set when the ZIP file has a data descriptor (which we will be ignoring) */ + private $expectDataDescriptor = false; + + /** @var integer The UNIX last modification timestamp of the file last extracted */ + private $lastExtractedFileTimestamp = 0; + + /** @var string The file path of the file last extracted */ + private $lastExtractedFilename = null; + + /** + * Public constructor. + * + * Sets up the internal timer. + */ + public function __construct() + { + $this->setupMaxExecTime(); + + // Initialize start time + $this->startTime = microtime(true); + } + + /** + * Singleton implementation. + * + * @return static + */ + public static function getInstance(): self + { + if (is_null(self::$instance)) + { + self::$instance = new self; + } + + return self::$instance; + } + + /** + * Returns a serialised copy of the object. + * + * This is different to calling serialise() directly. This operates on a copy of the object which undergoes a + * call to shutdown() first so any open files are closed first. + * + * @return string The serialised data, potentially base64 encoded. + */ + public static function getSerialised(): string + { + $clone = clone self::getInstance(); + $clone->shutdown(); + $serialized = serialize($clone); + + return (function_exists('base64_encode') && function_exists('base64_decode')) ? base64_encode($serialized) : $serialized; + } + + /** + * Restores a serialised instance into the singleton implementation and returns it. + * + * If the serialised data is corrupt it will return null. + * + * @param string $serialised The serialised data, potentially base64 encoded, to deserialize. + * + * @return static|null The instance of the object, NULL if it cannot be deserialised. + */ + public static function unserialiseInstance(string $serialised): ?self + { + if (function_exists('base64_encode') && function_exists('base64_decode')) + { + $serialised = base64_decode($serialised); + } + + $instance = @unserialize($serialised, [ + 'allowed_classes' => [ + self::class, + stdClass::class, + ], + ] + ); + + if (($instance === false) || !is_object($instance) || !($instance instanceof self)) + { + return null; + } + + self::$instance = $instance; + + return self::$instance; + } + + /** + * Wakeup function, called whenever the class is deserialized. + * + * This method does the following: + * * Restart the timer. + * * Reopen the archive file, if one is defined. + * * Seek to the correct offset of the file. + * + * @return void + * @internal + */ + public function __wakeup(): void + { + // Reset the timer when deserializing the object. + $this->startTime = microtime(true); + + if (!$this->archiveFileIsBeingRead) + { + return; + } + + $this->fp = @fopen($this->filename, 'rb'); + + if ((is_resource($this->fp)) && ($this->currentOffset > 0)) + { + @fseek($this->fp, $this->currentOffset); + } + } + + /** + * Enforce the minimum execution time. + * + * @return void + */ + public function enforceMinimumExecutionTime() + { + $elapsed = $this->getRunningTime() * 1000; + $minExecTime = 1000.0 * min(1, (min(self::MIN_EXEC_TIME, $this->getPhpMaxExecTime()) - 1)); + + // Only run a sleep delay if we haven't reached the minimum execution time + if (($minExecTime <= $elapsed) || ($elapsed <= 0)) + { + return; + } + + $sleepMillisec = $minExecTime - $elapsed; + + /** + * If we need to sleep for more than 1 second we should be using sleep() or time_sleep_until() to prevent high + * CPU usage, also because some OS might not support sleeping for over 1 second using these functions. In all + * other cases we will try to use usleep or time_nanosleep instead. + */ + $longSleep = $sleepMillisec > 1000; + $miniSleepSupported = function_exists('usleep') || function_exists('time_nanosleep'); + + if (!$longSleep && $miniSleepSupported) + { + if (function_exists('usleep') && ($sleepMillisec < 1000)) + { + usleep(1000 * $sleepMillisec); + + return; + } + + if (function_exists('time_nanosleep') && ($sleepMillisec < 1000)) + { + time_nanosleep(0, 1000000 * $sleepMillisec); + + return; + } + } + + if (function_exists('sleep')) + { + sleep(ceil($sleepMillisec / 1000)); + + return; + } + + if (function_exists('time_sleep_until')) + { + time_sleep_until(time() + ceil($sleepMillisec / 1000)); + } + } + + /** + * Set the filepath to the ZIP archive which will be extracted. + * + * @param string $value The filepath to the archive. Only LOCAL files are allowed! + * + * @return void + */ + public function setFilename(string $value) + { + // Security check: disallow remote filenames + if (!empty($value) && strpos($value, '://') !== false) + { + $this->setError('Invalid archive location'); + + return; + } + + $this->filename = $value; + } + + /** + * Sets the path to prefix all extracted files with. Essentially, where the archive will be extracted to. + * + * @param string $addPath The path where the archive will be extracted. + * + * @return void + */ + public function setAddPath(string $addPath): void + { + $this->addPath = $addPath; + $this->addPath = str_replace('\\', '/', $this->addPath); + $this->addPath = rtrim($this->addPath, '/'); + + if (!empty($this->addPath)) + { + $this->addPath .= '/'; + } + } + + /** + * Set the list of files to skip when extracting the ZIP file. + * + * @param array $skipFiles A list of files to skip when extracting the ZIP archive + * + * @return void + */ + public function setSkipFiles(array $skipFiles): void + { + $this->skipFiles = array_values($skipFiles); + } + + /** + * Set the directories to skip over when extracting the ZIP archive + * + * @param array $ignoreDirectories The list of directories to ignore. + * + * @return void + */ + public function setIgnoreDirectories(array $ignoreDirectories): void + { + $this->ignoreDirectories = array_values($ignoreDirectories); + } + + /** + * Prepares for the archive extraction + * + * @return void + */ + public function initialize(): void + { + $this->totalSize = @filesize($this->filename) ?: 0; + $this->archiveFileIsBeingRead = false; + $this->currentOffset = 0; + $this->runState = self::AK_STATE_NOFILE; + + $this->readArchiveHeader(); + + if (!empty($this->getError())) + { + return; + } + + $this->runState = self::AK_STATE_NOFILE; + } + + /** + * Executes a step of the archive extraction + * + * @return boolean True if we are done extracting or an error occurred + */ + public function step(): bool + { + $status = true; + + while ($status && ($this->getTimeLeft() > 0)) + { + switch ($this->runState) + { + case self::AK_STATE_INITIALIZE: + $this->initialize(); + break; + + case self::AK_STATE_NOFILE: + $status = $this->readFileHeader(); + + if ($status) + { + // Update running tallies when we start extracting a file + $this->filesProcessed++; + $this->compressedTotal += array_key_exists('compressed', get_object_vars($this->fileHeader)) + ? $this->fileHeader->compressed : 0; + $this->uncompressedTotal += $this->fileHeader->uncompressed; + } + + break; + + case self::AK_STATE_HEADER: + case self::AK_STATE_DATA: + $status = $this->processFileData(); + break; + + case self::AK_STATE_DATAREAD: + case self::AK_STATE_POSTPROC: + $this->setLastExtractedFileTimestamp($this->fileHeader->timestamp); + $this->processLastExtractedFile(); + + $status = true; + $this->runState = self::AK_STATE_DONE; + break; + + case self::AK_STATE_DONE: + default: + $this->runState = self::AK_STATE_NOFILE; + + break; + + case self::AK_STATE_FINISHED: + $status = false; + break; + } + } + + $error = $this->getError() ?? null; + + // Did we just finish or run into an error? + if (!empty($error) || $this->runState === self::AK_STATE_FINISHED) + { + // Reset internal state, prevents __wakeup from trying to open a non-existent file + $this->archiveFileIsBeingRead = false; + + return true; + } + + return false; + } + + /** + * Get the most recent error message + * + * @return string|null The message string, null if there's no error + */ + public function getError(): ?string + { + return $this->lastErrorMessage; + } + + /** + * Gets the number of seconds left, before we hit the "must break" threshold + * + * @return float + */ + private function getTimeLeft(): float + { + return $this->maxExecTime - $this->getRunningTime(); + } + + /** + * Gets the time elapsed since object creation/unserialization, effectively how + * long Akeeba Engine has been processing data + * + * @return float + */ + private function getRunningTime(): float + { + return microtime(true) - $this->startTime; + } + + /** + * Process the last extracted file or directory + * + * This invalidates OPcache for .php files. Also applies the correct permissions and timestamp. + * + * @return void + */ + private function processLastExtractedFile(): void + { + if (@is_file($this->lastExtractedFilename)) + { + @chmod($this->lastExtractedFilename, 0644); + + clearFileInOPCache($this->lastExtractedFilename, true); + } + else + { + @chmod($this->lastExtractedFilename, 0755); + } + + if ($this->lastExtractedFileTimestamp > 0) + { + @touch($this->lastExtractedFilename, $this->lastExtractedFileTimestamp); + } + } + + /** + * Set the last extracted filename + * + * @param string|null $lastExtractedFilename The last extracted filename + * + * @return void + */ + private function setLastExtractedFilename(?string $lastExtractedFilename): void + { + $this->lastExtractedFilename = $lastExtractedFilename; + } + + /** + * Set the last modification UNIX timestamp for the last extracted file + * + * @param int $lastExtractedFileTimestamp The timestamp + * + * @return void + */ + private function setLastExtractedFileTimestamp(int $lastExtractedFileTimestamp): void + { + $this->lastExtractedFileTimestamp = $lastExtractedFileTimestamp; + } + + /** + * Sleep function, called whenever the class is serialized + * + * @return void + * @internal + */ + private function shutdown(): void + { + if (!is_resource($this->fp)) + { + return; + } + + $this->currentOffset = @ftell($this->fp); + + @fclose($this->fp); + } + + /** + * Unicode-safe binary data length + * + * @param string|null $string The binary data to get the length for + * + * @return integer + */ + private function binStringLength(?string $string): int + { + if (is_null($string)) + { + return 0; + } + + if (function_exists('mb_strlen')) + { + return mb_strlen($string, '8bit') ?: 0; + } + + return strlen($string) ?: 0; + } + + /** + * Add an error message + * + * @param string $error Error message + * + * @return void + */ + private function setError(string $error): void + { + $this->lastErrorMessage = $error; + } + + /** + * Reads data from the archive. + * + * @param resource $fp The file pointer to read data from + * @param int|null $length The volume of data to read, in bytes + * + * @return string The data read from the file + */ + private function fread($fp, ?int $length = null): string + { + $readLength = (is_numeric($length) && ($length > 0)) ? $length : PHP_INT_MAX; + $data = fread($fp, $readLength); + + if ($data === false) + { + $data = ''; + } + + return $data; + } + + /** + * Read the header of the archive, making sure it's a valid ZIP file. + * + * @return void + */ + private function readArchiveHeader(): void + { + // Open the first part + $this->openArchiveFile(); + + // Fail for unreadable files + if ($this->fp === false) + { + return; + } + + // Read the header data. + $sigBinary = fread($this->fp, 4); + $headerData = unpack('Vsig', $sigBinary); + + // We only support single part ZIP files + if ($headerData['sig'] != 0x04034b50) + { + $this->setError('The archive file is corrupt: bad header'); + + return; + } + + // Roll back the file pointer + fseek($this->fp, -4, SEEK_CUR); + + $this->currentOffset = @ftell($this->fp); + $this->dataReadLength = 0; + + } + + /** + * Concrete classes must use this method to read the file header + * + * @return boolean True if reading the file was successful, false if an error occurred or we reached end of archive + */ + private function readFileHeader(): bool + { + if (!is_resource($this->fp)) + { + return false; + } + + // Unexpected end of file + if ($this->isEOF()) + { + $this->setError('The archive is corrupt or truncated'); + + return false; + } + + $this->currentOffset = 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); + $readLength = ($junk['sig'] == 0x08074b50) ? 12 : 8; + $junk = @fread($this->fp, $readLength); + + // And check for EOF, too + if ($this->isEOF()) + { + $this->setError('The archive is corrupt or truncated'); + + return false; + } + } + + // 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)) + { + // The signature is not the one used for files. Is this a central directory record (i.e. we're done)? + if ($headerData['sig'] == 0x02014b50) + { + // End of ZIP file detected. We'll just skip to the end of file... + @fseek($this->fp, 0, SEEK_END); + $this->runState = self::AK_STATE_FINISHED; + + return false; + } + + $this->setError('The archive file is corrupt or truncated'); + + 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) + { + $vHour = ($lastmodtime & 0xF800) >> 11; + $vMInute = ($lastmodtime & 0x07E0) >> 5; + $vSeconds = ($lastmodtime & 0x001F) * 2; + $vYear = (($lastmoddate & 0xFE00) >> 9) + 1980; + $vMonth = ($lastmoddate & 0x01E0) >> 5; + $vDay = $lastmoddate & 0x001F; + + $this->fileHeader->timestamp = @mktime($vHour, $vMInute, $vSeconds, $vMonth, $vDay, $vYear); + } + + $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); + + // Read extra field if present + if ($extraFieldLength > 0) + { + $extrafield = fread($this->fp, $extraFieldLength); + } + + // 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; + default: + $messageTemplate = 'This script cannot handle ZIP compression method %d. ' + . 'Only 0 (no compression) and 8 (DEFLATE, gzip) can be handled.'; + $actualMessage = sprintf($messageTemplate, $headerData['compmethod']); + $this->setError($actualMessage); + + return false; + 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) && 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->filename); + $curPos = @ftell($this->fp); + $canSeek = $curSize - $curPos; + $canSeek = ($canSeek > $seekleft) ? $seekleft : $canSeek; + @fseek($this->fp, $canSeek, SEEK_CUR); + $seekleft -= $canSeek; + + if ($seekleft) + { + $this->setError('The archive is corrupt or truncated'); + + return false; + } + } + + $this->currentOffset = @ftell($this->fp); + $this->runState = self::AK_STATE_DONE; + + return true; + } + + // Last chance to prepend a path to the filename + if (!empty($this->addPath)) + { + $this->fileHeader->file = $this->addPath . $this->fileHeader->file; + } + + // Get the translated path name + if ($this->fileHeader->type == 'file') + { + $this->fileHeader->realFile = $this->fileHeader->file; + $this->setLastExtractedFilename($this->fileHeader->file); + } + elseif ($this->fileHeader->type == 'dir') + { + $this->fileHeader->timestamp = 0; + + $dir = $this->fileHeader->file; + + if (!@is_dir($dir)) + { + mkdir($dir, 0755, true); + } + + $this->setLastExtractedFilename(null); + } + else + { + // Symlink; do not post-process + $this->fileHeader->timestamp = 0; + $this->setLastExtractedFilename(null); + } + + $this->createDirectory(); + + // Header is read + $this->runState = self::AK_STATE_HEADER; + + return true; + } + + /** + * Creates the directory this file points to + * + * @return void + */ + private function createDirectory(): void + { + // 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 = 0755; + $ignore = $this->isIgnoredDirectory($dirName); + + if (@is_dir($dirName)) + { + return; + } + + if ((@mkdir($dirName, $perms, true) === false) && (!$ignore)) + { + $this->setError(sprintf('Could not create %s folder', $dirName)); + } + + } + + /** + * Concrete classes must use this method to process file data. It must set $runState to self::AK_STATE_DATAREAD when + * it's finished processing the file data. + * + * @return boolean True if processing the file data was successful, false if an error occurred + */ + private function processFileData(): bool + { + 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->processTypeFileCompressed(); + break; + + case 'default': + $this->setError(sprintf('Unknown compression type %s.', $this->fileHeader->compression)); + + return false; + break; + } + break; + } + + $this->setError(sprintf('Unknown entry type %s.', $this->fileHeader->type)); + + return false; + } + + /** + * Opens the next part file for reading + * + * @return void + */ + private function openArchiveFile(): void + { + if ($this->archiveFileIsBeingRead) + { + return; + } + + if (is_resource($this->fp)) + { + @fclose($this->fp); + } + + $this->fp = @fopen($this->filename, 'rb'); + + if ($this->fp === false) + { + $message = 'Could not open archive 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.'; + $this->setError($message); + + return; + } + + fseek($this->fp, 0); + $this->currentOffset = 0; + + } + + /** + * Returns true if we have reached the end of file + * + * @return boolean True if we have reached End Of File + */ + private function isEOF(): bool + { + /** + * feof() will return false if the file pointer is exactly at the last byte of the file. However, this is a + * condition we want to treat as a proper EOF for the purpose of extracting a ZIP file. Hence the second part + * after the logical OR. + */ + return @feof($this->fp) || (@ftell($this->fp) > @filesize($this->filename)); + } + + /** + * Handles the permissions of the parent directory to a file and the file itself to make it writeable. + * + * @param string $path A path to a file + * + * @return void + */ + private function setCorrectPermissions(string $path): void + { + static $rootDir = null; + + if (is_null($rootDir)) + { + $rootDir = rtrim($this->addPath, '/\\'); + } + + $directory = rtrim(dirname($path), '/\\'); + + // Is this an unwritable directory? + if (($directory != $rootDir) && !is_writeable($directory)) + { + @chmod($directory, 0755); + } + + @chmod($path, 0644); + } + + /** + * 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 + */ + private function isIgnoredDirectory(string $shortFilename): bool + { + $check = substr($shortFilename, -1) == '/' ? rtrim($shortFilename, '/') : dirname($shortFilename); + + return in_array($check, $this->ignoreDirectories); + } + + /** + * Process the file data of a directory entry + * + * @return boolean + */ + private function processTypeDir(): bool + { + // Directory entries in the JPA do not have file data, therefore we're done processing the entry + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + /** + * Process the file data of a link entry + * + * @return boolean + */ + private function processTypeLink(): bool + { + $toReadBytes = 0; + $leftBytes = $this->fileHeader->compressed; + $data = ''; + + while ($leftBytes > 0) + { + $toReadBytes = min($leftBytes, self::CHUNK_SIZE); + $mydata = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = $this->binStringLength($mydata); + $data .= $mydata; + $leftBytes -= $reallyReadBytes; + + if ($reallyReadBytes < $toReadBytes) + { + // We read less than requested! + if ($this->isEOF(true) && !$this->isEOF(false)) + { + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + } + + $filename = isset($this->fileHeader->realFile) ? $this->fileHeader->realFile : $this->fileHeader->file; + + // 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 + @symlink($data, $filename); + + $this->runState = self::AK_STATE_DATAREAD; + + // No matter if the link was created! + return true; + } + + /** + * Processes an uncompressed (stored) file + * + * @return boolean + */ + private function processTypeFileUncompressed(): bool + { + // Uncompressed files are being processed in small chunks, to avoid timeouts + if ($this->dataReadLength == 0) + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + } + + // Open the output file + $ignore = $this->isIgnoredDirectory($this->fileHeader->file); + + $writeMode = ($this->dataReadLength == 0) ? 'wb' : 'ab'; + $outfp = @fopen($this->fileHeader->realFile, $writeMode); + + // Can we write to the file? + if (($outfp === false) && (!$ignore)) + { + // An error occurred + $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); + + return false; + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) + { + // No file data! + if (is_resource($outfp)) + { + @fclose($outfp); + } + + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + $leftBytes = $this->fileHeader->compressed - $this->dataReadLength; + + // Loop while there's data to read and enough time to do it + while (($leftBytes > 0) && ($this->getTimeLeft() > 0)) + { + $toReadBytes = min($leftBytes, self::CHUNK_SIZE); + $data = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = $this->binStringLength($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)) + { + // Nope. The archive is corrupt + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + + if (is_resource($outfp)) + { + @fwrite($outfp, $data); + } + } + + // Close the file pointer + if (is_resource($outfp)) + { + @fclose($outfp); + } + + // Was this a pre-timeout bail out? + if ($leftBytes > 0) + { + $this->runState = self::AK_STATE_DATA; + + return true; + } + + // Oh! We just finished! + $this->runState = self::AK_STATE_DATAREAD; + $this->dataReadLength = 0; + + return true; + } + + /** + * Processes a compressed file + * + * @return boolean + */ + private function processTypeFileCompressed(): bool + { + // 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 = $this->isIgnoredDirectory($this->fileHeader->file); + + if (($outfp === false) && (!$ignore)) + { + // An error occurred + $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); + + return false; + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) + { + // No file data! + if (is_resource($outfp)) + { + @fclose($outfp); + } + + $this->runState = self::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 ($this->binStringLength($zipData) < $this->fileHeader->compressed) + { + // End of local file before reading all data? + if ($this->isEOF()) + { + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + + switch ($this->fileHeader->compression) + { + case 'gzip': + /** @noinspection PhpComposerExtensionStubsInspection */ + $unzipData = gzinflate($zipData); + break; + + case 'bzip2': + /** @noinspection PhpComposerExtensionStubsInspection */ + $unzipData = bzdecompress($zipData); + break; + + default: + $this->setError(sprintf('Unknown compression method %s', $this->fileHeader->compression)); + + return false; + break; + } + + unset($zipData); + + // Write to the file. + if (is_resource($outfp)) + { + @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed); + @fclose($outfp); + } + + unset($unzipData); + + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + /** + * Set up the maximum execution time + * + * @return void + */ + private function setupMaxExecTime(): void + { + $configMaxTime = self::MAX_EXEC_TIME; + $bias = self::RUNTIME_BIAS / 100; + $this->maxExecTime = min($this->getPhpMaxExecTime(), $configMaxTime) * $bias; + } + + /** + * Get the PHP maximum execution time. + * + * If it's not defined or it's zero (infinite) we use a fake value of 10 seconds. + * + * @return integer + */ + private function getPhpMaxExecTime(): int + { + if (!@function_exists('ini_get')) + { + return 10; + } + + $phpMaxTime = @ini_get("maximum_execution_time"); + $phpMaxTime = (!is_numeric($phpMaxTime) ? 10 : @intval($phpMaxTime)) ?: 10; + + return max(1, $phpMaxTime); + } +} + +// Skip over the mini-controller for testing purposes +if (defined('_JOOMLA_UPDATE_TESTING')) +{ + return; +} + +/** + * Invalidate a file in OPcache. + * + * Only applies if the file has a .php extension. + * + * @param string $file The filepath to clear from OPcache + * + * @return boolean + */ +function clearFileInOPCache(string $file): bool +{ + static $hasOpCache = null; + + if (is_null($hasOpCache)) + { + $hasOpCache = ini_get('opcache.enable') + && function_exists('opcache_invalidate') + && (!ini_get('opcache.restrict_api') || stripos(realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')) === 0); + } + + if ($hasOpCache && (strtolower(substr($file, -4)) === '.php')) + { + return \opcache_invalidate($file, true); + } + + return false; +} + +/** + * Recursively remove directory. + * + * Used by the finalization script provided with Joomla Update. + * + * @param string $directory The directory to remove + * + * @return boolean + */ +function recursiveRemoveDirectory($directory) +{ + if (substr($directory, -1) == '/') + { + $directory = substr($directory, 0, -1); + } + + if (!@file_exists($directory) || !@is_dir($directory) || !is_readable($directory)) + { + return false; + } + + $di = new DirectoryIterator($directory); + + /** @var DirectoryIterator $item */ + foreach ($di as $item) + { + if ($item->isDot()) + { + continue; + } + + if ($item->isDir()) + { + $status = recursive_remove_directory($item->getPathname()); + + if (!$status) + { + return false; + } + + continue; + } + + @unlink($item->getPathname()); + + clearFileInOPCache($item->getPathname()); + } + + return @rmdir($directory); +} + +/** + * A timing safe equals comparison. + * + * Uses the built-in hash_equals() method if it exists. It SHOULD exist, as it's available since PHP 5.6 whereas even + * Joomla 4.0 requires PHP 7.2 or later. If for any reason the built-in function is not available (for example, a host + * has disabled it because they do not understand the first thing about security) we will fall back to a safe, userland + * implementation. + * + * @param string $known The known value to check against + * @param string $user The user submitted value to check + * + * @return boolean True if the two strings are identical. + * + * @see http://blog.ircmaxell.com/2014/11/its-all-about-time.html + */ +function timingSafeEquals($known, $user) +{ + if (function_exists('hash_equals')) + { + return hash_equals($known, $user); + } + + $safeLen = strlen($known); + $userLen = strlen($user); + + if ($userLen != $safeLen) + { + return false; + } + + $result = 0; + + for ($i = 0; $i < $userLen; $i++) + { + $result |= (ord($known[$i]) ^ ord($user[$i])); + } + + // They are only identical strings if $result is exactly 0... + return $result === 0; +} + +/** + * Gets the configuration parameters from the update.php file and validates the password sent with the request. + * + * @return array|null The configuration parameters to use. NULL if this is an invalid request. + */ +function getConfiguration(): ?array +{ + // Make sure the locale is correct for basename() to work + if (function_exists('setlocale')) + { + @setlocale(LC_ALL, 'en_US.UTF8'); + } + + // Require update.php or fail + $setupFile = __DIR__ . '/update.php'; + + if (!file_exists($setupFile)) + { + return null; + } + + /** + * If the setup file was created more than 1.5 hours ago we can assume that it's stale and someone forgot to + * remove it from the server. + * + * This prevents brute force attacks against the randomly generated password. Even a simple 8 character simple + * alphanum (a-z, 0-9) password yields over 2.8e12 permutation. Assuming a very fast server which can + * serve 100 requests to extract.php per second and an easy to attack password requiring going over just 1% of + * the search space it'd still take over 282 million seconds to brute force it. Our limit is more than 4 orders + * of magnitude lower than this best practical case scenario, giving us adequate protection against all but the + * luckiest attacker (spoiler alert: the mathematics of probabilities say you're not gonna get too lucky). + * + * It is still advisable to remove the update.php file once you are done with the extraction. This check + * here is only meant as a failsafe in case of a server error during the extraction and subsequent lack of user + * action to remove the update.php file from their server. + */ + clearstatcache(true); + $setupFileCreationTime = filectime($setupFile); + + if (abs(time() - $setupFileCreationTime) > 5400) + { + return null; + } + + // Load update.php. It pulls a variable named $restoration_setup into the local scope. + clearFileInOPCache($setupFile); + + require_once $setupFile; + + /** @var array $extractionSetup */ + + // The file exists but no configuration is present? + if (empty($extractionSetup ?? null) || !is_array($extractionSetup)) + { + return null; + } + + /** + * Immediately reject any attempt to run extract.php without a password. + * + * Doing that is a GRAVE SECURITY RISK. It makes it trivial to hack a site. Therefore we are preventing this script + * to run without a password. + */ + $password = $extractionSetup['security.password'] ?? null; + $userPassword = $_REQUEST['password'] ?? ''; + $userPassword = !is_string($userPassword) ? '' : trim($userPassword); + + if (empty($password) || !is_string($password) || (trim($password) == '') || (strlen(trim($password)) < 32)) + { + return null; + } + + // Timing-safe password comparison. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html + if (!timingSafeEquals($password, $userPassword)) + { + return null; + } + + // An "instance" variable will resume the engine from the serialised instance + $serialized = $_REQUEST['instance'] ?? null; + + if (!is_null($serialized) && empty(ZIPExtraction::unserialiseInstance($serialized))) + { + // The serialised instance is corrupt or someone tries to trick us. YOU SHALL NOT PASS! + return null; + } + + return $extractionSetup; +} + +// Import configuration +$retArray = [ + 'status' => true, + 'message' => null, +]; + +$configuration = getConfiguration(); +$enabled = !empty($configuration); + +if ($enabled) +{ + $sourcePath = $configuration['setup.sourcepath'] ?? ''; + $sourceFile = $configuration['setup.sourcefile'] ?? ''; + $destDir = ($configuration['setup.destdir'] ?? null) ?: __DIR__; + $basePath = rtrim(str_replace('\\', '/', __DIR__), '/'); + $basePath = empty($basePath) ? $basePath : ($basePath . '/'); + $sourceFile = (empty($sourceFile) ? '' : (rtrim($sourcePath, '/\\') . '/')) . $sourceFile; + $engine = ZIPExtraction::getInstance(); + + $engine->setFilename($sourceFile); + $engine->setAddPath($destDir); + $engine->setSkipFiles([ + 'administrator/components/com_joomlaupdate/restoration.php', + 'administrator/components/com_joomlaupdate/update.php', + ] + ); + $engine->setIgnoreDirectories([ + 'tmp', 'administrator/logs', + ] + ); + + $task = $_REQUEST['task'] ?? null; + + switch ($task) + { + case 'startExtract': + case 'stepExtract': + $done = $engine->step(); + $error = $engine->getError(); + + if ($error != '') + { + $retArray['status'] = false; + $retArray['done'] = true; + $retArray['message'] = $error; + } + elseif ($done) + { + $retArray['files'] = $engine->filesProcessed; + $retArray['bytesIn'] = $engine->compressedTotal; + $retArray['bytesOut'] = $engine->uncompressedTotal; + $retArray['status'] = true; + $retArray['done'] = true; + } + else + { + $retArray['files'] = $engine->filesProcessed; + $retArray['bytesIn'] = $engine->compressedTotal; + $retArray['bytesOut'] = $engine->uncompressedTotal; + $retArray['status'] = true; + $retArray['done'] = false; + $retArray['instance'] = ZIPExtraction::getSerialised(); + } + + $engine->enforceMinimumExecutionTime(); + + break; + + case 'finalizeUpdate': + $root = $configuration['setup.destdir'] ?? ''; + + // Remove update.php + @unlink($basePath . 'update.php'); + + // Import a custom finalisation file + $filename = dirname(__FILE__) . '/finalisation.php'; + + if (file_exists($filename)) + { + clearFileInOPCache($filename); + + include_once $filename; + } + + // Run a custom finalisation script + if (function_exists('finalizeUpdate')) + { + finalizeUpdate($root, $basePath); + } + + $engine->enforceMinimumExecutionTime(); + + break; + + default: + // Invalid task! + $enabled = false; + break; + } +} + +// This could happen even if $enabled was true, e.g. if we were asked for an invalid task. +if (!$enabled) +{ + // Maybe we weren't authorized or the task was invalid? + $retArray['status'] = false; + $retArray['message'] = 'Invalid login'; +} + +// JSON encode the message +echo json_encode($retArray); diff --git a/administrator/components/com_joomlaupdate/restore_finalisation.php b/administrator/components/com_joomlaupdate/finalisation.php similarity index 69% rename from administrator/components/com_joomlaupdate/restore_finalisation.php rename to administrator/components/com_joomlaupdate/finalisation.php index 2d56a6155e2b5..145d1be7a156c 100644 --- a/administrator/components/com_joomlaupdate/restore_finalisation.php +++ b/administrator/components/com_joomlaupdate/finalisation.php @@ -14,7 +14,7 @@ namespace { // Require the restoration environment or fail cold. Prevents direct web access. - \defined('_AKEEBA_RESTORATION') or die(); + \defined('_JOOMLA_UPDATE') or die(); // Fake a miniature Joomla environment if (!\defined('_JEXEC')) @@ -25,34 +25,35 @@ if (!function_exists('jimport')) { /** - * We don't use it but the post-update script is using it anyway, so LET'S FAKE IT! + * This is deprecated but it may still be used in the update finalisation script. * - * @param string $path A dot syntax path. - * @param string $base Search this directory for the class. + * @param string $path Ignored. + * @param string $base Ignored. * - * @return boolean True on success. + * @return boolean Always true. * * @since 1.7.0 */ function jimport($path, $base = null) { // Do nothing + return true; } } - if (!function_exists('finalizeRestore')) + if (!function_exists('finalizeUpdate')) { /** * Run part of the Joomla! finalisation script, namely the part that cleans up unused files/folders * * @param string $siteRoot The root to the Joomla! site - * @param string $restorePath The base path to restore.php + * @param string $restorePath The base path to extract.php * * @return void * * @since 3.5.1 */ - function finalizeRestore($siteRoot, $restorePath) + function finalizeUpdate($siteRoot, $restorePath) { if (!\defined('JPATH_ROOT')) { @@ -80,11 +81,11 @@ function finalizeRestore($siteRoot, $restorePath) namespace Joomla\CMS\Filesystem { - // Fake the JFile class, mapping it to Restore's post-processing class + // Fake the File class if (!class_exists('\Joomla\CMS\Filesystem\File')) { /** - * JFile mock class proxying behaviour in the post-upgrade script to that of either native PHP or restore.php + * JFile mock class * * @since 3.5.1 */ @@ -99,13 +100,13 @@ abstract class File * * @since 3.5.1 */ - public static function exists($fileName) + public static function exists(string $fileName): bool { return @file_exists($fileName); } /** - * Proxies deleting a file to the restore.php version + * Delete a file and invalidate the PHP OPcache * * @param string $fileName The path to the file to be deleted * @@ -113,14 +114,20 @@ public static function exists($fileName) * * @since 3.5.1 */ - public static function delete($fileName) + public static function delete(string $fileName): bool { - /** @var \AKPostprocDirect $postproc */ - $postproc = \AKFactory::getPostProc(); - $postproc->unlink($fileName); + $result = @unlink($fileName); + + if ($result) + { + self::invalidateFileCache($fileName); + } + + return $result; } + /** - * Proxies moving a file to the restore.php version + * Rename a file and invalidate the PHP OPcache * * @param string $src The path to the source file * @param string $dest The path to the destination file @@ -129,11 +136,17 @@ public static function delete($fileName) * * @since 4.0.1 */ - public static function move($src, $dest) + public static function move(string $src, string $dest): bool { - /** @var \AKPostprocDirect $postproc */ - $postproc = \AKFactory::getPostProc(); - $postproc->rename($src, $dest); + $result = @rename($src, $dest); + + if ($result) + { + self::invalidateFileCache($src); + self::invalidateFileCache($dest); + } + + return $result; } /** @@ -150,13 +163,8 @@ public static function move($src, $dest) */ public static function invalidateFileCache($filepath, $force = true) { - if ('.php' === strtolower(substr($filepath, -4))) - { - $postproc = \AKFactory::getPostProc(); - $postproc->clearFileInOPCache($filepath); - } - - return false; + /** @noinspection PhpUndefinedFunctionInspection */ + return \clearFileInOPCache($filepath); } } @@ -166,7 +174,7 @@ public static function invalidateFileCache($filepath, $force = true) if (!class_exists('\Joomla\CMS\Filesystem\Folder')) { /** - * Folder mock class proxying behaviour in the post-upgrade script to that of either native PHP or restore.php + * Folder mock class * * @since 3.5.1 */ @@ -187,17 +195,55 @@ public static function exists($folderName) } /** - * Proxies deleting a folder to the restore.php version + * Delete a folder recursively and invalidate the PHP OPcache * * @param string $folderName The path to the folder to be deleted * - * @return void + * @return boolean * * @since 3.5.1 */ public static function delete($folderName) { - recursive_remove_directory($folderName); + if (substr($folderName, -1) == '/') + { + $folderName = substr($folderName, 0, -1); + } + + if (!@file_exists($folderName) || !@is_dir($folderName) || !is_readable($folderName)) + { + return false; + } + + $di = new \DirectoryIterator($folderName); + + /** @var \DirectoryIterator $item */ + foreach ($di as $item) + { + if ($item->isDot()) + { + continue; + } + + if ($item->isDir()) + { + $status = self::delete($item->getPathname()); + + if (!$status) + { + return false; + } + + continue; + } + + @unlink($item->getPathname()); + + /** @noinspection PhpUndefinedFunctionInspection */ + \clearFileInOPCache($item->getPathname()); + } + + return @rmdir($folderName); } } } @@ -209,7 +255,7 @@ public static function delete($folderName) if (!class_exists('\Joomla\CMS\Language\Text')) { /** - * Text mock class proxying behaviour in the post-upgrade script to that of either native PHP or restore.php + * Text mock class * * @since 3.5.1 */ diff --git a/administrator/components/com_joomlaupdate/joomlaupdate.xml b/administrator/components/com_joomlaupdate/joomlaupdate.xml index cfe6be554b2d5..8f2efca0b1423 100644 --- a/administrator/components/com_joomlaupdate/joomlaupdate.xml +++ b/administrator/components/com_joomlaupdate/joomlaupdate.xml @@ -16,8 +16,8 @@ config.xml - restore.php - restore_finalisation.php + extract.php + finalisation.php services src tmpl diff --git a/administrator/components/com_joomlaupdate/restore.php b/administrator/components/com_joomlaupdate/restore.php deleted file mode 100644 index d0c0d777579bc..0000000000000 --- a/administrator/components/com_joomlaupdate/restore.php +++ /dev/null @@ -1,5237 +0,0 @@ -|'), - 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(); - - /** - * 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() - { - 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() - { - // 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; - } -} diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index e345937a36671..b895e730d116b 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -515,9 +515,18 @@ protected function downloadPackage($url, $target) return basename($target); } + public function createRestorationFile($basename = null) + { + return $this->createUpdateFile($basename); + } + /** - * Create restoration file and trigger onJoomlaBeforeUpdate event, which find the updated core files - * which have changed during the update, where there are override for. + * Create the update.php file and trigger onJoomlaBeforeUpdate event. + * + * The onJoomlaBeforeUpdate event stores the core files for which overrides have been defined. + * This will be compared in the onJoomlaAfterUpdate event with the current filesystem state, + * thereby determining how many and which overrides need to be checked and possibly updated + * after Joomla installed an update. * * @param string $basename Optional base path to the file. * @@ -525,7 +534,7 @@ protected function downloadPackage($url, $target) * * @since 2.5.4 */ - public function createRestorationFile($basename = null) + public function createUpdateFile($basename = null) { // Load overrides plugin. PluginHelper::importPlugin('installer'); @@ -557,31 +566,26 @@ public function createRestorationFile($basename = null) $app->setUserState('com_joomlaupdate.password', $password); $app->setUserState('com_joomlaupdate.filesize', $filesize); - $data = " '$password', - 'kickstart.tuning.max_exec_time' => '5', - 'kickstart.tuning.run_time_bias' => '75', - 'kickstart.tuning.min_exec_time' => '0', - 'kickstart.procengine' => 'direct', - 'kickstart.setup.sourcefile' => '$file', - 'kickstart.setup.destdir' => '$siteroot', - 'kickstart.setup.restoreperms' => '0', - 'kickstart.setup.filetype' => 'zip', - 'kickstart.setup.dryrun' => '0', - 'kickstart.setup.renamefiles' => array(), - 'kickstart.setup.postrenamefiles' => false + 'security.password' => '$password', + 'setup.sourcefile' => '$file', + 'setup.destdir' => '$siteroot', ENDDATA; - $data .= ');'; + $data .= '];'; // Remove the old file, if it's there... - $configpath = JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php'; + $configpath = JPATH_COMPONENT_ADMINISTRATOR . '/update.php'; if (File::exists($configpath)) { - File::delete($configpath); + if (!File::delete($configpath)) + { + @unlink($configpath); + File::invalidateFileCache($configpath); + } } // Write new file. First try with File. @@ -621,9 +625,14 @@ public function createRestorationFile($basename = null) } /** - * Runs the schema update SQL files, the PHP update script and updates the - * manifest cache and #__extensions entry. Essentially, it is identical to - * InstallerFile::install() without the file copy. + * Finalise the upgrade. + * + * This method will do the following: + * * Run the schema update SQL files. + * * Run the Joomla post-update script. + * * Update the manifest cache and #__extensions entry for Joomla itself. + * + * It performs essentially the same function as InstallerFile::install() without the file copy. * * @return boolean True on success. * @@ -842,8 +851,12 @@ public function finaliseUpgrade() } /** - * Removes the extracted package file and trigger onJoomlaAfterUpdate event, which find the updated core files - * which have changed during the update, where there are override for. + * Removes the extracted package file and trigger onJoomlaAfterUpdate event. + * + * The onJoomlaAfterUpdate event compares the stored list of files previously overridden with + * the updated core files, finding out which ones have changed during the update, thereby + * determining how many and which override files need to be checked and possibly updated after + * the Joomla update. * * @return void * @@ -865,11 +878,29 @@ public function cleanUp() $file = $app->getUserState('com_joomlaupdate.file', null); File::delete($tempdir . '/' . $file); - // Remove the restoration.php file. - File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php'); + // Remove the update.php file used in Joomla 4.0.3 and later. + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/update.php')) + { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/update.php'); + } + + // Remove the legacy restoration.php file (when updating from Joomla 4.0.2 and earlier). + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php')) + { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php'); + } + + // Remove the legacy restoration_finalisation.php file used in Joomla 4.0.2 and earlier. + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restoration_finalisation.php')) + { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restoration_finalisation.php'); + } // Remove joomla.xml from the site's root. - File::delete(JPATH_ROOT . '/joomla.xml'); + if (File::exists(JPATH_ROOT . '/joomla.xml')) + { + File::delete(JPATH_ROOT . '/joomla.xml'); + } // Unset the update filename from the session. $app = Factory::getApplication(); diff --git a/administrator/components/com_joomlaupdate/tmpl/update/default.php b/administrator/components/com_joomlaupdate/tmpl/update/default.php index f1aaf9b569eb7..e9a1abbe052b1 100644 --- a/administrator/components/com_joomlaupdate/tmpl/update/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/update/default.php @@ -17,13 +17,11 @@ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') ->useScript('jquery') - ->useScript('com_joomlaupdate.encryption') - ->useScript('com_joomlaupdate.update') ->useScript('com_joomlaupdate.admin-update'); $password = Factory::getApplication()->getUserState('com_joomlaupdate.password', null); $filesize = Factory::getApplication()->getUserState('com_joomlaupdate.filesize', null); -$ajaxUrl = Uri::base() . 'components/com_joomlaupdate/restore.php'; +$ajaxUrl = Uri::base() . 'components/com_joomlaupdate/extract.php'; $returnUrl = 'index.php?option=com_joomlaupdate&task=update.finalise&' . Factory::getSession()->getFormToken() . '=1'; $this->document->addScriptOptions( diff --git a/build/media_source/com_joomlaupdate/joomla.asset.json b/build/media_source/com_joomlaupdate/joomla.asset.json index 73ee972cc431f..b8a89e3d8b5b2 100644 --- a/build/media_source/com_joomlaupdate/joomla.asset.json +++ b/build/media_source/com_joomlaupdate/joomla.asset.json @@ -28,30 +28,6 @@ "attributes": { "defer": true } - }, - { - "name": "com_joomlaupdate.encryption", - "type": "script", - "uri": "com_joomlaupdate/encryption.min.js", - "dependencies": [ - "core" - ], - "attributes": { - "defer": true - } - }, - { - "name": "com_joomlaupdate.update", - "type": "script", - "uri": "com_joomlaupdate/update.min.js", - "dependencies": [ - "core", - "jquery", - "com_joomlaupdate.encryption" - ], - "attributes": { - "defer": true - } } ] } diff --git a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js index f70f415634b32..1fb17d3ce25e9 100644 --- a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js +++ b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js @@ -5,11 +5,144 @@ Joomla = window.Joomla || {}; +Joomla.Update = window.Joomla.Update || { + stat_total: 0, + stat_files: 0, + stat_inbytes: 0, + stat_outbytes: 0, + password: null, + totalsize: 0, + ajax_url: null, + return_url: null, + errorHandler: (message) => { + alert(`ERROR:\n${message}`); + }, + startExtract: () => { + // Reset variables + Joomla.Update.stat_files = 0; + Joomla.Update.stat_inbytes = 0; + Joomla.Update.stat_outbytes = 0; + + const postData = new FormData(); + postData.append('task', 'startExtract'); + postData.append('password', Joomla.Update.password); + + // Make the initial request to the extraction script + Joomla.request({ + url: Joomla.Update.ajax_url, + data: postData, + method: 'POST', + perform: true, + onSuccess: (rawJson) => { + try { + // If we can decode the response as JSON step through the update + const data = JSON.parse(rawJson); + Joomla.Update.stepExtract(data); + } catch (e) { + // Decoding failed; display an error + Joomla.Update.errorHandler(e.message); + } + }, + onError: () => { + // A server error has occurred. + Joomla.Update.errorHandler('AJAX Error'); + }, + }); + }, + stepExtract: (data) => { + // Did we get an error from the ZIP extraction engine? + if (data.status === false) { + Joomla.Update.errorHandler(data.message); + + return; + } + + const elProgress = document.getElementById('progress-bar'); + + // Are we done extracting? + if (data.done) { + elProgress.classList.add('bg-success'); + elProgress.style.width = '100%'; + elProgress.setAttribute('aria-valuenow', 100); + + Joomla.Update.finalizeUpdate(); + + return; + } + + // Add data to variables + Joomla.Update.stat_inbytes += data.bytesIn; + Joomla.Update.stat_percent = (Joomla.Update.stat_inbytes * 100) / Joomla.Update.totalsize; + + // Update GUI + Joomla.Update.stat_outbytes += data.bytesOut; + Joomla.Update.stat_files += data.files; + + if (Joomla.Update.stat_percent < 100) { + elProgress.classList.remove('bg-success'); + elProgress.style.width = `${Joomla.Update.stat_percent}%`; + elProgress.setAttribute('aria-valuenow', Joomla.Update.stat_percent); + } else if (Joomla.Update.stat_percent >= 100) { + elProgress.classList.add('bg-success'); + elProgress.style.width = '100%'; + elProgress.setAttribute('aria-valuenow', 100); + } + + document.getElementById('extpercent').innerText = `${Joomla.Update.stat_percent.toFixed(1)}%`; + document.getElementById('extbytesin').innerText = Joomla.Update.stat_inbytes; + document.getElementById('extbytesout').innerText = Joomla.Update.stat_outbytes; + document.getElementById('extfiles').innerText = Joomla.Update.stat_files; + + const postData = new FormData(); + postData.append('task', 'stepExtract'); + postData.append('password', Joomla.Update.password); + + if (data.instance) { + postData.append('instance', data.instance); + } + + Joomla.request({ + url: Joomla.Update.ajax_url, + data: postData, + method: 'POST', + perform: true, + onSuccess: (rawJson) => { + try { + const newData = JSON.parse(rawJson); + Joomla.Update.stepExtract(newData); + } catch (e) { + Joomla.Update.errorHandler(e.message); + } + }, + onError: () => { + Joomla.Update.errorHandler('AJAX Error'); + }, + }); + }, + finalizeUpdate: () => { + const postData = new FormData(); + postData.append('task', 'finalizeUpdate'); + postData.append('password', Joomla.Update.password); + Joomla.request({ + url: Joomla.Update.ajax_url, + data: postData, + method: 'POST', + perform: true, + onSuccess: () => { + window.location = Joomla.Update.return_url; + }, + onError: () => { + Joomla.Update.errorHandler('AJAX Error'); + }, + }); + }, +}; + document.addEventListener('DOMContentLoaded', () => { const JoomlaUpdateOptions = Joomla.getOptions('joomlaupdate'); - window.joomlaupdate_password = JoomlaUpdateOptions.password; - window.joomlaupdate_totalsize = JoomlaUpdateOptions.totalsize; - window.joomlaupdate_ajax_url = JoomlaUpdateOptions.ajax_url; - window.joomlaupdate_return_url = JoomlaUpdateOptions.return_url; - window.pingExtract(); + Joomla.Update.password = JoomlaUpdateOptions.password; + Joomla.Update.totalsize = JoomlaUpdateOptions.totalsize; + Joomla.Update.ajax_url = JoomlaUpdateOptions.ajax_url; + Joomla.Update.return_url = JoomlaUpdateOptions.return_url; + Joomla.Update.startExtract(); }); diff --git a/build/media_source/com_joomlaupdate/js/encryption.es5.js b/build/media_source/com_joomlaupdate/js/encryption.es5.js deleted file mode 100644 index 5346028124324..0000000000000 --- a/build/media_source/com_joomlaupdate/js/encryption.es5.js +++ /dev/null @@ -1,457 +0,0 @@ -/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -/* AES implementation in JavaScript (c) Chris Veness 2005-2010 */ -/* - see http://csrc.nist.gov/publications/PubsFIPS.html#197 */ -/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - -var Aes = {}; // Aes namespace - -/** - * AES Cipher function: encrypt 'input' state with Rijndael algorithm - * applies Nr rounds (10/12/14) using key schedule w for 'add round key' stage - * - * @param {Number[]} input 16-byte (128-bit) input state array - * @param {Number[][]} w Key schedule as 2D byte-array (Nr+1 x Nb bytes) - * @returns {Number[]} Encrypted output state array - */ -Aes.Cipher = function(input, w) { // main Cipher function [§5.1] - var Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES) - var Nr = w.length/Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys - - var state = [[],[],[],[]]; // initialise 4xNb byte-array 'state' with input [§3.4] - for (var i=0; i<4*Nb; i++) state[i%4][Math.floor(i/4)] = input[i]; - - state = Aes.AddRoundKey(state, w, 0, Nb); - - for (var round=1; round 6 && i%Nk == 4) { - temp = Aes.SubWord(temp); - } - for (var t=0; t<4; t++) w[i][t] = w[i-Nk][t] ^ temp[t]; - } - - return w; -} - -/* - * ---- remaining routines are private, not called externally ---- - */ - -Aes.SubBytes = function(s, Nb) { // apply SBox to state S [§5.1.1] - for (var r=0; r<4; r++) { - for (var c=0; c>> i*8) & 0xff; - for (var i=0; i<4; i++) counterBlock[i+4] = nonceMs & 0xff; - // and convert it to a string to go on the front of the ciphertext - var ctrTxt = ''; - for (var i=0; i<8; i++) ctrTxt += String.fromCharCode(counterBlock[i]); - - // generate key schedule - an expansion of the key into distinct Key Rounds for each round - var keySchedule = Aes.KeyExpansion(key); - - var blockCount = Math.ceil(plaintext.length/blockSize); - var ciphertxt = new Array(blockCount); // ciphertext as array of strings - - for (var b=0; b>> c*8) & 0xff; - for (var c=0; c<4; c++) counterBlock[15-c-4] = (b/0x100000000 >>> c*8) - - var cipherCntr = Aes.Cipher(counterBlock, keySchedule); // -- encrypt counter block -- - - // block size is reduced on final block - var blockLength = b>> c*8) & 0xff; - for (var c=0; c<4; c++) counterBlock[15-c-4] = (((b+1)/0x100000000-1) >>> c*8) & 0xff; - - var cipherCntr = Aes.Cipher(counterBlock, keySchedule); // encrypt counter block - - var plaintxtByte = new Array(ciphertext[b].length); - for (var i=0; i 0) { while (c++ < 3) { pad += '='; plain += '\0'; } } - // note: doing padding here saves us doing special-case packing for trailing 1 or 2 chars - - for (c=0; c>18 & 0x3f; - h2 = bits>>12 & 0x3f; - h3 = bits>>6 & 0x3f; - h4 = bits & 0x3f; - - // use hextets to index into code string - e[c/3] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); - } - coded = e.join(''); // join() is far faster than repeated string concatenation in IE - - // replace 'A's from padded nulls with '='s - coded = coded.slice(0, coded.length-pad.length) + pad; - - return coded; -} - -/** - * Decode string from Base64, as defined by RFC 4648 [http://tools.ietf.org/html/rfc4648] - * (instance method extending String object). As per RFC 4648, newlines are not catered for. - * - * @param {String} str The string to be decoded from base-64 - * @param {Boolean} [utf8decode=false] Flag to indicate whether str is Unicode string to be decoded - * from UTF8 after conversion from base64 - * @returns {String} decoded string - */ -Base64.decode = function(str, utf8decode) { - utf8decode = (typeof utf8decode == 'undefined') ? false : utf8decode; - var o1, o2, o3, h1, h2, h3, h4, bits, d=[], plain, coded; - var b64 = Base64.code; - - coded = utf8decode ? str.decodeUTF8() : str; - - - for (var c=0; c>>16 & 0xff; - o2 = bits>>>8 & 0xff; - o3 = bits & 0xff; - - d[c/4] = String.fromCharCode(o1, o2, o3); - // check for padding - if (h4 == 0x40) d[c/4] = String.fromCharCode(o1, o2); - if (h3 == 0x40) d[c/4] = String.fromCharCode(o1); - } - plain = d.join(''); // join() is far faster than repeated string concatenation in IE - - return utf8decode ? plain.decodeUTF8() : plain; -} - - -/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -/* Utf8 class: encode / decode between multi-byte Unicode characters and UTF-8 multiple */ -/* single-byte character encoding (c) Chris Veness 2002-2010 */ -/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - -var Utf8 = {}; // Utf8 namespace - -/** - * Encode multi-byte Unicode string into utf-8 multiple single-byte characters - * (BMP / basic multilingual plane only) - * - * Chars in range U+0080 - U+07FF are encoded in 2 chars, U+0800 - U+FFFF in 3 chars - * - * @param {String} strUni Unicode string to be encoded as UTF-8 - * @returns {String} encoded string - */ -Utf8.encode = function(strUni) { - // use regular expressions & String.replace callback function for better efficiency - // than procedural approaches - var strUtf = strUni.replace( - /[\u0080-\u07ff]/g, // U+0080 - U+07FF => 2 bytes 110yyyyy, 10zzzzzz - function(c) { - var cc = c.charCodeAt(0); - return String.fromCharCode(0xc0 | cc>>6, 0x80 | cc&0x3f); } - ); - strUtf = strUtf.replace( - /[\u0800-\uffff]/g, // U+0800 - U+FFFF => 3 bytes 1110xxxx, 10yyyyyy, 10zzzzzz - function(c) { - var cc = c.charCodeAt(0); - return String.fromCharCode(0xe0 | cc>>12, 0x80 | cc>>6&0x3F, 0x80 | cc&0x3f); } - ); - return strUtf; -} - -/** - * Decode utf-8 encoded string back into multi-byte Unicode characters - * - * @param {String} strUtf UTF-8 string to be decoded back to Unicode - * @returns {String} decoded string - */ -Utf8.decode = function(strUtf) { - var strUni = strUtf.replace( - /[\u00c0-\u00df][\u0080-\u00bf]/g, // 2-byte chars - function(c) { // (note parentheses for precence) - var cc = (c.charCodeAt(0)&0x1f)<<6 | c.charCodeAt(1)&0x3f; - return String.fromCharCode(cc); } - ); - strUni = strUni.replace( - /[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g, // 3-byte chars - function(c) { // (note parentheses for precence) - var cc = ((c.charCodeAt(0)&0x0f)<<12) | ((c.charCodeAt(1)&0x3f)<<6) | ( c.charCodeAt(2)&0x3f); - return String.fromCharCode(cc); } - ); - return strUni; -} - -/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ \ No newline at end of file diff --git a/build/media_source/com_joomlaupdate/js/update.es5.js b/build/media_source/com_joomlaupdate/js/update.es5.js deleted file mode 100644 index bf48c60e77b56..0000000000000 --- a/build/media_source/com_joomlaupdate/js/update.es5.js +++ /dev/null @@ -1,391 +0,0 @@ -/** - * @package AkeebaCMSUpdate - * @copyright Copyright (c)2010-2014 Nicholas K. Dionysopoulos - * @license GNU General Public License version 3, or later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -var stat_total = 0; -var stat_files = 0; -var stat_inbytes = 0; -var stat_outbytes = 0; - -/** - * An extremely simple error handler, dumping error messages to screen - * - * @param error The error message string - */ -function error_callback(error) -{ - alert("ERROR:\n"+error); -} - -/** - * Performs an encrypted AJAX request and returns the parsed JSON output. - * The window.ajax_url is used as the AJAX proxy URL. - * If there is no errorCallback, the window.error_callback is used. - * - * @param object data An object with the query data, e.g. a serialized form - * @param function successCallback A function accepting a single object parameter, called on success - * @param function errorCallback A function accepting a single string parameter, called on failure - */ -doEncryptedAjax = function(data, successCallback, errorCallback) -{ - var json = JSON.stringify(data); - if( joomlaupdate_password.length > 0 ) - { - json = AesCtr.encrypt( json, joomlaupdate_password, 128 ); - } - var post_data = { - 'json': json - }; - - var structure = - { - type: "POST", - url: joomlaupdate_ajax_url, - cache: false, - data: post_data, - timeout: 600000, - - success: function(msg, responseXML) - { - // Initialize - var junk = null; - var message = ""; - - // Get rid of junk before the data - var valid_pos = msg.indexOf('###'); - - if( valid_pos == -1 ) - { - // Valid data not found in the response - msg = 'Invalid AJAX data:\n' + msg; - - if (errorCallback == null) - { - if(error_callback != null) - { - error_callback(msg); - } - } - else - { - errorCallback(msg); - } - - return; - } - else if( valid_pos != 0 ) - { - // Data is prefixed with junk - junk = msg.substr(0, valid_pos); - message = msg.substr(valid_pos); - } - else - { - message = msg; - } - - message = message.substr(3); // Remove triple hash in the beginning - - // Get of rid of junk after the data - var valid_pos = message.lastIndexOf('###'); - - message = message.substr(0, valid_pos); // Remove triple hash in the end - - // Decrypt if required - var data = null; - if( joomlaupdate_password.length > 0 ) - { - try - { - var data = JSON.parse(message); - } - catch(err) - { - message = AesCtr.decrypt(message, joomlaupdate_password, 128); - } - } - - try - { - if (empty(data)) - { - data = JSON.parse(message); - } - } - catch(err) - { - var msg = err.message + "\n
\n
\n" + message + "\n
"; - - if (errorCallback == null) - { - if (error_callback != null) - { - error_callback(msg); - } - } - else - { - errorCallback(msg); - } - - return; - } - - // Call the callback function - successCallback(data); - }, - - error: function(req) - { - var message = 'AJAX Loading Error: ' + req.statusText; - - if(errorCallback == null) - { - if (error_callback != null) - { - error_callback(message); - } - } - else - { - errorCallback(message); - } - } - }; - - jQuery.ajax( structure ); -}; - -/** - * Pings the update script (making sure its executable) - */ -pingExtract = function() -{ - // Reset variables - this.stat_files = 0; - this.stat_inbytes = 0; - this.stat_outbytes = 0; - - // Do AJAX post - var post = {task : 'ping'}; - - this.doEncryptedAjax(post, - function(data) { - startExtract(data); - }); -}; - -startExtract = function() -{ - // Reset variables - this.stat_files = 0; - this.stat_inbytes = 0; - this.stat_outbytes = 0; - - var post = { task : 'startRestore' }; - - this.doEncryptedAjax(post, function(data){ - stepExtract(data); - }); -}; - -stepExtract = function(data) -{ - if(data.status == false) - { - // handle failure - error_callback(data.message); - - return; - } - - if( !empty(data.Warnings) ) - { - // @todo Handle warnings - /** - $.each(data.Warnings, function(i, item){ - $('#warnings').append( - $(document.createElement('div')) - .html(item) - ); - $('#warningsBox').show('fast'); - }); - /**/ - } - - if (!empty(data.factory)) - { - extract_factory = data.factory; - } - - if(data.done) - { - finalizeUpdate(); - } - else - { - // Add data to variables - stat_inbytes += data.bytesIn; - stat_percent = (stat_inbytes * 100) / joomlaupdate_totalsize; - - // Update GUI - stat_outbytes += data.bytesOut; - stat_files += data.files; - - if (stat_percent < 100) - { - jQuery('#progress-bar').css('width', stat_percent + '%').attr('aria-valuenow', stat_percent); - } - else if (stat_percent > 100) - { - stat_percent = 100; - jQuery('#progress-bar').css('width', stat_percent + '%').attr('aria-valuenow', stat_percent); - } - else - { - jQuery('#progress-bar').removeClass('bar-success'); - } - - jQuery('#extpercent').text(stat_percent.toFixed(1) + '%'); - jQuery('#extbytesin').text(stat_inbytes); - jQuery('#extbytesout').text(stat_outbytes); - jQuery('#extfiles').text(stat_files); - - // Do AJAX post - post = { - task: 'stepRestore', - factory: data.factory - }; - doEncryptedAjax(post, function(data){ - stepExtract(data); - }); - } -}; - -finalizeUpdate = function () -{ - // Do AJAX post - var post = { task : 'finalizeRestore', factory: window.factory }; - doEncryptedAjax(post, function(data){ - window.location = joomlaupdate_return_url; - }); -}; - - -/** - * Is a variable empty? - * - * Part of php.js - * - * @see http://phpjs.org/ - * - * @param mixed mixed_var The variable - * - * @returns boolean True if empty - */ -function empty (mixed_var) -{ - var key; - - if (mixed_var === "" || - mixed_var === 0 || - mixed_var === "0" || - mixed_var === null || - mixed_var === false || - typeof mixed_var === 'undefined' - ){ - return true; - } - - if (typeof mixed_var == 'object') - { - for (key in mixed_var) - { - return false; - } - - return true; - } - - return false; -} - -/** - * Is the variable an array? - * - * Part of php.js - * - * @see http://phpjs.org/ - * - * @param mixed mixed_var The variable - * - * @returns boolean True if it is an array or an object - */ -function is_array (mixed_var) -{ - var key = ''; - var getFuncName = function (fn) { - var name = (/\W*function\s+([\w\$]+)\s*\(/).exec(fn); - - if (!name) { - return '(Anonymous)'; - } - - return name[1]; - }; - - if (!mixed_var) - { - return false; - } - - // BEGIN REDUNDANT - this.php_js = this.php_js || {}; - this.php_js.ini = this.php_js.ini || {}; - // END REDUNDANT - - if (typeof mixed_var === 'object') - { - if (this.php_js.ini['phpjs.objectsAsArrays'] && // Strict checking for being a JavaScript array (only check this way if call ini_set('phpjs.objectsAsArrays', 0) to disallow objects as arrays) - ( - (this.php_js.ini['phpjs.objectsAsArrays'].local_value.toLowerCase && - this.php_js.ini['phpjs.objectsAsArrays'].local_value.toLowerCase() === 'off') || - parseInt(this.php_js.ini['phpjs.objectsAsArrays'].local_value, 10) === 0) - ) { - return mixed_var.hasOwnProperty('length') && // Not non-enumerable because of being on parent class - !mixed_var.propertyIsEnumerable('length') && // Since is own property, if not enumerable, it must be a built-in function - getFuncName(mixed_var.constructor) !== 'String'; // exclude String() - } - - if (mixed_var.hasOwnProperty) - { - for (key in mixed_var) { - // Checks whether the object has the specified property - // if not, we figure it's not an object in the sense of a php-associative-array. - if (false === mixed_var.hasOwnProperty(key)) { - return false; - } - } - } - - // Read discussion at: http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_is_array/ - return true; - } - - return false; -} diff --git a/templates/system/build_incomplete.html b/templates/system/build_incomplete.html index 4b0ce59f5f6c9..cadf59298ccd7 100644 --- a/templates/system/build_incomplete.html +++ b/templates/system/build_incomplete.html @@ -6,7 +6,7 @@ Joomla: Environment Setup Incomplete - +
diff --git a/templates/system/fatal-error.html b/templates/system/fatal-error.html index e99371a607d01..462afbd9aabfd 100644 --- a/templates/system/fatal-error.html +++ b/templates/system/fatal-error.html @@ -6,7 +6,7 @@ An Error Occurred: {{statusText}} - +
From df2d25ab2e0d8be87ad9d224dace458c016f200e Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 09:08:00 +0300 Subject: [PATCH 02/58] Change concrete versions to __DEPLOY_VERSION__ Co-authored-by: Phil E. Taylor --- administrator/components/com_joomlaupdate/extract.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index b1b6e2f0d5d34..a4d8664a18d84 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -12,7 +12,7 @@ /** * ZIP archive extraction class * - * @since 4.0.3 + * @since __DEPLOY_VERSION__ */ class ZIPExtraction { From e7ed637c6c695f78653966126828211ea343b82e Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 09:16:04 +0300 Subject: [PATCH 03/58] Typo fix Co-authored-by: Brian Teeman --- administrator/components/com_joomlaupdate/extract.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index a4d8664a18d84..ff30aa40fba8a 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -41,7 +41,7 @@ class ZIPExtraction * consumed at least this much percentage of the MAX_EXEC_TIME we will stop processing the archive in this page * load, return the result to the client and wait for it to call us again so we can resume the extraction. * - * This becomes important when the MAX_EXEC_TIME is close the the PHP, PHP-FPM or Apache timeout oon the server + * This becomes important when the MAX_EXEC_TIME is close the the PHP, PHP-FPM or Apache timeout on the server * (whichever is lowest) and there are fairly large files in the backup archive. If we start extracting a large, * compressed file close to a hard server timeout it's possible that we will overshoot that hard timeout and see the * extraction failing. From 45c52521e80ad92fd60145cacf6ff4829a2a6089 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 09:16:43 +0300 Subject: [PATCH 04/58] Remove jQuery dependency Co-authored-by: Dimitris Grammatikogiannis --- .../components/com_joomlaupdate/tmpl/update/default.php | 1 - 1 file changed, 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/tmpl/update/default.php b/administrator/components/com_joomlaupdate/tmpl/update/default.php index e9a1abbe052b1..4157fab358104 100644 --- a/administrator/components/com_joomlaupdate/tmpl/update/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/update/default.php @@ -16,7 +16,6 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') - ->useScript('jquery') ->useScript('com_joomlaupdate.admin-update'); $password = Factory::getApplication()->getUserState('com_joomlaupdate.password', null); From a4718ef5ae579ff171a59198c303c048d3abd312 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 09:17:29 +0300 Subject: [PATCH 05/58] Fix typo Co-authored-by: Brian Teeman --- administrator/components/com_joomlaupdate/extract.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index ff30aa40fba8a..c7817d0a98a01 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -52,7 +52,7 @@ class ZIPExtraction * Example: if MAX_EXEC_TIME is 10 seconds and RUNTIME_BIAS is 80 each page load will take between 80% and 100% of * the MAX_EXEC_TIME, i.e. anywhere between 8 and 10 seconds. * - * Lower values make it less overshooting MAX_EXEC_TIME when extracting large files. + * Lower values make it less likely to overshoot MAX_EXEC_TIME when extracting large files. * * @var int */ From 1c1213e73f498a4ab61cf8362fdcbb57ccb664b4 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 09:17:51 +0300 Subject: [PATCH 06/58] Fix typo Co-authored-by: Brian Teeman --- administrator/components/com_joomlaupdate/extract.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index c7817d0a98a01..1c946f256c6c9 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -72,7 +72,7 @@ class ZIPExtraction * spike over a short period of time. * * The chosen value of 3 seconds belongs to the first category, essentially making sure that we have a decent rate - * limiting without annoying the user too much but also without catering for the most badly configured of shared + * limiting without annoying the user too much but also catering for the most badly configured of shared * hosting. It's a happy medium which works for the majority (~90%) of commercial servers out there. * * @var int From ac9ccfa3669e2e8127ba1fca0978a047dfd0788e Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 09:18:20 +0300 Subject: [PATCH 07/58] Change docblock list character Co-authored-by: Phil E. Taylor --- administrator/components/com_joomlaupdate/extract.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index 1c946f256c6c9..c5e624cccf305 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -249,9 +249,9 @@ public static function unserialiseInstance(string $serialised): ?self * Wakeup function, called whenever the class is deserialized. * * This method does the following: - * * Restart the timer. - * * Reopen the archive file, if one is defined. - * * Seek to the correct offset of the file. + * - Restart the timer. + * - Reopen the archive file, if one is defined. + * - Seek to the correct offset of the file. * * @return void * @internal From 5164b0d7ca39e6ff1fa5da98216a921a40efa740 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 09:27:01 +0300 Subject: [PATCH 08/58] Typo --- administrator/components/com_joomlaupdate/extract.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index c5e624cccf305..9c8561c92a080 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -1067,7 +1067,7 @@ private function isIgnoredDirectory(string $shortFilename): bool */ private function processTypeDir(): bool { - // Directory entries in the JPA do not have file data, therefore we're done processing the entry + // Directory entries do not have file data, therefore we're done processing the entry. $this->runState = self::AK_STATE_DATAREAD; return true; From 32c9494093e8fb1687ae86ebd7f34646ff0976f0 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 10:17:53 +0300 Subject: [PATCH 09/58] Make sure these files are not added to the PR --- templates/system/build_incomplete.html | 2 +- templates/system/fatal-error.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/system/build_incomplete.html b/templates/system/build_incomplete.html index cadf59298ccd7..4b0ce59f5f6c9 100644 --- a/templates/system/build_incomplete.html +++ b/templates/system/build_incomplete.html @@ -6,7 +6,7 @@ Joomla: Environment Setup Incomplete - +
diff --git a/templates/system/fatal-error.html b/templates/system/fatal-error.html index 462afbd9aabfd..e99371a607d01 100644 --- a/templates/system/fatal-error.html +++ b/templates/system/fatal-error.html @@ -6,7 +6,7 @@ An Error Occurred: {{statusText}} - +
From 3569ec09be7cba3f5a3ccf50d27b30ab32fcba58 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 10:27:33 +0300 Subject: [PATCH 10/58] Change the copyright date --- administrator/components/com_joomlaupdate/extract.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index 9c8561c92a080..e1e71e30b4edb 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -3,7 +3,7 @@ * @package Joomla.Administrator * @subpackage com_joomlaupdate * - * @copyright (C) 2016 Open Source Matters, Inc. + * @copyright (C) 2021 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt */ From 3d57865995c7f79743dfcd5f1f6e7e3b521a2d61 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 10:29:52 +0300 Subject: [PATCH 11/58] Add a legal note I reckon this is important to protect Open Source Matters, Inc. My original work is licensed under the GPLv3, this derivative I chose to license under the GPLv2 and share the copyright with OSM. If there is no documentation about this having taken place, accepting this PR could be a copyright violation. I'm not sure since I am the author of both the original code and the derivative but better be safe than sorry, right? --- administrator/components/com_joomlaupdate/extract.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index e1e71e30b4edb..757874f48764e 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -12,6 +12,17 @@ /** * ZIP archive extraction class * + * This is a derivative work of Akeeba Restore which is Copyright (c)2008-2021 Nicholas K. + * Dionysopoulos and Akeeba Ltd, distributed under the terms of GNU General Public License version 3 + * or later. + * + * The author of the original work has decided to relicense the derivative work under the terms of + * the GNU General Public License version 2 or later and share the copyright of the derivative work + * with Open Source Matters, Inc (OSM), granting OSM non-exclusive rights to this work per the terms + * of the Joomla Contributor Agreement (JCA) the author signed back in 2011 and which is still in + * effect. This is affirmed by the cryptographically signed commits in the Git repository containing + * this file, the copyright messages and this notice here. + * * @since __DEPLOY_VERSION__ */ class ZIPExtraction From 4e71949e364083da5b906d0abb43c0aad7d1c095 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 12:35:30 +0300 Subject: [PATCH 12/58] Update joomla.asset.json per @dgrammatiko suggestion --- .../com_joomlaupdate/joomla.asset.json | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/build/media_source/com_joomlaupdate/joomla.asset.json b/build/media_source/com_joomlaupdate/joomla.asset.json index b8a89e3d8b5b2..4819f52d34c37 100644 --- a/build/media_source/com_joomlaupdate/joomla.asset.json +++ b/build/media_source/com_joomlaupdate/joomla.asset.json @@ -5,16 +5,40 @@ "description": "Joomla CMS", "license": "GPL-2.0-or-later", "assets": [ + { + "name": "com_joomlaupdate.admin-update-es5", + "type": "script", + "uri": "com_joomlaupdate/admin-update-default-es5.min.js", + "dependencies": [ + "core" + ], + "attributes": { + "defer": true, + "nomodule": true + } + }, { "name": "com_joomlaupdate.admin-update", "type": "script", "uri": "com_joomlaupdate/admin-update-default.min.js", + "dependencies": [ + "com_joomlaupdate.admin-update-es5" + ], + "attributes": { + "type": "module" + } + }, + { + "name": "com_joomlaupdate.default-es5", + "type": "script", + "uri": "com_joomlaupdate/default-es5.min.js", "dependencies": [ "core", "jquery" ], "attributes": { - "defer": true + "defer": true, + "nomodule": true } }, { @@ -22,11 +46,10 @@ "type": "script", "uri": "com_joomlaupdate/default.min.js", "dependencies": [ - "core", - "jquery" + "com_joomlaupdate.default-es5" ], "attributes": { - "defer": true + "type": "module" } } ] From b610e8b612efd12d1f29dfe9de1827d879429e45 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 12:36:52 +0300 Subject: [PATCH 13/58] Remove useless DOMContentLoaded event Per @dgrammatiko suggestion Since this file is loaded deferred it's guaranteed to load AFTER the DOM content has loaded. Therefore we do not have to add an event handler. By definition, the handler will run immediately. --- .../js/admin-update-default.es6.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js index 1fb17d3ce25e9..27b40a05e3f03 100644 --- a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js +++ b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js @@ -138,11 +138,9 @@ Joomla.Update = window.Joomla.Update || { }, }; -document.addEventListener('DOMContentLoaded', () => { - const JoomlaUpdateOptions = Joomla.getOptions('joomlaupdate'); - Joomla.Update.password = JoomlaUpdateOptions.password; - Joomla.Update.totalsize = JoomlaUpdateOptions.totalsize; - Joomla.Update.ajax_url = JoomlaUpdateOptions.ajax_url; - Joomla.Update.return_url = JoomlaUpdateOptions.return_url; - Joomla.Update.startExtract(); -}); +const JoomlaUpdateOptions = Joomla.getOptions('joomlaupdate'); +Joomla.Update.password = JoomlaUpdateOptions.password; +Joomla.Update.totalsize = JoomlaUpdateOptions.totalsize; +Joomla.Update.ajax_url = JoomlaUpdateOptions.ajax_url; +Joomla.Update.return_url = JoomlaUpdateOptions.return_url; +Joomla.Update.startExtract(); From 1054ec2a92ad10414ca4b88d3dbf053e715ab071 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 12:38:48 +0300 Subject: [PATCH 14/58] Wrap the extraction start in a conditional Per @dgrammatiko suggestion --- .../js/admin-update-default.es6.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js index 27b40a05e3f03..447787d43282b 100644 --- a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js +++ b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js @@ -139,8 +139,12 @@ Joomla.Update = window.Joomla.Update || { }; const JoomlaUpdateOptions = Joomla.getOptions('joomlaupdate'); -Joomla.Update.password = JoomlaUpdateOptions.password; -Joomla.Update.totalsize = JoomlaUpdateOptions.totalsize; -Joomla.Update.ajax_url = JoomlaUpdateOptions.ajax_url; -Joomla.Update.return_url = JoomlaUpdateOptions.return_url; -Joomla.Update.startExtract(); + +if (JoomlaUpdateOptions && Object.keys(JoomlaUpdateOptions).length) { + Joomla.Update.password = JoomlaUpdateOptions.password; + Joomla.Update.totalsize = JoomlaUpdateOptions.totalsize; + Joomla.Update.ajax_url = JoomlaUpdateOptions.ajax_url; + Joomla.Update.return_url = JoomlaUpdateOptions.return_url; + + Joomla.Update.startExtract(); +} From 6ab8871b4b96538dab27efaef69469591a356088 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 12:40:22 +0300 Subject: [PATCH 15/58] Consistency Co-authored-by: Richard Fath --- administrator/components/com_joomlaupdate/finalisation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/finalisation.php b/administrator/components/com_joomlaupdate/finalisation.php index 145d1be7a156c..3fd8aec1162a8 100644 --- a/administrator/components/com_joomlaupdate/finalisation.php +++ b/administrator/components/com_joomlaupdate/finalisation.php @@ -85,7 +85,7 @@ function finalizeUpdate($siteRoot, $restorePath) if (!class_exists('\Joomla\CMS\Filesystem\File')) { /** - * JFile mock class + * File mock class * * @since 3.5.1 */ From d1a15cfc4d881e249309ed559b7578136919e382 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 12:47:39 +0300 Subject: [PATCH 16/58] Invalidate OPcache before deleting/moving file Per @PhilETaylor suggestion --- .../components/com_joomlaupdate/finalisation.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/administrator/components/com_joomlaupdate/finalisation.php b/administrator/components/com_joomlaupdate/finalisation.php index 3fd8aec1162a8..0ea55cc3f0713 100644 --- a/administrator/components/com_joomlaupdate/finalisation.php +++ b/administrator/components/com_joomlaupdate/finalisation.php @@ -116,14 +116,9 @@ public static function exists(string $fileName): bool */ public static function delete(string $fileName): bool { - $result = @unlink($fileName); + self::invalidateFileCache($fileName); - if ($result) - { - self::invalidateFileCache($fileName); - } - - return $result; + return @unlink($fileName); } /** @@ -138,11 +133,12 @@ public static function delete(string $fileName): bool */ public static function move(string $src, string $dest): bool { + self::invalidateFileCache($src); + $result = @rename($src, $dest); if ($result) { - self::invalidateFileCache($src); self::invalidateFileCache($dest); } @@ -237,10 +233,10 @@ public static function delete($folderName) continue; } - @unlink($item->getPathname()); - /** @noinspection PhpUndefinedFunctionInspection */ \clearFileInOPCache($item->getPathname()); + + @unlink($item->getPathname()); } return @rmdir($folderName); From 2a10f974caa5e508673979384dcbb24ee3a39ac1 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 12:49:29 +0300 Subject: [PATCH 17/58] Show Console error if core JS is not loaded Per @dgrammatiko suggestion --- .../com_joomlaupdate/js/admin-update-default.es6.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js index 447787d43282b..1d728bd2babf8 100644 --- a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js +++ b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js @@ -3,7 +3,9 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -Joomla = window.Joomla || {}; +if (!Joomla) { + throw new Error('Joomla API was not initialised properly'); +} Joomla.Update = window.Joomla.Update || { stat_total: 0, From 1a8a8eca43412fc915fc042571f1d8b0c0fa6a67 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 13:49:46 +0300 Subject: [PATCH 18/58] MUCH MORE DETAILED error modal dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error modal dialog covers the following cases: * 403 Forbidden. Tells the user where to look so they can fix it. * Other HTTP error. Tells the user something is broken. I really don't have much else to tell them without looking at their error logs. * “Invalid login” returned from `extract.php`. Explain why this happens and what to do to troubleshoot it. * Any other error. It simply conveys the error message sent by `extract.php` as it's self-explanatory and typically indicates a corrupt archive. --- .../com_joomlaupdate/tmpl/update/default.php | 40 ++++++++++++- .../language/en-GB/com_joomlaupdate.ini | 7 +++ .../com_joomlaupdate/joomla.asset.json | 3 +- .../js/admin-update-default.es6.js | 56 ++++++++++++++----- 4 files changed, 89 insertions(+), 17 deletions(-) diff --git a/administrator/components/com_joomlaupdate/tmpl/update/default.php b/administrator/components/com_joomlaupdate/tmpl/update/default.php index 4157fab358104..41a3c7adde5de 100644 --- a/administrator/components/com_joomlaupdate/tmpl/update/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/update/default.php @@ -10,13 +10,22 @@ defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\CMS\Help\Help; use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.admin-update'); + ->useScript('com_joomlaupdate.admin-update') + ->useScript('bootstrap.modal'); + +Text::script('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_FORBIDDEN'); +Text::script('COM_JOOMLAUPDATE_ERRORMODAL_BODY_FORBIDDEN'); +Text::script('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_SERVERERROR'); +Text::script('COM_JOOMLAUPDATE_ERRORMODAL_BODY_SERVERERROR'); +Text::script('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_GENERIC'); +Text::script('COM_JOOMLAUPDATE_ERRORMODAL_BODY_INVALIDLOGIN'); $password = Factory::getApplication()->getUserState('com_joomlaupdate.password', null); $filesize = Factory::getApplication()->getUserState('com_joomlaupdate.filesize', null); @@ -36,6 +45,35 @@

+ +
diff --git a/administrator/language/en-GB/com_joomlaupdate.ini b/administrator/language/en-GB/com_joomlaupdate.ini index 49c8caa637859..7fcad946b3919 100644 --- a/administrator/language/en-GB/com_joomlaupdate.ini +++ b/administrator/language/en-GB/com_joomlaupdate.ini @@ -20,6 +20,13 @@ COM_JOOMLAUPDATE_EMPTYSTATE_APPEND="Update your site by manually uploading the u COM_JOOMLAUPDATE_EMPTYSTATE_BUTTON_ADD="Check for Updates" COM_JOOMLAUPDATE_EMPTYSTATE_CONTENT="Select the button below to check for updates." COM_JOOMLAUPDATE_EMPTYSTATE_TITLE="Check if an update is available." +COM_JOOMLAUPDATE_ERRORMODAL_BODY_FORBIDDEN="

Joomla cannot communicate with the file administrator/components/com_joomlaupdate/extract.php which performs the update. This typically happens because of one of the following reasons:

  • A server configuration file in your site's root, such as .htacess or web.config, prevents access to this file.
  • Your server configuration prevents access to this file.
  • The ownership and permissions of this file do not allow your server to access this file.
  • Your site is behind a load balancer or CDN but its configuration does not allow access to this file or blocks sending commands to it.
  • Even though the file can be accessed, sending commands to it is blocked by your server's protection such as mod_security2.

If you are not sure please contact your host.

" +COM_JOOMLAUPDATE_ERRORMODAL_BODY_INVALIDLOGIN="

Joomla Update cannot communicate correctly with the administrator/components/com_joomlaupdate/extract.php which performs the update. This typically happens because of one of the following reasons:

  • The file administrator/components/com_joomlaupdate/update.php, containing the instructions for applying the update, could not be created e.g. because of permissions problems.
  • The file mentioned above cannot be read by the extract.php script because of permissions problems.
  • Your browser, network gear or server is corrupting the password we are sending with each request to extract.php for security reasons

Please try using a different browser, without any browser extensions installed, ideally on a different computer connected to the Internet using a different provider. If the error persists you may want to contact your host.

" +COM_JOOMLAUPDATE_ERRORMODAL_BODY_SERVERERROR="

Joomla Update encountered a server error when applying the update to your site.

Your site may not have been upgraded completely to the new Joomla! version.

" +COM_JOOMLAUPDATE_ERRORMODAL_HEAD_FORBIDDEN="Access forbidden" +COM_JOOMLAUPDATE_ERRORMODAL_HEAD_GENERIC="An error occurred" +COM_JOOMLAUPDATE_ERRORMODAL_HEAD_SERVERERROR="Server error" +COM_JOOMLAUPDATE_ERRORMODAL_BTN_HELP="Get help with this error" COM_JOOMLAUPDATE_FAILED_TO_CHECK_UPDATES="Failed to check for updates." COM_JOOMLAUPDATE_MINIMUM_STABILITY_ALPHA="Alpha" COM_JOOMLAUPDATE_MINIMUM_STABILITY_BETA="Beta" diff --git a/build/media_source/com_joomlaupdate/joomla.asset.json b/build/media_source/com_joomlaupdate/joomla.asset.json index 4819f52d34c37..c35d5d7ec9723 100644 --- a/build/media_source/com_joomlaupdate/joomla.asset.json +++ b/build/media_source/com_joomlaupdate/joomla.asset.json @@ -10,7 +10,8 @@ "type": "script", "uri": "com_joomlaupdate/admin-update-default-es5.min.js", "dependencies": [ - "core" + "core", + "bootstrap.modal" ], "attributes": { "defer": true, diff --git a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js index 1d728bd2babf8..6b6f66687e172 100644 --- a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js +++ b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js @@ -16,8 +16,41 @@ Joomla.Update = window.Joomla.Update || { totalsize: 0, ajax_url: null, return_url: null, - errorHandler: (message) => { - alert(`ERROR:\n${message}`); + genericErrorMessage: (message) => { + const header = document.getElementById('errorDialogLabel'); + const messageDiv = document.getElementById('errorDialogMessage'); + + header.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_GENERIC'); + messageDiv.innerHTML = message; + + if (message.toLowerCase() === 'invalid login') { + messageDiv.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_BODY_INVALIDLOGIN'); + } + + const myModal = new bootstrap.Modal(document.getElementById('errorDialog'), { + keyboard: true, + backdrop: true, + }); + myModal.show(); + }, + handleErrorResponse: (xhr) => { + const isForbidden = xhr.status === 403; + const header = document.getElementById('errorDialogLabel'); + const message = document.getElementById('errorDialogMessage'); + + if (isForbidden) { + header.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_FORBIDDEN'); + message.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_BODY_FORBIDDEN'); + } else { + header.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_SERVERERROR'); + message.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_BODY_SERVERERROR'); + } + + const myModal = new bootstrap.Modal(document.getElementById('errorDialog'), { + keyboard: true, + backdrop: true, + }); + myModal.show(); }, startExtract: () => { // Reset variables @@ -42,19 +75,16 @@ Joomla.Update = window.Joomla.Update || { Joomla.Update.stepExtract(data); } catch (e) { // Decoding failed; display an error - Joomla.Update.errorHandler(e.message); + Joomla.Update.genericErrorMessage(e.message); } }, - onError: () => { - // A server error has occurred. - Joomla.Update.errorHandler('AJAX Error'); - }, + onError: Joomla.Update.handleErrorResponse, }); }, stepExtract: (data) => { // Did we get an error from the ZIP extraction engine? if (data.status === false) { - Joomla.Update.errorHandler(data.message); + Joomla.Update.genericErrorMessage(data.message); return; } @@ -113,12 +143,10 @@ Joomla.Update = window.Joomla.Update || { const newData = JSON.parse(rawJson); Joomla.Update.stepExtract(newData); } catch (e) { - Joomla.Update.errorHandler(e.message); + Joomla.Update.genericErrorMessage(e.message); } }, - onError: () => { - Joomla.Update.errorHandler('AJAX Error'); - }, + onError: Joomla.Update.handleErrorResponse, }); }, finalizeUpdate: () => { @@ -133,9 +161,7 @@ Joomla.Update = window.Joomla.Update || { onSuccess: () => { window.location = Joomla.Update.return_url; }, - onError: () => { - Joomla.Update.errorHandler('AJAX Error'); - }, + onError: Joomla.Update.handleErrorResponse, }); }, }; From cc0f77a3a0d51f419abe39ff3173af81d7377190 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 16:53:37 +0300 Subject: [PATCH 19/58] Language Co-authored-by: Phil E. Taylor --- .../components/com_joomlaupdate/src/Model/UpdateModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index b895e730d116b..bfead0a8c6c0e 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -854,7 +854,7 @@ public function finaliseUpgrade() * Removes the extracted package file and trigger onJoomlaAfterUpdate event. * * The onJoomlaAfterUpdate event compares the stored list of files previously overridden with - * the updated core files, finding out which ones have changed during the update, thereby + * the updated core files, finding out which files have changed during the update, thereby * determining how many and which override files need to be checked and possibly updated after * the Joomla update. * From af7e052eb8369cf6a43519420495e3041198ce9e Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 17:07:03 +0300 Subject: [PATCH 20/58] Remove unnecessary suppression Co-authored-by: Phil E. Taylor --- administrator/components/com_joomlaupdate/finalisation.php | 1 - 1 file changed, 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/finalisation.php b/administrator/components/com_joomlaupdate/finalisation.php index 0ea55cc3f0713..d8f6c08e8a0fc 100644 --- a/administrator/components/com_joomlaupdate/finalisation.php +++ b/administrator/components/com_joomlaupdate/finalisation.php @@ -159,7 +159,6 @@ public static function move(string $src, string $dest): bool */ public static function invalidateFileCache($filepath, $force = true) { - /** @noinspection PhpUndefinedFunctionInspection */ return \clearFileInOPCache($filepath); } From 0770e3e72a3031653f814e440747596f27cdbb18 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 17:07:18 +0300 Subject: [PATCH 21/58] Remove unnecessary suppression Co-authored-by: Phil E. Taylor --- administrator/components/com_joomlaupdate/finalisation.php | 1 - 1 file changed, 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/finalisation.php b/administrator/components/com_joomlaupdate/finalisation.php index d8f6c08e8a0fc..ebf663e11c7f1 100644 --- a/administrator/components/com_joomlaupdate/finalisation.php +++ b/administrator/components/com_joomlaupdate/finalisation.php @@ -232,7 +232,6 @@ public static function delete($folderName) continue; } - /** @noinspection PhpUndefinedFunctionInspection */ \clearFileInOPCache($item->getPathname()); @unlink($item->getPathname()); From be90c4aa8142875bd9f8309aadb2e3d9c6cd32c3 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 17:07:33 +0300 Subject: [PATCH 22/58] Comment alignment Co-authored-by: Phil E. Taylor --- administrator/components/com_joomlaupdate/extract.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index 757874f48764e..f29ff9103db5d 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -3,7 +3,7 @@ * @package Joomla.Administrator * @subpackage com_joomlaupdate * - * @copyright (C) 2021 Open Source Matters, Inc. + * @copyright (C) 2021 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt */ From b8b5302cc6744dfbae87198c993c67faa8c340fe Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 15:33:00 +0300 Subject: [PATCH 23/58] Remove unused method --- .../components/com_joomlaupdate/extract.php | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index f29ff9103db5d..849611e71afa9 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -1386,57 +1386,6 @@ function clearFileInOPCache(string $file): bool return false; } -/** - * Recursively remove directory. - * - * Used by the finalization script provided with Joomla Update. - * - * @param string $directory The directory to remove - * - * @return boolean - */ -function recursiveRemoveDirectory($directory) -{ - if (substr($directory, -1) == '/') - { - $directory = substr($directory, 0, -1); - } - - if (!@file_exists($directory) || !@is_dir($directory) || !is_readable($directory)) - { - return false; - } - - $di = new DirectoryIterator($directory); - - /** @var DirectoryIterator $item */ - foreach ($di as $item) - { - if ($item->isDot()) - { - continue; - } - - if ($item->isDir()) - { - $status = recursive_remove_directory($item->getPathname()); - - if (!$status) - { - return false; - } - - continue; - } - - @unlink($item->getPathname()); - - clearFileInOPCache($item->getPathname()); - } - - return @rmdir($directory); -} - /** * A timing safe equals comparison. * From 540ab88f65001b35960a2631a70e3521f043ebae Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 15:40:10 +0300 Subject: [PATCH 24/58] Allow multi-step extraction without browser errors --- .../com_joomlaupdate/js/admin-update-default.es6.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js index 6b6f66687e172..56f4d47273036 100644 --- a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js +++ b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js @@ -125,12 +125,18 @@ Joomla.Update = window.Joomla.Update || { document.getElementById('extbytesout').innerText = Joomla.Update.stat_outbytes; document.getElementById('extfiles').innerText = Joomla.Update.stat_files; + // This is required so we can get outside the scope of the previous XHR's success handler. + window.setTimeout(() => { + Joomla.Update.delayedStepExtract(data.instance); + }, 50); + }, + delayedStepExtract: (instance) => { const postData = new FormData(); postData.append('task', 'stepExtract'); postData.append('password', Joomla.Update.password); - if (data.instance) { - postData.append('instance', data.instance); + if (instance) { + postData.append('instance', instance); } Joomla.request({ From 85871c2d81b246d9a2a2cd56e0db4108a676c793 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 15:41:48 +0300 Subject: [PATCH 25/58] Extraction could loop forever --- .../components/com_joomlaupdate/extract.php | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index 849611e71afa9..3bad32e1cf97a 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -114,7 +114,7 @@ class ZIPExtraction /** @var int Internal state when extracting files: finished extracting the ZIP file */ private const AK_STATE_FINISHED = 999; - /** @var null|self Singleton isntance */ + /** @var null|self Singleton instance */ private static $instance = null; /** @var integer The total size of the ZIP archive */ @@ -239,10 +239,10 @@ public static function unserialiseInstance(string $serialised): ?self } $instance = @unserialize($serialised, [ - 'allowed_classes' => [ - self::class, - stdClass::class, - ], + 'allowed_classes' => [ + self::class, + stdClass::class, + ], ] ); @@ -423,7 +423,8 @@ public function initialize(): void return; } - $this->runState = self::AK_STATE_NOFILE; + $this->archiveFileIsBeingRead = true; + $this->runState = self::AK_STATE_NOFILE; } /** @@ -732,8 +733,8 @@ private function readFileHeader(): bool // 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); + $headerData = + unpack('Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/Vuncomp/vfnamelen/veflen', $headerBinary); // Check signature if (!($headerData['sig'] == 0x04034b50)) @@ -815,7 +816,7 @@ private function readFileHeader(): bool default: $messageTemplate = 'This script cannot handle ZIP compression method %d. ' . 'Only 0 (no compression) and 8 (DEFLATE, gzip) can be handled.'; - $actualMessage = sprintf($messageTemplate, $headerData['compmethod']); + $actualMessage = sprintf($messageTemplate, $headerData['compmethod']); $this->setError($actualMessage); return false; @@ -1000,8 +1001,8 @@ private function openArchiveFile(): void if ($this->fp === false) { $message = 'Could not open archive 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.'; + . '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.'; $this->setError($message); return; @@ -1539,12 +1540,12 @@ function getConfiguration(): ?array $engine->setFilename($sourceFile); $engine->setAddPath($destDir); $engine->setSkipFiles([ - 'administrator/components/com_joomlaupdate/restoration.php', - 'administrator/components/com_joomlaupdate/update.php', + 'administrator/components/com_joomlaupdate/restoration.php', + 'administrator/components/com_joomlaupdate/update.php', ] ); $engine->setIgnoreDirectories([ - 'tmp', 'administrator/logs', + 'tmp', 'administrator/logs', ] ); @@ -1568,14 +1569,18 @@ function getConfiguration(): ?array $retArray['files'] = $engine->filesProcessed; $retArray['bytesIn'] = $engine->compressedTotal; $retArray['bytesOut'] = $engine->uncompressedTotal; + $retArray['percent'] = ($engine->totalSize > 0) ? ($engine->uncompressedTotal / $engine->totalSize) : 0; $retArray['status'] = true; $retArray['done'] = true; + + $retArray['percent'] = min($retArray['percent'], 100); } else { $retArray['files'] = $engine->filesProcessed; $retArray['bytesIn'] = $engine->compressedTotal; $retArray['bytesOut'] = $engine->uncompressedTotal; + $retArray['percent'] = 100; $retArray['status'] = true; $retArray['done'] = false; $retArray['instance'] = ZIPExtraction::getSerialised(); From 5a95d3e66df5d08d00fe65864afd7dba13a0c362 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 15:48:14 +0300 Subject: [PATCH 26/58] Make the progress bar danger color on failure --- .../com_joomlaupdate/js/admin-update-default.es6.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js index 56f4d47273036..41653b20f13f5 100644 --- a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js +++ b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js @@ -19,6 +19,10 @@ Joomla.Update = window.Joomla.Update || { genericErrorMessage: (message) => { const header = document.getElementById('errorDialogLabel'); const messageDiv = document.getElementById('errorDialogMessage'); + const elProgress = document.getElementById('progress-bar'); + + elProgress.classList.add('bg-danger'); + elProgress.classList.remove('bg-success'); header.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_GENERIC'); messageDiv.innerHTML = message; @@ -37,6 +41,10 @@ Joomla.Update = window.Joomla.Update || { const isForbidden = xhr.status === 403; const header = document.getElementById('errorDialogLabel'); const message = document.getElementById('errorDialogMessage'); + const elProgress = document.getElementById('progress-bar'); + + elProgress.classList.add('bg-danger'); + elProgress.classList.remove('bg-success'); if (isForbidden) { header.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_FORBIDDEN'); From 888746d25cc5c91d6eb7390f3bba3681d2d797f0 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 15:56:52 +0300 Subject: [PATCH 27/58] Correct the bytes and percentage displayed --- .../js/admin-update-default.es6.js | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js index 41653b20f13f5..5a3e60ca35281 100644 --- a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js +++ b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js @@ -33,7 +33,7 @@ Joomla.Update = window.Joomla.Update || { const myModal = new bootstrap.Modal(document.getElementById('errorDialog'), { keyboard: true, - backdrop: true, + backdrop: true }); myModal.show(); }, @@ -56,7 +56,7 @@ Joomla.Update = window.Joomla.Update || { const myModal = new bootstrap.Modal(document.getElementById('errorDialog'), { keyboard: true, - backdrop: true, + backdrop: true }); myModal.show(); }, @@ -86,7 +86,7 @@ Joomla.Update = window.Joomla.Update || { Joomla.Update.genericErrorMessage(e.message); } }, - onError: Joomla.Update.handleErrorResponse, + onError: Joomla.Update.handleErrorResponse }); }, stepExtract: (data) => { @@ -111,12 +111,14 @@ Joomla.Update = window.Joomla.Update || { } // Add data to variables - Joomla.Update.stat_inbytes += data.bytesIn; - Joomla.Update.stat_percent = (Joomla.Update.stat_inbytes * 100) / Joomla.Update.totalsize; + Joomla.Update.stat_inbytes = data.bytesIn; + Joomla.Update.stat_percent = data.percent; + Joomla.Update.stat_percent = Joomla.Update.stat_percent + || (100 * (Joomla.Update.stat_inbytes / Joomla.Update.totalsize)); // Update GUI - Joomla.Update.stat_outbytes += data.bytesOut; - Joomla.Update.stat_files += data.files; + Joomla.Update.stat_outbytes = data.bytesOut; + Joomla.Update.stat_files = data.files; if (Joomla.Update.stat_percent < 100) { elProgress.classList.remove('bg-success'); @@ -160,7 +162,7 @@ Joomla.Update = window.Joomla.Update || { Joomla.Update.genericErrorMessage(e.message); } }, - onError: Joomla.Update.handleErrorResponse, + onError: Joomla.Update.handleErrorResponse }); }, finalizeUpdate: () => { @@ -175,9 +177,9 @@ Joomla.Update = window.Joomla.Update || { onSuccess: () => { window.location = Joomla.Update.return_url; }, - onError: Joomla.Update.handleErrorResponse, + onError: Joomla.Update.handleErrorResponse }); - }, + } }; const JoomlaUpdateOptions = Joomla.getOptions('joomlaupdate'); From 341390ce016a3e85f1de05b1031e1f13e1eb6761 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 16:59:18 +0300 Subject: [PATCH 28/58] Mark deprecated method --- .../com_joomlaupdate/src/Model/UpdateModel.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index bfead0a8c6c0e..403eac9047de5 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -515,7 +515,16 @@ protected function downloadPackage($url, $target) return basename($target); } - public function createRestorationFile($basename = null) + /** + * Backwards compatibility. Use createUpdateFile() instead. + * + * @param null $basename The basename of the file to create + * + * @return boolean + * @since 2.5.1 + * @deprecated 5.0 + */ + public function createRestorationFile($basename = null): bool { return $this->createUpdateFile($basename); } @@ -534,7 +543,7 @@ public function createRestorationFile($basename = null) * * @since 2.5.4 */ - public function createUpdateFile($basename = null) + public function createUpdateFile($basename = null): bool { // Load overrides plugin. PluginHelper::importPlugin('installer'); From c471811cbf451cd1874df37f46929e570aebc6da Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 17:06:26 +0300 Subject: [PATCH 29/58] Bump version --- administrator/components/com_joomlaupdate/joomlaupdate.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/administrator/components/com_joomlaupdate/joomlaupdate.xml b/administrator/components/com_joomlaupdate/joomlaupdate.xml index 8f2efca0b1423..ba75d2b89e472 100644 --- a/administrator/components/com_joomlaupdate/joomlaupdate.xml +++ b/administrator/components/com_joomlaupdate/joomlaupdate.xml @@ -2,12 +2,12 @@ com_joomlaupdate Joomla! Project - February 2012 + August 2021 (C) 2012 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org www.joomla.org - 4.0.2 + 4.0.3 COM_JOOMLAUPDATE_XML_DESCRIPTION Joomla\Component\Joomlaupdate From b77d1fe8f464acc6931efaca9fd733a07630950a Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 17:08:51 +0300 Subject: [PATCH 30/58] Beautify the update progress page --- .../com_joomlaupdate/tmpl/update/default.php | 53 ++++++++----- .../language/en-GB/com_joomlaupdate.ini | 4 + .../js/admin-update-default.es6.js | 75 +++++++++++-------- 3 files changed, 82 insertions(+), 50 deletions(-) diff --git a/administrator/components/com_joomlaupdate/tmpl/update/default.php b/administrator/components/com_joomlaupdate/tmpl/update/default.php index 41a3c7adde5de..a784c1972c1d7 100644 --- a/administrator/components/com_joomlaupdate/tmpl/update/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/update/default.php @@ -26,6 +26,8 @@ Text::script('COM_JOOMLAUPDATE_ERRORMODAL_BODY_SERVERERROR'); Text::script('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_GENERIC'); Text::script('COM_JOOMLAUPDATE_ERRORMODAL_BODY_INVALIDLOGIN'); +Text::script('COM_JOOMLAUPDATE_UPDATING_FAIL'); +Text::script('COM_JOOMLAUPDATE_UPDATING_COMPLETE'); $password = Factory::getApplication()->getUserState('com_joomlaupdate.password', null); $filesize = Factory::getApplication()->getUserState('com_joomlaupdate.filesize', null); @@ -41,6 +43,8 @@ 'return_url' => $returnUrl, ] ); + +$helpUrl = Help::createUrl('JHELP_COMPONENTS_JOOMLA_UPDATE', false); ?>

@@ -62,7 +66,7 @@ -
-
-
-
-
-
- - -
-
- - +
+ +

+
+

+ +

+
+
+
-
diff --git a/administrator/language/en-GB/com_joomlaupdate.ini b/administrator/language/en-GB/com_joomlaupdate.ini index 7fcad946b3919..d2eaca60726de 100644 --- a/administrator/language/en-GB/com_joomlaupdate.ini +++ b/administrator/language/en-GB/com_joomlaupdate.ini @@ -70,6 +70,10 @@ COM_JOOMLAUPDATE_UPDATE_LOG_FINALISE="Finalising installation." COM_JOOMLAUPDATE_UPDATE_LOG_INSTALL="Starting installation of new version." COM_JOOMLAUPDATE_UPDATE_LOG_START="Update started by user %2$s (%1$s). Old version is %3$s." COM_JOOMLAUPDATE_UPDATE_LOG_URL="Downloading update file from %s." +COM_JOOMLAUPDATE_UPDATING_HEAD="Update in progress" +COM_JOOMLAUPDATE_UPDATING_INPROGRESS="Please wait; Joomla is being updated. This may take a while." +COM_JOOMLAUPDATE_UPDATING_FAIL="The update has failed." +COM_JOOMLAUPDATE_UPDATING_COMPLETE="The update is finalising." COM_JOOMLAUPDATE_VIEW_COMPLETE_HEADING="Joomla Version Update Status" COM_JOOMLAUPDATE_VIEW_COMPLETE_MESSAGE="Your site has been updated. Your Joomla version is now %s." COM_JOOMLAUPDATE_VIEW_DEFAULT_ACTUAL="Actual" diff --git a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js index 5a3e60ca35281..3faea4c0bfcb9 100644 --- a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js +++ b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js @@ -17,14 +17,19 @@ Joomla.Update = window.Joomla.Update || { ajax_url: null, return_url: null, genericErrorMessage: (message) => { - const header = document.getElementById('errorDialogLabel'); + const headerDiv = document.getElementById('errorDialogLabel'); const messageDiv = document.getElementById('errorDialogMessage'); - const elProgress = document.getElementById('progress-bar'); + const progressDiv = document.getElementById('progress-bar'); + const titleDiv = document.getElementById('update-title'); + const helpDiv = document.getElementById('update-help'); - elProgress.classList.add('bg-danger'); - elProgress.classList.remove('bg-success'); + progressDiv.classList.add('bg-danger'); + progressDiv.classList.remove('bg-success'); + titleDiv.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_UPDATING_FAIL'); + helpDiv.classList.remove('d-none'); + helpDiv.classList.add('d-grid'); - header.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_GENERIC'); + headerDiv.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_GENERIC'); messageDiv.innerHTML = message; if (message.toLowerCase() === 'invalid login') { @@ -33,30 +38,35 @@ Joomla.Update = window.Joomla.Update || { const myModal = new bootstrap.Modal(document.getElementById('errorDialog'), { keyboard: true, - backdrop: true + backdrop: true, }); myModal.show(); }, handleErrorResponse: (xhr) => { const isForbidden = xhr.status === 403; - const header = document.getElementById('errorDialogLabel'); - const message = document.getElementById('errorDialogMessage'); - const elProgress = document.getElementById('progress-bar'); + const headerDiv = document.getElementById('errorDialogLabel'); + const messageDiv = document.getElementById('errorDialogMessage'); + const progressDiv = document.getElementById('progress-bar'); + const titleDiv = document.getElementById('update-title'); + const helpDiv = document.getElementById('update-help'); - elProgress.classList.add('bg-danger'); - elProgress.classList.remove('bg-success'); + progressDiv.classList.add('bg-danger'); + progressDiv.classList.remove('bg-success'); + titleDiv.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_UPDATING_FAIL'); + helpDiv.classList.remove('d-none'); + helpDiv.classList.add('d-grid'); if (isForbidden) { - header.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_FORBIDDEN'); - message.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_BODY_FORBIDDEN'); + headerDiv.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_FORBIDDEN'); + messageDiv.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_BODY_FORBIDDEN'); } else { - header.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_SERVERERROR'); - message.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_BODY_SERVERERROR'); + headerDiv.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_SERVERERROR'); + messageDiv.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_ERRORMODAL_BODY_SERVERERROR'); } const myModal = new bootstrap.Modal(document.getElementById('errorDialog'), { keyboard: true, - backdrop: true + backdrop: true, }); myModal.show(); }, @@ -86,7 +96,7 @@ Joomla.Update = window.Joomla.Update || { Joomla.Update.genericErrorMessage(e.message); } }, - onError: Joomla.Update.handleErrorResponse + onError: Joomla.Update.handleErrorResponse, }); }, stepExtract: (data) => { @@ -97,13 +107,15 @@ Joomla.Update = window.Joomla.Update || { return; } - const elProgress = document.getElementById('progress-bar'); + const progressDiv = document.getElementById('progress-bar'); + const titleDiv = document.getElementById('update-title'); // Are we done extracting? if (data.done) { - elProgress.classList.add('bg-success'); - elProgress.style.width = '100%'; - elProgress.setAttribute('aria-valuenow', 100); + progressDiv.classList.add('bg-success'); + progressDiv.style.width = '100%'; + progressDiv.setAttribute('aria-valuenow', 100); + titleDiv.innerHTML = Joomla.Text._('COM_JOOMLAUPDATE_UPDATING_COMPLETE'); Joomla.Update.finalizeUpdate(); @@ -121,16 +133,17 @@ Joomla.Update = window.Joomla.Update || { Joomla.Update.stat_files = data.files; if (Joomla.Update.stat_percent < 100) { - elProgress.classList.remove('bg-success'); - elProgress.style.width = `${Joomla.Update.stat_percent}%`; - elProgress.setAttribute('aria-valuenow', Joomla.Update.stat_percent); + progressDiv.classList.remove('bg-success'); + progressDiv.style.width = `${Joomla.Update.stat_percent}%`; + progressDiv.setAttribute('aria-valuenow', Joomla.Update.stat_percent); } else if (Joomla.Update.stat_percent >= 100) { - elProgress.classList.add('bg-success'); - elProgress.style.width = '100%'; - elProgress.setAttribute('aria-valuenow', 100); + progressDiv.classList.add('bg-success'); + progressDiv.style.width = '100%'; + progressDiv.setAttribute('aria-valuenow', 100); } - document.getElementById('extpercent').innerText = `${Joomla.Update.stat_percent.toFixed(1)}%`; + progressDiv.innerText = `${Joomla.Update.stat_percent.toFixed(1)}%`; + document.getElementById('extbytesin').innerText = Joomla.Update.stat_inbytes; document.getElementById('extbytesout').innerText = Joomla.Update.stat_outbytes; document.getElementById('extfiles').innerText = Joomla.Update.stat_files; @@ -162,7 +175,7 @@ Joomla.Update = window.Joomla.Update || { Joomla.Update.genericErrorMessage(e.message); } }, - onError: Joomla.Update.handleErrorResponse + onError: Joomla.Update.handleErrorResponse, }); }, finalizeUpdate: () => { @@ -177,9 +190,9 @@ Joomla.Update = window.Joomla.Update || { onSuccess: () => { window.location = Joomla.Update.return_url; }, - onError: Joomla.Update.handleErrorResponse + onError: Joomla.Update.handleErrorResponse, }); - } + }, }; const JoomlaUpdateOptions = Joomla.getOptions('joomlaupdate'); From cbb936e078d441cced45f07cfdd88af0823964f7 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 17:21:26 +0300 Subject: [PATCH 31/58] Add since DocBlock tags in extract.php --- .../components/com_joomlaupdate/extract.php | 274 +++++++++++++++--- 1 file changed, 235 insertions(+), 39 deletions(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index 3bad32e1cf97a..645ed2be2cf9d 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -27,7 +27,12 @@ */ class ZIPExtraction { - /** @var int How much data to read at once when processing files */ + /** + * How much data to read at once when processing files + * + * @var int + * @since __DEPLOY_VERSION__ + */ private const CHUNK_SIZE = 524288; /** @@ -41,7 +46,8 @@ class ZIPExtraction * causing the extraction to fail. If this is too big the extraction will not be as verbose and the user might think * something is broken. A value between 3 and 7 seconds is, therefore, recommended. * - * @var int + * @var int + * @since __DEPLOY_VERSION__ */ private const MAX_EXEC_TIME = 4; @@ -65,7 +71,8 @@ class ZIPExtraction * * Lower values make it less likely to overshoot MAX_EXEC_TIME when extracting large files. * - * @var int + * @var int + * @since __DEPLOY_VERSION__ */ private const RUNTIME_BIAS = 90; @@ -86,101 +93,250 @@ class ZIPExtraction * limiting without annoying the user too much but also catering for the most badly configured of shared * hosting. It's a happy medium which works for the majority (~90%) of commercial servers out there. * - * @var int + * @var int + * @since __DEPLOY_VERSION__ */ private const MIN_EXEC_TIME = 3; - /** @var int Internal state when extracting files: we need to be initialised */ + /** + * Internal state when extracting files: we need to be initialised + * + * @var int + * @since __DEPLOY_VERSION__ + */ private const AK_STATE_INITIALIZE = -1; - /** @var int Internal state when extracting files: no file currently being extracted */ + /** + * Internal state when extracting files: no file currently being extracted + * + * @var int + * @since __DEPLOY_VERSION__ + */ private const AK_STATE_NOFILE = 0; - /** @var int Internal state when extracting files: reading the file header */ + /** + * Internal state when extracting files: reading the file header + * + * @var int + * @since __DEPLOY_VERSION__ + */ private const AK_STATE_HEADER = 1; - /** @var int Internal state when extracting files: reading file data */ + /** + * Internal state when extracting files: reading file data + * + * @var int + * @since __DEPLOY_VERSION__ + */ private const AK_STATE_DATA = 2; - /** @var int Internal state when extracting files: file data has been read thoroughly */ + /** + * Internal state when extracting files: file data has been read thoroughly + * + * @var int + * @since __DEPLOY_VERSION__ + */ private const AK_STATE_DATAREAD = 3; - /** @var int Internal state when extracting files: post-processing the file */ + /** + * Internal state when extracting files: post-processing the file + * + * @var int + * @since __DEPLOY_VERSION__ + */ private const AK_STATE_POSTPROC = 4; - /** @var int Internal state when extracting files: done with this file */ + /** + * Internal state when extracting files: done with this file + * + * @var int + * @since __DEPLOY_VERSION__ + */ private const AK_STATE_DONE = 5; - /** @var int Internal state when extracting files: finished extracting the ZIP file */ + /** + * Internal state when extracting files: finished extracting the ZIP file + * + * @var int + * @since __DEPLOY_VERSION__ + */ private const AK_STATE_FINISHED = 999; - /** @var null|self Singleton instance */ + /** + * Singleton instance + * + * @var null|self + * @since __DEPLOY_VERSION__ + */ private static $instance = null; - /** @var integer The total size of the ZIP archive */ + /** + * The total size of the ZIP archive + * + * @var integer + * @since __DEPLOY_VERSION__ + */ public $totalSize = []; - /** @var array Which files to skip */ + /** + * Which files to skip + * + * @var array + * @since __DEPLOY_VERSION__ + */ public $skipFiles = []; - /** @var integer Current tally of compressed size read */ + /** + * Current tally of compressed size read + * + * @var integer + * @since __DEPLOY_VERSION__ + */ public $compressedTotal = 0; - /** @var integer Current tally of bytes written to disk */ + /** + * Current tally of bytes written to disk + * + * @var integer + * @since __DEPLOY_VERSION__ + */ public $uncompressedTotal = 0; - /** @var integer Current tally of files extracted */ + /** + * Current tally of files extracted + * + * @var integer + * @since __DEPLOY_VERSION__ + */ public $filesProcessed = 0; - /** @var integer Maximum execution time allowance per step */ + /** + * Maximum execution time allowance per step + * + * @var integer + * @since __DEPLOY_VERSION__ + */ private $maxExecTime = null; - /** @var integer Timestamp of execution start */ + /** + * Timestamp of execution start + * + * @var integer + * @since __DEPLOY_VERSION__ + */ private $startTime = null; - /** @var string|null The last error message */ + /** + * The last error message + * + * @var string|null + * @since __DEPLOY_VERSION__ + */ private $lastErrorMessage = null; - /** @var string Archive filename */ + /** + * Archive filename + * + * @var string + * @since __DEPLOY_VERSION__ + */ private $filename = null; - /** @var boolean Current archive part number */ + /** + * Current archive part number + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ private $archiveFileIsBeingRead = false; - /** @var integer The offset inside the current part */ + /** + * The offset inside the current part + * + * @var integer + * @since __DEPLOY_VERSION__ + */ private $currentOffset = 0; - /** @var string Absolute path to prepend to extracted files */ + /** + * Absolute path to prepend to extracted files + * + * @var string + * @since __DEPLOY_VERSION__ + */ private $addPath = ''; - /** @var resource File pointer to the current archive part file */ + /** + * File pointer to the current archive part file + * + * @var resource|null + * @since __DEPLOY_VERSION__ + */ private $fp = null; - /** @var integer Run state when processing the current archive file */ + /** + * Run state when processing the current archive file + * + * @var integer + * @since __DEPLOY_VERSION__ + */ private $runState = self::AK_STATE_INITIALIZE; - /** @var stdClass File header data, as read by the readFileHeader() method */ + /** + * File header data, as read by the readFileHeader() method + * + * @var stdClass + * @since __DEPLOY_VERSION__ + */ private $fileHeader = null; - /** @var integer How much of the uncompressed data we've read so far */ + /** + * How much of the uncompressed data we've read so far + * + * @var integer + * @since __DEPLOY_VERSION__ + */ private $dataReadLength = 0; - /** @var array Unwritable files in these directories are always ignored and do not cause errors when not extracted */ + /** + * Unwritable files in these directories are always ignored and do not cause errors when not + * extracted. + * + * @var array + * @since __DEPLOY_VERSION__ + */ private $ignoreDirectories = []; - /** @var boolean Internal flag, set when the ZIP file has a data descriptor (which we will be ignoring) */ + /** + * Internal flag, set when the ZIP file has a data descriptor (which we will be ignoring) + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ private $expectDataDescriptor = false; - /** @var integer The UNIX last modification timestamp of the file last extracted */ + /** + * The UNIX last modification timestamp of the file last extracted + * + * @var integer + * @since __DEPLOY_VERSION__ + */ private $lastExtractedFileTimestamp = 0; - /** @var string The file path of the file last extracted */ + /** + * The file path of the file last extracted + * + * @var string + * @since __DEPLOY_VERSION__ + */ private $lastExtractedFilename = null; /** * Public constructor. * * Sets up the internal timer. + * + * @since __DEPLOY_VERSION__ */ public function __construct() { @@ -194,6 +350,7 @@ public function __construct() * Singleton implementation. * * @return static + * @since __DEPLOY_VERSION__ */ public static function getInstance(): self { @@ -212,6 +369,7 @@ public static function getInstance(): self * call to shutdown() first so any open files are closed first. * * @return string The serialised data, potentially base64 encoded. + * @since __DEPLOY_VERSION__ */ public static function getSerialised(): string { @@ -230,6 +388,7 @@ public static function getSerialised(): string * @param string $serialised The serialised data, potentially base64 encoded, to deserialize. * * @return static|null The instance of the object, NULL if it cannot be deserialised. + * @since __DEPLOY_VERSION__ */ public static function unserialiseInstance(string $serialised): ?self { @@ -265,6 +424,7 @@ public static function unserialiseInstance(string $serialised): ?self * - Seek to the correct offset of the file. * * @return void + * @since __DEPLOY_VERSION__ * @internal */ public function __wakeup(): void @@ -289,6 +449,7 @@ public function __wakeup(): void * Enforce the minimum execution time. * * @return void + * @since __DEPLOY_VERSION__ */ public function enforceMinimumExecutionTime() { @@ -347,6 +508,7 @@ public function enforceMinimumExecutionTime() * @param string $value The filepath to the archive. Only LOCAL files are allowed! * * @return void + * @since __DEPLOY_VERSION__ */ public function setFilename(string $value) { @@ -367,6 +529,7 @@ public function setFilename(string $value) * @param string $addPath The path where the archive will be extracted. * * @return void + * @since __DEPLOY_VERSION__ */ public function setAddPath(string $addPath): void { @@ -386,6 +549,7 @@ public function setAddPath(string $addPath): void * @param array $skipFiles A list of files to skip when extracting the ZIP archive * * @return void + * @since __DEPLOY_VERSION__ */ public function setSkipFiles(array $skipFiles): void { @@ -398,6 +562,7 @@ public function setSkipFiles(array $skipFiles): void * @param array $ignoreDirectories The list of directories to ignore. * * @return void + * @since __DEPLOY_VERSION__ */ public function setIgnoreDirectories(array $ignoreDirectories): void { @@ -408,6 +573,7 @@ public function setIgnoreDirectories(array $ignoreDirectories): void * Prepares for the archive extraction * * @return void + * @since __DEPLOY_VERSION__ */ public function initialize(): void { @@ -431,6 +597,7 @@ public function initialize(): void * Executes a step of the archive extraction * * @return boolean True if we are done extracting or an error occurred + * @since __DEPLOY_VERSION__ */ public function step(): bool { @@ -502,6 +669,7 @@ public function step(): bool * Get the most recent error message * * @return string|null The message string, null if there's no error + * @since __DEPLOY_VERSION__ */ public function getError(): ?string { @@ -512,6 +680,7 @@ public function getError(): ?string * Gets the number of seconds left, before we hit the "must break" threshold * * @return float + * @since __DEPLOY_VERSION__ */ private function getTimeLeft(): float { @@ -523,6 +692,7 @@ private function getTimeLeft(): float * long Akeeba Engine has been processing data * * @return float + * @since __DEPLOY_VERSION__ */ private function getRunningTime(): float { @@ -535,6 +705,7 @@ private function getRunningTime(): float * This invalidates OPcache for .php files. Also applies the correct permissions and timestamp. * * @return void + * @since __DEPLOY_VERSION__ */ private function processLastExtractedFile(): void { @@ -561,6 +732,7 @@ private function processLastExtractedFile(): void * @param string|null $lastExtractedFilename The last extracted filename * * @return void + * @since __DEPLOY_VERSION__ */ private function setLastExtractedFilename(?string $lastExtractedFilename): void { @@ -573,6 +745,7 @@ private function setLastExtractedFilename(?string $lastExtractedFilename): void * @param int $lastExtractedFileTimestamp The timestamp * * @return void + * @since __DEPLOY_VERSION__ */ private function setLastExtractedFileTimestamp(int $lastExtractedFileTimestamp): void { @@ -583,6 +756,7 @@ private function setLastExtractedFileTimestamp(int $lastExtractedFileTimestamp): * Sleep function, called whenever the class is serialized * * @return void + * @since __DEPLOY_VERSION__ * @internal */ private function shutdown(): void @@ -603,6 +777,7 @@ private function shutdown(): void * @param string|null $string The binary data to get the length for * * @return integer + * @since __DEPLOY_VERSION__ */ private function binStringLength(?string $string): int { @@ -625,6 +800,7 @@ private function binStringLength(?string $string): int * @param string $error Error message * * @return void + * @since __DEPLOY_VERSION__ */ private function setError(string $error): void { @@ -638,6 +814,7 @@ private function setError(string $error): void * @param int|null $length The volume of data to read, in bytes * * @return string The data read from the file + * @since __DEPLOY_VERSION__ */ private function fread($fp, ?int $length = null): string { @@ -656,6 +833,7 @@ private function fread($fp, ?int $length = null): string * Read the header of the archive, making sure it's a valid ZIP file. * * @return void + * @since __DEPLOY_VERSION__ */ private function readArchiveHeader(): void { @@ -691,7 +869,9 @@ private function readArchiveHeader(): void /** * Concrete classes must use this method to read the file header * - * @return boolean True if reading the file was successful, false if an error occurred or we reached end of archive + * @return boolean True if reading the file was successful, false if an error occurred or we + * reached end of archive. + * @since __DEPLOY_VERSION__ */ private function readFileHeader(): bool { @@ -909,6 +1089,7 @@ private function readFileHeader(): bool * Creates the directory this file points to * * @return void + * @since __DEPLOY_VERSION__ */ private function createDirectory(): void { @@ -939,7 +1120,8 @@ private function createDirectory(): void * Concrete classes must use this method to process file data. It must set $runState to self::AK_STATE_DATAREAD when * it's finished processing the file data. * - * @return boolean True if processing the file data was successful, false if an error occurred + * @return boolean True if processing the file data was successful, false if an error occurred + * @since __DEPLOY_VERSION__ */ private function processFileData(): bool { @@ -983,6 +1165,7 @@ private function processFileData(): bool * Opens the next part file for reading * * @return void + * @since __DEPLOY_VERSION__ */ private function openArchiveFile(): void { @@ -1016,7 +1199,8 @@ private function openArchiveFile(): void /** * Returns true if we have reached the end of file * - * @return boolean True if we have reached End Of File + * @return boolean True if we have reached End Of File + * @since __DEPLOY_VERSION__ */ private function isEOF(): bool { @@ -1034,6 +1218,7 @@ private function isEOF(): bool * @param string $path A path to a file * * @return void + * @since __DEPLOY_VERSION__ */ private function setCorrectPermissions(string $path): void { @@ -1064,6 +1249,7 @@ private function setCorrectPermissions(string $path): void * @param string $shortFilename The relative path of the file/directory in the package * * @return boolean True if it belongs in an ignored directory + * @since __DEPLOY_VERSION__ */ private function isIgnoredDirectory(string $shortFilename): bool { @@ -1076,6 +1262,7 @@ private function isIgnoredDirectory(string $shortFilename): bool * Process the file data of a directory entry * * @return boolean + * @since __DEPLOY_VERSION__ */ private function processTypeDir(): bool { @@ -1089,6 +1276,7 @@ private function processTypeDir(): bool * Process the file data of a link entry * * @return boolean + * @since __DEPLOY_VERSION__ */ private function processTypeLink(): bool { @@ -1144,6 +1332,7 @@ private function processTypeLink(): bool * Processes an uncompressed (stored) file * * @return boolean + * @since __DEPLOY_VERSION__ */ private function processTypeFileUncompressed(): bool { @@ -1237,6 +1426,7 @@ private function processTypeFileUncompressed(): bool * Processes a compressed file * * @return boolean + * @since __DEPLOY_VERSION__ */ private function processTypeFileCompressed(): bool { @@ -1324,6 +1514,7 @@ private function processTypeFileCompressed(): bool * Set up the maximum execution time * * @return void + * @since __DEPLOY_VERSION__ */ private function setupMaxExecTime(): void { @@ -1337,7 +1528,8 @@ private function setupMaxExecTime(): void * * If it's not defined or it's zero (infinite) we use a fake value of 10 seconds. * - * @return integer + * @return integer + * @since __DEPLOY_VERSION__ */ private function getPhpMaxExecTime(): int { @@ -1367,6 +1559,7 @@ private function getPhpMaxExecTime(): int * @param string $file The filepath to clear from OPcache * * @return boolean + * @since __DEPLOY_VERSION__ */ function clearFileInOPCache(string $file): bool { @@ -1399,6 +1592,7 @@ function clearFileInOPCache(string $file): bool * @param string $user The user submitted value to check * * @return boolean True if the two strings are identical. + * @since __DEPLOY_VERSION__ * * @see http://blog.ircmaxell.com/2014/11/its-all-about-time.html */ @@ -1429,9 +1623,11 @@ function timingSafeEquals($known, $user) } /** - * Gets the configuration parameters from the update.php file and validates the password sent with the request. + * Gets the configuration parameters from the update.php file and validates the password sent with + * the request. * - * @return array|null The configuration parameters to use. NULL if this is an invalid request. + * @return array|null The configuration parameters to use. NULL if this is an invalid request. + * @since __DEPLOY_VERSION__ */ function getConfiguration(): ?array { From 918f7fc9d6f554016669ce7d7ecb25ea7123b027 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 17:33:43 +0300 Subject: [PATCH 32/58] Various small fixes Per @PhilETaylor suggestions --- .../components/com_joomlaupdate/extract.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index 645ed2be2cf9d..4614aaaa64f26 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -224,7 +224,7 @@ class ZIPExtraction * @var integer * @since __DEPLOY_VERSION__ */ - private $startTime = null; + private $startTime; /** * The last error message @@ -713,7 +713,7 @@ private function processLastExtractedFile(): void { @chmod($this->lastExtractedFilename, 0644); - clearFileInOPCache($this->lastExtractedFilename, true); + clearFileInOPCache($this->lastExtractedFilename); } else { @@ -913,8 +913,9 @@ private function readFileHeader(): bool // 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); + $format = 'Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/' + . 'Vuncomp/vfnamelen/veflen'; + $headerData = unpack($format, $headerBinary); // Check signature if (!($headerData['sig'] == 0x04034b50)) @@ -1295,7 +1296,7 @@ private function processTypeLink(): bool if ($reallyReadBytes < $toReadBytes) { // We read less than requested! - if ($this->isEOF(true) && !$this->isEOF(false)) + if ($this->isEOF()) { $this->setError('The archive file is corrupt or truncated'); @@ -1304,7 +1305,7 @@ private function processTypeLink(): bool } } - $filename = isset($this->fileHeader->realFile) ? $this->fileHeader->realFile : $this->fileHeader->file; + $filename = $this->fileHeader->realFile ?? $this->fileHeader->file; // Try to remove an existing file or directory by the same name if (file_exists($filename)) @@ -1386,7 +1387,7 @@ private function processTypeFileUncompressed(): bool if ($reallyReadBytes < $toReadBytes) { // We read less than requested! Why? Did we hit local EOF? - if ($this->isEOF(true) && !$this->isEOF(false)) + if ($this->isEOF()) { // Nope. The archive is corrupt $this->setError('The archive file is corrupt or truncated'); @@ -1574,7 +1575,7 @@ function clearFileInOPCache(string $file): bool if ($hasOpCache && (strtolower(substr($file, -4)) === '.php')) { - return \opcache_invalidate($file, true); + return opcache_invalidate($file, true); } return false; From 55f967952bbd530e0817afc59ba9a9a031de4f17 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 17:37:50 +0300 Subject: [PATCH 33/58] Reset OPcache before unlink --- administrator/components/com_joomlaupdate/extract.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index 4614aaaa64f26..16bce556b7b80 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -1310,6 +1310,7 @@ private function processTypeLink(): bool // Try to remove an existing file or directory by the same name if (file_exists($filename)) { + clearFileInOPCache($filename); @unlink($filename); @rmdir($filename); } @@ -1791,6 +1792,7 @@ function getConfiguration(): ?array $root = $configuration['setup.destdir'] ?? ''; // Remove update.php + clearFileInOPCache($basePath . 'update.php'); @unlink($basePath . 'update.php'); // Import a custom finalisation file From d78475af8ad3df05905aeea52ab389711c7aeb78 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 22:27:45 +0300 Subject: [PATCH 34/58] No bang Co-authored-by: Brian Teeman --- administrator/language/en-GB/com_joomlaupdate.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/language/en-GB/com_joomlaupdate.ini b/administrator/language/en-GB/com_joomlaupdate.ini index d2eaca60726de..7493feec80d8c 100644 --- a/administrator/language/en-GB/com_joomlaupdate.ini +++ b/administrator/language/en-GB/com_joomlaupdate.ini @@ -22,7 +22,7 @@ COM_JOOMLAUPDATE_EMPTYSTATE_CONTENT="Select the button below to check for update COM_JOOMLAUPDATE_EMPTYSTATE_TITLE="Check if an update is available." COM_JOOMLAUPDATE_ERRORMODAL_BODY_FORBIDDEN="

Joomla cannot communicate with the file administrator/components/com_joomlaupdate/extract.php which performs the update. This typically happens because of one of the following reasons:

  • A server configuration file in your site's root, such as .htacess or web.config, prevents access to this file.
  • Your server configuration prevents access to this file.
  • The ownership and permissions of this file do not allow your server to access this file.
  • Your site is behind a load balancer or CDN but its configuration does not allow access to this file or blocks sending commands to it.
  • Even though the file can be accessed, sending commands to it is blocked by your server's protection such as mod_security2.

If you are not sure please contact your host.

" COM_JOOMLAUPDATE_ERRORMODAL_BODY_INVALIDLOGIN="

Joomla Update cannot communicate correctly with the administrator/components/com_joomlaupdate/extract.php which performs the update. This typically happens because of one of the following reasons:

  • The file administrator/components/com_joomlaupdate/update.php, containing the instructions for applying the update, could not be created e.g. because of permissions problems.
  • The file mentioned above cannot be read by the extract.php script because of permissions problems.
  • Your browser, network gear or server is corrupting the password we are sending with each request to extract.php for security reasons

Please try using a different browser, without any browser extensions installed, ideally on a different computer connected to the Internet using a different provider. If the error persists you may want to contact your host.

" -COM_JOOMLAUPDATE_ERRORMODAL_BODY_SERVERERROR="

Joomla Update encountered a server error when applying the update to your site.

Your site may not have been upgraded completely to the new Joomla! version.

" +COM_JOOMLAUPDATE_ERRORMODAL_BODY_SERVERERROR="

Joomla Update encountered a server error when applying the update to your site.

Your site may not have been upgraded completely to the new Joomla version.

" COM_JOOMLAUPDATE_ERRORMODAL_HEAD_FORBIDDEN="Access forbidden" COM_JOOMLAUPDATE_ERRORMODAL_HEAD_GENERIC="An error occurred" COM_JOOMLAUPDATE_ERRORMODAL_HEAD_SERVERERROR="Server error" From 54bfa614e194151d4082bc56e9b501824ecbb8b2 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 27 Aug 2021 23:26:31 +0300 Subject: [PATCH 35/58] Reset OPcache before unlink --- .../components/com_joomlaupdate/src/Model/UpdateModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 403eac9047de5..dd49e302a7046 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -592,8 +592,8 @@ public function createUpdateFile($basename = null): bool { if (!File::delete($configpath)) { - @unlink($configpath); File::invalidateFileCache($configpath); + @unlink($configpath); } } From fe88d22d91852add21d74e11a789d03803d28729 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Sat, 28 Aug 2021 12:40:51 +0300 Subject: [PATCH 36/58] Show running file sizes in KiB/MiB etc --- .../com_joomlaupdate/tmpl/update/default.php | 9 +++++++ administrator/language/en-GB/joomla.ini | 11 ++++++++ .../js/admin-update-default.es6.js | 25 +++++++++++++++++-- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/administrator/components/com_joomlaupdate/tmpl/update/default.php b/administrator/components/com_joomlaupdate/tmpl/update/default.php index a784c1972c1d7..c741ffa6dfc86 100644 --- a/administrator/components/com_joomlaupdate/tmpl/update/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/update/default.php @@ -28,6 +28,15 @@ Text::script('COM_JOOMLAUPDATE_ERRORMODAL_BODY_INVALIDLOGIN'); Text::script('COM_JOOMLAUPDATE_UPDATING_FAIL'); Text::script('COM_JOOMLAUPDATE_UPDATING_COMPLETE'); +Text::script('JLIB_SIZE_BYTES'); +Text::script('JLIB_SIZE_KB'); +Text::script('JLIB_SIZE_MB'); +Text::script('JLIB_SIZE_GB'); +Text::script('JLIB_SIZE_TB'); +Text::script('JLIB_SIZE_PB'); +Text::script('JLIB_SIZE_EB'); +Text::script('JLIB_SIZE_ZB'); +Text::script('JLIB_SIZE_YB'); $password = Factory::getApplication()->getUserState('com_joomlaupdate.password', null); $filesize = Factory::getApplication()->getUserState('com_joomlaupdate.filesize', null); diff --git a/administrator/language/en-GB/joomla.ini b/administrator/language/en-GB/joomla.ini index 2ba39e4e88657..5e65136c8b1d5 100644 --- a/administrator/language/en-GB/joomla.ini +++ b/administrator/language/en-GB/joomla.ini @@ -1145,3 +1145,14 @@ TRASH="Trash" TRASHED="Trashed" UNPUBLISH="Unpublish" UNPUBLISHED="Unpublished" + +; Units of measurement for file sizes. Note: 1 KiB = 1024 bytes. +JLIB_SIZE_BYTES="Bytes" +JLIB_SIZE_KB="KiB" +JLIB_SIZE_MB="MiB" +JLIB_SIZE_GB="GiB" +JLIB_SIZE_TB="TiB" +JLIB_SIZE_PB="PiB" +JLIB_SIZE_EB="EiB" +JLIB_SIZE_ZB="ZiB" +JLIB_SIZE_YB="YiB" diff --git a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js index 3faea4c0bfcb9..c738d02e37cfa 100644 --- a/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js +++ b/build/media_source/com_joomlaupdate/js/admin-update-default.es6.js @@ -123,13 +123,13 @@ Joomla.Update = window.Joomla.Update || { } // Add data to variables - Joomla.Update.stat_inbytes = data.bytesIn; + Joomla.Update.stat_inbytes = Joomla.Update.formatBytes(data.bytesIn); Joomla.Update.stat_percent = data.percent; Joomla.Update.stat_percent = Joomla.Update.stat_percent || (100 * (Joomla.Update.stat_inbytes / Joomla.Update.totalsize)); // Update GUI - Joomla.Update.stat_outbytes = data.bytesOut; + Joomla.Update.stat_outbytes = Joomla.Update.formatBytes(data.bytesOut); Joomla.Update.stat_files = data.files; if (Joomla.Update.stat_percent < 100) { @@ -193,6 +193,27 @@ Joomla.Update = window.Joomla.Update || { onError: Joomla.Update.handleErrorResponse, }); }, + formatBytes: (bytes, decimals = 2) => { + if (bytes === 0) return `0 ${Joomla.Text._('JLIB_SIZE_BYTES')}`; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = [ + Joomla.Text._('JLIB_SIZE_BYTES'), + Joomla.Text._('JLIB_SIZE_KB'), + Joomla.Text._('JLIB_SIZE_MB'), + Joomla.Text._('JLIB_SIZE_GB'), + Joomla.Text._('JLIB_SIZE_TB'), + Joomla.Text._('JLIB_SIZE_PB'), + Joomla.Text._('JLIB_SIZE_EB'), + Joomla.Text._('JLIB_SIZE_ZB'), + Joomla.Text._('JLIB_SIZE_YB'), + ]; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / (k ** i)).toFixed(dm))} ${sizes[i]}`; + }, }; const JoomlaUpdateOptions = Joomla.getOptions('joomlaupdate'); From 984a5adf0996a7c6cb0fc48eea4fb7039f1f9e32 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Sat, 28 Aug 2021 12:41:11 +0300 Subject: [PATCH 37/58] Fix missing icon for bytes written --- .../components/com_joomlaupdate/tmpl/update/default.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/tmpl/update/default.php b/administrator/components/com_joomlaupdate/tmpl/update/default.php index c741ffa6dfc86..4ca201179b660 100644 --- a/administrator/components/com_joomlaupdate/tmpl/update/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/update/default.php @@ -106,7 +106,7 @@ class="btn btn-info">
- +
From 02c179b12f767ccd97e68223243629935e5956c1 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Sat, 28 Aug 2021 18:37:55 +0300 Subject: [PATCH 38/58] Move language strings around --- administrator/language/en-GB/joomla.ini | 11 ----------- administrator/language/en-GB/lib_joomla.ini | 11 +++++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/administrator/language/en-GB/joomla.ini b/administrator/language/en-GB/joomla.ini index 5e65136c8b1d5..2ba39e4e88657 100644 --- a/administrator/language/en-GB/joomla.ini +++ b/administrator/language/en-GB/joomla.ini @@ -1145,14 +1145,3 @@ TRASH="Trash" TRASHED="Trashed" UNPUBLISH="Unpublish" UNPUBLISHED="Unpublished" - -; Units of measurement for file sizes. Note: 1 KiB = 1024 bytes. -JLIB_SIZE_BYTES="Bytes" -JLIB_SIZE_KB="KiB" -JLIB_SIZE_MB="MiB" -JLIB_SIZE_GB="GiB" -JLIB_SIZE_TB="TiB" -JLIB_SIZE_PB="PiB" -JLIB_SIZE_EB="EiB" -JLIB_SIZE_ZB="ZiB" -JLIB_SIZE_YB="YiB" diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index a3dea5b645bfc..5f718b1de0c14 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -740,3 +740,14 @@ JLIB_UTIL_ERROR_CONNECT_DATABASE="JDatabase: :getInstance: Could not connect to JLIB_UTIL_ERROR_DOMIT="DommitDocument is deprecated. Use DomDocument instead." JLIB_UTIL_ERROR_LOADING_FEED_DATA="Error loading feed data." JLIB_UTIL_ERROR_XML_LOAD="Failed loading XML file." + +; Units of measurement for file sizes. Note: 1 KiB = 1024 bytes. +JLIB_SIZE_BYTES="Bytes" +JLIB_SIZE_KB="KiB" +JLIB_SIZE_MB="MiB" +JLIB_SIZE_GB="GiB" +JLIB_SIZE_TB="TiB" +JLIB_SIZE_PB="PiB" +JLIB_SIZE_EB="EiB" +JLIB_SIZE_ZB="ZiB" +JLIB_SIZE_YB="YiB" From 228b997c5d81694b4083d0222146a46bcfd77252 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Sat, 28 Aug 2021 18:39:31 +0300 Subject: [PATCH 39/58] Remove useless message --- .../components/com_joomlaupdate/tmpl/update/default.php | 2 -- administrator/language/en-GB/com_joomlaupdate.ini | 1 - 2 files changed, 3 deletions(-) diff --git a/administrator/components/com_joomlaupdate/tmpl/update/default.php b/administrator/components/com_joomlaupdate/tmpl/update/default.php index 4ca201179b660..7fc1dee745e75 100644 --- a/administrator/components/com_joomlaupdate/tmpl/update/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/update/default.php @@ -56,8 +56,6 @@ $helpUrl = Help::createUrl('JHELP_COMPONENTS_JOOMLA_UPDATE', false); ?> -

-