From 10e8f8c484980a88a298faf394c6eee8b688cd96 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:32:17 +0100 Subject: [PATCH 01/54] Added after package download events. --- .../Installer/AfterPackageDownloadEvent.php | 25 +++++++++++++++++++ .../AfterPackageDownloadFailedEvent.php | 25 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 libraries/src/Event/Installer/AfterPackageDownloadEvent.php create mode 100644 libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php diff --git a/libraries/src/Event/Installer/AfterPackageDownloadEvent.php b/libraries/src/Event/Installer/AfterPackageDownloadEvent.php new file mode 100644 index 0000000000000..67f94cae3b858 --- /dev/null +++ b/libraries/src/Event/Installer/AfterPackageDownloadEvent.php @@ -0,0 +1,25 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Event\Installer; + +use Joomla\Event\Event; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Class for Installer events + * + * @since 6.1.0 + */ +class AfterPackageDownloadEvent extends Event +{ +} diff --git a/libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php b/libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php new file mode 100644 index 0000000000000..99e7736808b27 --- /dev/null +++ b/libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php @@ -0,0 +1,25 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Event\Installer; + +use Joomla\Event\Event; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Class for Installer events + * + * @since 6.1.0 + */ +class AfterPackageDownloadFailedEvent extends Event +{ +} From 4a01d1e4413d33b9f9e764136e1e160ac3baacb5 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:43:02 +0100 Subject: [PATCH 02/54] Update UpdateAdapter.php --- libraries/src/Updater/UpdateAdapter.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/src/Updater/UpdateAdapter.php b/libraries/src/Updater/UpdateAdapter.php index ee84ff777693b..e7f040facd634 100644 --- a/libraries/src/Updater/UpdateAdapter.php +++ b/libraries/src/Updater/UpdateAdapter.php @@ -300,6 +300,12 @@ protected function getUpdateSiteResponse($options = []) $newUrl = $event->getArgument('url', $url); $headers = $event->getArgument('headers', $headers); + if (empty($newUrl)) + { + // Any logging and messaging of this are the responsibility of the event handlers. + return false; + } + // Http transport throws an exception when there's no response. try { $http = (new HttpFactory())->getHttp($httpOption); From 532596dcbacb55fcbff9b0397eb414afd6189d19 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:51:13 +0100 Subject: [PATCH 03/54] Update InstallerHelper.php --- libraries/src/Installer/InstallerHelper.php | 45 ++++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 5914d16910d25..12119e2f58d24 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -10,6 +10,7 @@ namespace Joomla\CMS\Installer; use Joomla\Archive\Archive; +use Joomla\CMS\Event\Installer\AfterPackageDownloadEvent; use Joomla\CMS\Event\Installer\BeforePackageDownloadEvent; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; @@ -85,18 +86,52 @@ public static function downloadPackage($url, $target = false) $url = $event->getArgument('url', $url); $headers = $event->getArgument('headers', $headers); + if (empty($url)) + { + // Any logging and messaging of this are the responsibility of the event handlers. + return false; + } + try { - $response = (new HttpFactory())->getHttp()->get($url, $headers); + $response = HttpFactory::getHttp()->get($url, $headers); + + // Convert keys of headers to lowercase, to accommodate for case variations + $headers = array_change_key_case($response->headers, CASE_LOWER); } catch (\RuntimeException $exception) { - Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $exception->getMessage()), Log::WARNING, 'jerror'); + $response = $exception; + } + // Load installer plugins, and check response + $headers = []; + $dispatcher = Factory::getApplication()->getDispatcher(); + PluginHelper::importPlugin('installer', null, true, $dispatcher); + $event = new AfterPackageDownloadEvent('onInstallerAfterPackageDownload', [ + 'url' => &$url, + 'response' => &$response, + 'headers' => &$headers, + 'return' => true, + ]); + $dispatcher->dispatch('onInstallerAfterPackageDownload', $event); + $return = $event->getArgument('return'); + + if ($return === false) + { return false; } - // Convert keys of headers to lowercase, to accommodate for case variations - $headers = array_change_key_case($response->getHeaders(), CASE_LOWER); + if ($return !== true) + { + return $return; + } + + if ($response instanceof \Exception) + { + Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $exception->getMessage()), Log::WARNING, 'jerror'); + + return false; + } - if (302 == $response->getStatusCode() && !empty($headers['location'])) { + if (302 == $response->code && !empty($headers['location'])) { return self::downloadPackage($headers['location']); } From 129244947a2a20718ddc097eee6529e76b582ff8 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:09:34 +0100 Subject: [PATCH 04/54] Update InstallerHelper.php --- libraries/src/Installer/InstallerHelper.php | 667 ++++++++++---------- 1 file changed, 335 insertions(+), 332 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 12119e2f58d24..07b51640f6392 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -13,6 +13,7 @@ use Joomla\CMS\Event\Installer\AfterPackageDownloadEvent; use Joomla\CMS\Event\Installer\BeforePackageDownloadEvent; use Joomla\CMS\Factory; +use Joomla\CMS\Http\HttpFactory; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Plugin\PluginHelper; @@ -21,7 +22,6 @@ use Joomla\Filesystem\File; use Joomla\Filesystem\Folder; use Joomla\Filesystem\Path; -use Joomla\Http\HttpFactory; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; @@ -34,57 +34,57 @@ */ abstract class InstallerHelper { - /** - * Hash not validated identifier. - * - * @var integer - * @since 3.9.0 - */ - public const HASH_NOT_VALIDATED = 0; - - /** - * Hash validated identifier. - * - * @var integer - * @since 3.9.0 - */ - public const HASH_VALIDATED = 1; - - /** - * Hash not provided identifier. - * - * @var integer - * @since 3.9.0 - */ - public const HASH_NOT_PROVIDED = 2; - - /** - * Downloads a package - * - * @param string $url URL of file to download - * @param string|bool $target Download target filename or false to get the filename from the URL - * - * @return string|boolean Path to downloaded package or boolean false on failure - * - * @since 3.1 - */ - public static function downloadPackage($url, $target = false) - { - // Set user agent - $version = new Version(); - ini_set('user_agent', $version->getUserAgent('Installer')); - - // Load installer plugins, and allow URL and headers modification - $headers = []; - $dispatcher = Factory::getApplication()->getDispatcher(); - PluginHelper::importPlugin('installer', null, true, $dispatcher); - $event = new BeforePackageDownloadEvent('onInstallerBeforePackageDownload', [ - 'url' => &$url, // @todo: Remove reference in Joomla 7, see BeforePackageDownloadEvent::__constructor() - 'headers' => &$headers, // @todo: Remove reference in Joomla 7, see BeforePackageDownloadEvent::__constructor() - ]); - $dispatcher->dispatch('onInstallerBeforePackageDownload', $event); - $url = $event->getArgument('url', $url); - $headers = $event->getArgument('headers', $headers); + /** + * Hash not validated identifier. + * + * @var integer + * @since 3.9.0 + */ + public const HASH_NOT_VALIDATED = 0; + + /** + * Hash validated identifier. + * + * @var integer + * @since 3.9.0 + */ + public const HASH_VALIDATED = 1; + + /** + * Hash not provided identifier. + * + * @var integer + * @since 3.9.0 + */ + public const HASH_NOT_PROVIDED = 2; + + /** + * Downloads a package + * + * @param string $url URL of file to download + * @param string|bool $target Download target filename or false to get the filename from the URL + * + * @return string|boolean Path to downloaded package or boolean false on failure + * + * @since 3.1 + */ + public static function downloadPackage($url, $target = false) + { + // Set user agent + $version = new Version(); + ini_set('user_agent', $version->getUserAgent('Installer')); + + // Load installer plugins, and allow URL and headers modification + $headers = []; + $dispatcher = Factory::getApplication()->getDispatcher(); + PluginHelper::importPlugin('installer', null, true, $dispatcher); + $event = new BeforePackageDownloadEvent('onInstallerBeforePackageDownload', [ + 'url' => &$url, // @todo: Remove reference in Joomla 6, see BeforePackageDownloadEvent::__constructor() + 'headers' => &$headers, // @todo: Remove reference in Joomla 6, see BeforePackageDownloadEvent::__constructor() + ]); + $dispatcher->dispatch('onInstallerBeforePackageDownload', $event); + $url = $event->getArgument('url', $url); + $headers = $event->getArgument('headers', $headers); if (empty($url)) { @@ -92,12 +92,12 @@ public static function downloadPackage($url, $target = false) return false; } - try { - $response = HttpFactory::getHttp()->get($url, $headers); + try { + $response = HttpFactory::getHttp()->get($url, $headers); // Convert keys of headers to lowercase, to accommodate for case variations $headers = array_change_key_case($response->headers, CASE_LOWER); - } catch (\RuntimeException $exception) { + } catch (\RuntimeException $exception) { $response = $exception; } @@ -111,7 +111,7 @@ public static function downloadPackage($url, $target = false) 'headers' => &$headers, 'return' => true, ]); - $dispatcher->dispatch('onInstallerAfterPackageDownload', $event); + $dispatcher->dispatch('onInstallerAfterPackageDownload', $event); $return = $event->getArgument('return'); if ($return === false) @@ -126,280 +126,283 @@ public static function downloadPackage($url, $target = false) if ($response instanceof \Exception) { - Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $exception->getMessage()), Log::WARNING, 'jerror'); - - return false; - } - - if (302 == $response->code && !empty($headers['location'])) { - return self::downloadPackage($headers['location']); - } - - if (200 != $response->getStatusCode()) { - Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->getStatusCode()), Log::WARNING, 'jerror'); - - return false; - } - - // Parse the Content-Disposition header to get the file name - if ( - !empty($headers['content-disposition']) - && preg_match("/\s*filename\s?=\s?(.*)/", $headers['content-disposition'][0], $parts) - ) { - $flds = explode(';', $parts[1]); - $target = trim($flds[0], '"'); - } - - $tmpPath = Factory::getApplication()->get('tmp_path'); - - // Set the target path if not given - if (!$target) { - $target = $tmpPath . '/' . self::getFilenameFromUrl($url); - } else { - $target = $tmpPath . '/' . basename($target); - } - - // Fix Indirect Modification of Overloaded Property - $body = (string) $response->getBody(); - - // Write buffer to file - File::write($target, $body); - - // Bump the max execution time because not using built in php zip libs are slow - if (\function_exists('set_time_limit')) { - set_time_limit(\ini_get('max_execution_time')); - } - - // Return the name of the downloaded package - return basename($target); - } - - /** - * Unpacks a file and verifies it as a Joomla element package - * Supports .gz .tar .tar.gz and .zip - * - * @param string $packageFilename The uploaded package filename or install directory - * @param boolean $alwaysReturnArray If should return false (and leave garbage behind) or return $retval['type']=false - * - * @return array|boolean Array on success or boolean false on failure - * - * @since 3.1 - */ - public static function unpack($packageFilename, $alwaysReturnArray = false) - { - // Path to the archive - $archivename = $packageFilename; - - // Temporary folder to extract the archive into - $tmpdir = uniqid('install_'); - - // Clean the paths to use for archive extraction - $extractdir = Path::clean(\dirname($packageFilename) . '/' . $tmpdir); - $archivename = Path::clean($archivename); - - // Do the unpacking of the archive - try { - $archive = new Archive(['tmp_path' => Factory::getApplication()->get('tmp_path')]); - $extract = $archive->extract($archivename, $extractdir); - } catch (\Exception) { - if ($alwaysReturnArray) { - return [ - 'extractdir' => null, - 'packagefile' => $archivename, - 'type' => false, - ]; - } - - return false; - } - - if (!$extract) { - if ($alwaysReturnArray) { - return [ - 'extractdir' => null, - 'packagefile' => $archivename, - 'type' => false, - ]; - } - - return false; - } - - /* - * Let's set the extraction directory and package file in the result array so we can - * cleanup everything properly later on. - */ - $retval = []; - $retval['extractdir'] = $extractdir; - $retval['packagefile'] = $archivename; - - /* - * Try to find the correct install directory. In case the package is inside a - * subdirectory detect this and set the install directory to the correct path. - * - * List all the items in the installation directory. If there is only one, and - * it is a folder, then we will set that folder to be the installation folder. - */ - $dirList = array_merge((array) Folder::files($extractdir, ''), (array) Folder::folders($extractdir, '')); - - if (\count($dirList) === 1) { - if (is_dir(Path::clean($extractdir . '/' . $dirList[0]))) { - $extractdir = Path::clean($extractdir . '/' . $dirList[0]); - } - } - - /* - * We have found the install directory so lets set it and then move on - * to detecting the extension type. - */ - $retval['dir'] = $extractdir; - - /* - * Get the extension type and return the directory/type array on success or - * false on fail. - */ - $retval['type'] = self::detectType($extractdir); - - if ($alwaysReturnArray || $retval['type']) { - return $retval; - } - - return false; - } - - /** - * Method to detect the extension type from a package directory - * - * @param string $packageDirectory Path to package directory - * - * @return mixed Extension type string or boolean false on fail - * - * @since 3.1 - */ - public static function detectType($packageDirectory) - { - // Search the install dir for an XML file - $files = Folder::files($packageDirectory, '\.xml$', 1, true); - - if (!$files || !\count($files)) { - Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDXMLSETUPFILE'), Log::WARNING, 'jerror'); - - return false; - } - - foreach ($files as $file) { - $xml = simplexml_load_file($file); - - if (!$xml) { - continue; - } - - if ($xml->getName() !== 'extension') { - unset($xml); - continue; - } - - $type = (string) $xml->attributes()->type; - - // Free up memory - unset($xml); - - return $type; - } - - Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE'), Log::WARNING, 'jerror'); - - return false; - } - - /** - * Gets a file name out of a url - * - * @param string $url URL to get name from - * - * @return string Clean version of the filename or a unique id - * - * @since 3.1 - */ - public static function getFilenameFromUrl($url) - { - $default = uniqid(); - - if (!\is_string($url) || !str_contains($url, '/')) { - return $default; - } - - // Get last part of the url (after the last slash). - $parts = explode('/', $url); - $filename = array_pop($parts); - - // Replace special characters with underscores. - $filename = preg_replace('/[^a-z0-9\_\-\.]/i', '_', $filename); - - // Replace multiple underscores with just one. - $filename = preg_replace('/__+/', '_', trim($filename, '_')); - - // Return the cleaned filename or, if it is empty, a unique id. - return $filename ?: $default; - } - - /** - * Clean up temporary uploaded package and unpacked extension - * - * @param string $package Path to the uploaded package file - * @param string $resultdir Path to the unpacked extension - * - * @return void - * - * @since 3.1 - */ - public static function cleanupInstall($package, $resultdir) - { - // Does the unpacked extension directory exist? - if ($resultdir && is_dir($resultdir)) { - Folder::delete($resultdir); - } - - // Is the package file a valid file? - if (is_file($package)) { - File::delete($package); - } elseif (is_file(Path::clean(Factory::getApplication()->get('tmp_path') . '/' . $package))) { - // It might also be just a base filename - File::delete(Path::clean(Factory::getApplication()->get('tmp_path') . '/' . $package)); - } - } - - /** - * 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 integer one if the hashes match, zero if hashes doesn't match, two if hashes not found - * - * @since 3.9.0 - */ - public static function isChecksumValid($packagefile, $updateObject) - { - $hashes = ['sha256', 'sha384', 'sha512']; - $hashOnFile = false; - - foreach ($hashes as $hash) { - if ($updateObject->get($hash, false)) { - $hashPackage = hash_file($hash, $packagefile); - $hashRemote = $updateObject->$hash->_data; - $hashOnFile = true; - - if ($hashPackage !== strtolower($hashRemote)) { - return self::HASH_NOT_VALIDATED; - } - } - } - - if ($hashOnFile) { - return self::HASH_VALIDATED; - } - - return self::HASH_NOT_PROVIDED; - } + Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $exception->getMessage()), Log::WARNING, 'jerror'); + + return false; + } + + // Convert keys of headers to lowercase, to accommodate for case variations + $headers = array_change_key_case($response->headers, CASE_LOWER); + + if (302 == $response->code && !empty($headers['location'])) { + return self::downloadPackage($headers['location']); + } + + if (200 != $response->code) { + Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->code), Log::WARNING, 'jerror'); + + return false; + } + + // Parse the Content-Disposition header to get the file name + if ( + !empty($headers['content-disposition']) + && preg_match("/\s*filename\s?=\s?(.*)/", $headers['content-disposition'][0], $parts) + ) { + $flds = explode(';', $parts[1]); + $target = trim($flds[0], '"'); + } + + $tmpPath = Factory::getApplication()->get('tmp_path'); + + // Set the target path if not given + if (!$target) { + $target = $tmpPath . '/' . self::getFilenameFromUrl($url); + } else { + $target = $tmpPath . '/' . basename($target); + } + + // Fix Indirect Modification of Overloaded Property + $body = $response->body; + + // Write buffer to file + File::write($target, $body); + + // Bump the max execution time because not using built in php zip libs are slow + if (\function_exists('set_time_limit')) { + set_time_limit(\ini_get('max_execution_time')); + } + + // Return the name of the downloaded package + return basename($target); + } + + /** + * Unpacks a file and verifies it as a Joomla element package + * Supports .gz .tar .tar.gz and .zip + * + * @param string $packageFilename The uploaded package filename or install directory + * @param boolean $alwaysReturnArray If should return false (and leave garbage behind) or return $retval['type']=false + * + * @return array|boolean Array on success or boolean false on failure + * + * @since 3.1 + */ + public static function unpack($packageFilename, $alwaysReturnArray = false) + { + // Path to the archive + $archivename = $packageFilename; + + // Temporary folder to extract the archive into + $tmpdir = uniqid('install_'); + + // Clean the paths to use for archive extraction + $extractdir = Path::clean(\dirname($packageFilename) . '/' . $tmpdir); + $archivename = Path::clean($archivename); + + // Do the unpacking of the archive + try { + $archive = new Archive(['tmp_path' => Factory::getApplication()->get('tmp_path')]); + $extract = $archive->extract($archivename, $extractdir); + } catch (\Exception) { + if ($alwaysReturnArray) { + return [ + 'extractdir' => null, + 'packagefile' => $archivename, + 'type' => false, + ]; + } + + return false; + } + + if (!$extract) { + if ($alwaysReturnArray) { + return [ + 'extractdir' => null, + 'packagefile' => $archivename, + 'type' => false, + ]; + } + + return false; + } + + /* + * Let's set the extraction directory and package file in the result array so we can + * cleanup everything properly later on. + */ + $retval = []; + $retval['extractdir'] = $extractdir; + $retval['packagefile'] = $archivename; + + /* + * Try to find the correct install directory. In case the package is inside a + * subdirectory detect this and set the install directory to the correct path. + * + * List all the items in the installation directory. If there is only one, and + * it is a folder, then we will set that folder to be the installation folder. + */ + $dirList = array_merge((array) Folder::files($extractdir, ''), (array) Folder::folders($extractdir, '')); + + if (\count($dirList) === 1) { + if (is_dir(Path::clean($extractdir . '/' . $dirList[0]))) { + $extractdir = Path::clean($extractdir . '/' . $dirList[0]); + } + } + + /* + * We have found the install directory so lets set it and then move on + * to detecting the extension type. + */ + $retval['dir'] = $extractdir; + + /* + * Get the extension type and return the directory/type array on success or + * false on fail. + */ + $retval['type'] = self::detectType($extractdir); + + if ($alwaysReturnArray || $retval['type']) { + return $retval; + } + + return false; + } + + /** + * Method to detect the extension type from a package directory + * + * @param string $packageDirectory Path to package directory + * + * @return mixed Extension type string or boolean false on fail + * + * @since 3.1 + */ + public static function detectType($packageDirectory) + { + // Search the install dir for an XML file + $files = Folder::files($packageDirectory, '\.xml$', 1, true); + + if (!$files || !\count($files)) { + Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDXMLSETUPFILE'), Log::WARNING, 'jerror'); + + return false; + } + + foreach ($files as $file) { + $xml = simplexml_load_file($file); + + if (!$xml) { + continue; + } + + if ($xml->getName() !== 'extension') { + unset($xml); + continue; + } + + $type = (string) $xml->attributes()->type; + + // Free up memory + unset($xml); + + return $type; + } + + Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE'), Log::WARNING, 'jerror'); + + return false; + } + + /** + * Gets a file name out of a url + * + * @param string $url URL to get name from + * + * @return string Clean version of the filename or a unique id + * + * @since 3.1 + */ + public static function getFilenameFromUrl($url) + { + $default = uniqid(); + + if (!\is_string($url) || !str_contains($url, '/')) { + return $default; + } + + // Get last part of the url (after the last slash). + $parts = explode('/', $url); + $filename = array_pop($parts); + + // Replace special characters with underscores. + $filename = preg_replace('/[^a-z0-9\_\-\.]/i', '_', $filename); + + // Replace multiple underscores with just one. + $filename = preg_replace('/__+/', '_', trim($filename, '_')); + + // Return the cleaned filename or, if it is empty, a unique id. + return $filename ?: $default; + } + + /** + * Clean up temporary uploaded package and unpacked extension + * + * @param string $package Path to the uploaded package file + * @param string $resultdir Path to the unpacked extension + * + * @return void + * + * @since 3.1 + */ + public static function cleanupInstall($package, $resultdir) + { + // Does the unpacked extension directory exist? + if ($resultdir && is_dir($resultdir)) { + Folder::delete($resultdir); + } + + // Is the package file a valid file? + if (is_file($package)) { + File::delete($package); + } elseif (is_file(Path::clean(Factory::getApplication()->get('tmp_path') . '/' . $package))) { + // It might also be just a base filename + File::delete(Path::clean(Factory::getApplication()->get('tmp_path') . '/' . $package)); + } + } + + /** + * 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 integer one if the hashes match, zero if hashes doesn't match, two if hashes not found + * + * @since 3.9.0 + */ + public static function isChecksumValid($packagefile, $updateObject) + { + $hashes = ['sha256', 'sha384', 'sha512']; + $hashOnFile = false; + + foreach ($hashes as $hash) { + if ($updateObject->get($hash, false)) { + $hashPackage = hash_file($hash, $packagefile); + $hashRemote = $updateObject->$hash->_data; + $hashOnFile = true; + + if ($hashPackage !== strtolower($hashRemote)) { + return self::HASH_NOT_VALIDATED; + } + } + } + + if ($hashOnFile) { + return self::HASH_VALIDATED; + } + + return self::HASH_NOT_PROVIDED; + } } From 4b99f74a4721a60c840cae45f0126298135117cf Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:11:59 +0100 Subject: [PATCH 05/54] Update InstallerHelper.php --- libraries/src/Installer/InstallerHelper.php | 668 ++++++++++---------- 1 file changed, 334 insertions(+), 334 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 07b51640f6392..4f444d4f479e6 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -34,57 +34,57 @@ */ abstract class InstallerHelper { - /** - * Hash not validated identifier. - * - * @var integer - * @since 3.9.0 - */ - public const HASH_NOT_VALIDATED = 0; - - /** - * Hash validated identifier. - * - * @var integer - * @since 3.9.0 - */ - public const HASH_VALIDATED = 1; - - /** - * Hash not provided identifier. - * - * @var integer - * @since 3.9.0 - */ - public const HASH_NOT_PROVIDED = 2; - - /** - * Downloads a package - * - * @param string $url URL of file to download - * @param string|bool $target Download target filename or false to get the filename from the URL - * - * @return string|boolean Path to downloaded package or boolean false on failure - * - * @since 3.1 - */ - public static function downloadPackage($url, $target = false) - { - // Set user agent - $version = new Version(); - ini_set('user_agent', $version->getUserAgent('Installer')); - - // Load installer plugins, and allow URL and headers modification - $headers = []; - $dispatcher = Factory::getApplication()->getDispatcher(); - PluginHelper::importPlugin('installer', null, true, $dispatcher); - $event = new BeforePackageDownloadEvent('onInstallerBeforePackageDownload', [ - 'url' => &$url, // @todo: Remove reference in Joomla 6, see BeforePackageDownloadEvent::__constructor() - 'headers' => &$headers, // @todo: Remove reference in Joomla 6, see BeforePackageDownloadEvent::__constructor() - ]); - $dispatcher->dispatch('onInstallerBeforePackageDownload', $event); - $url = $event->getArgument('url', $url); - $headers = $event->getArgument('headers', $headers); + /** + * Hash not validated identifier. + * + * @var integer + * @since 3.9.0 + */ + public const HASH_NOT_VALIDATED = 0; + + /** + * Hash validated identifier. + * + * @var integer + * @since 3.9.0 + */ + public const HASH_VALIDATED = 1; + + /** + * Hash not provided identifier. + * + * @var integer + * @since 3.9.0 + */ + public const HASH_NOT_PROVIDED = 2; + + /** + * Downloads a package + * + * @param string $url URL of file to download + * @param string|bool $target Download target filename or false to get the filename from the URL + * + * @return string|boolean Path to downloaded package or boolean false on failure + * + * @since 3.1 + */ + public static function downloadPackage($url, $target = false) + { + // Set user agent + $version = new Version(); + ini_set('user_agent', $version->getUserAgent('Installer')); + + // Load installer plugins, and allow URL and headers modification + $headers = []; + $dispatcher = Factory::getApplication()->getDispatcher(); + PluginHelper::importPlugin('installer', null, true, $dispatcher); + $event = new BeforePackageDownloadEvent('onInstallerBeforePackageDownload', [ + 'url' => &$url, // @todo: Remove reference in Joomla 6, see BeforePackageDownloadEvent::__constructor() + 'headers' => &$headers, // @todo: Remove reference in Joomla 6, see BeforePackageDownloadEvent::__constructor() + ]); + $dispatcher->dispatch('onInstallerBeforePackageDownload', $event); + $url = $event->getArgument('url', $url); + $headers = $event->getArgument('headers', $headers); if (empty($url)) { @@ -92,12 +92,12 @@ public static function downloadPackage($url, $target = false) return false; } - try { - $response = HttpFactory::getHttp()->get($url, $headers); + try { + $response = HttpFactory::getHttp()->get($url, $headers); // Convert keys of headers to lowercase, to accommodate for case variations $headers = array_change_key_case($response->headers, CASE_LOWER); - } catch (\RuntimeException $exception) { + } catch (\RuntimeException $exception) { $response = $exception; } @@ -111,7 +111,7 @@ public static function downloadPackage($url, $target = false) 'headers' => &$headers, 'return' => true, ]); - $dispatcher->dispatch('onInstallerAfterPackageDownload', $event); + $dispatcher->dispatch('onInstallerAfterPackageDownload', $event); $return = $event->getArgument('return'); if ($return === false) @@ -126,283 +126,283 @@ public static function downloadPackage($url, $target = false) if ($response instanceof \Exception) { - Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $exception->getMessage()), Log::WARNING, 'jerror'); - - return false; - } - - // Convert keys of headers to lowercase, to accommodate for case variations - $headers = array_change_key_case($response->headers, CASE_LOWER); - - if (302 == $response->code && !empty($headers['location'])) { - return self::downloadPackage($headers['location']); - } - - if (200 != $response->code) { - Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->code), Log::WARNING, 'jerror'); - - return false; - } - - // Parse the Content-Disposition header to get the file name - if ( - !empty($headers['content-disposition']) - && preg_match("/\s*filename\s?=\s?(.*)/", $headers['content-disposition'][0], $parts) - ) { - $flds = explode(';', $parts[1]); - $target = trim($flds[0], '"'); - } - - $tmpPath = Factory::getApplication()->get('tmp_path'); - - // Set the target path if not given - if (!$target) { - $target = $tmpPath . '/' . self::getFilenameFromUrl($url); - } else { - $target = $tmpPath . '/' . basename($target); - } - - // Fix Indirect Modification of Overloaded Property - $body = $response->body; - - // Write buffer to file - File::write($target, $body); - - // Bump the max execution time because not using built in php zip libs are slow - if (\function_exists('set_time_limit')) { - set_time_limit(\ini_get('max_execution_time')); - } - - // Return the name of the downloaded package - return basename($target); - } - - /** - * Unpacks a file and verifies it as a Joomla element package - * Supports .gz .tar .tar.gz and .zip - * - * @param string $packageFilename The uploaded package filename or install directory - * @param boolean $alwaysReturnArray If should return false (and leave garbage behind) or return $retval['type']=false - * - * @return array|boolean Array on success or boolean false on failure - * - * @since 3.1 - */ - public static function unpack($packageFilename, $alwaysReturnArray = false) - { - // Path to the archive - $archivename = $packageFilename; - - // Temporary folder to extract the archive into - $tmpdir = uniqid('install_'); - - // Clean the paths to use for archive extraction - $extractdir = Path::clean(\dirname($packageFilename) . '/' . $tmpdir); - $archivename = Path::clean($archivename); - - // Do the unpacking of the archive - try { - $archive = new Archive(['tmp_path' => Factory::getApplication()->get('tmp_path')]); - $extract = $archive->extract($archivename, $extractdir); - } catch (\Exception) { - if ($alwaysReturnArray) { - return [ - 'extractdir' => null, - 'packagefile' => $archivename, - 'type' => false, - ]; - } - - return false; - } - - if (!$extract) { - if ($alwaysReturnArray) { - return [ - 'extractdir' => null, - 'packagefile' => $archivename, - 'type' => false, - ]; - } - - return false; - } - - /* - * Let's set the extraction directory and package file in the result array so we can - * cleanup everything properly later on. - */ - $retval = []; - $retval['extractdir'] = $extractdir; - $retval['packagefile'] = $archivename; - - /* - * Try to find the correct install directory. In case the package is inside a - * subdirectory detect this and set the install directory to the correct path. - * - * List all the items in the installation directory. If there is only one, and - * it is a folder, then we will set that folder to be the installation folder. - */ - $dirList = array_merge((array) Folder::files($extractdir, ''), (array) Folder::folders($extractdir, '')); - - if (\count($dirList) === 1) { - if (is_dir(Path::clean($extractdir . '/' . $dirList[0]))) { - $extractdir = Path::clean($extractdir . '/' . $dirList[0]); - } - } - - /* - * We have found the install directory so lets set it and then move on - * to detecting the extension type. - */ - $retval['dir'] = $extractdir; - - /* - * Get the extension type and return the directory/type array on success or - * false on fail. - */ - $retval['type'] = self::detectType($extractdir); - - if ($alwaysReturnArray || $retval['type']) { - return $retval; - } - - return false; - } - - /** - * Method to detect the extension type from a package directory - * - * @param string $packageDirectory Path to package directory - * - * @return mixed Extension type string or boolean false on fail - * - * @since 3.1 - */ - public static function detectType($packageDirectory) - { - // Search the install dir for an XML file - $files = Folder::files($packageDirectory, '\.xml$', 1, true); - - if (!$files || !\count($files)) { - Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDXMLSETUPFILE'), Log::WARNING, 'jerror'); - - return false; - } - - foreach ($files as $file) { - $xml = simplexml_load_file($file); - - if (!$xml) { - continue; - } - - if ($xml->getName() !== 'extension') { - unset($xml); - continue; - } - - $type = (string) $xml->attributes()->type; - - // Free up memory - unset($xml); - - return $type; - } - - Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE'), Log::WARNING, 'jerror'); - - return false; - } - - /** - * Gets a file name out of a url - * - * @param string $url URL to get name from - * - * @return string Clean version of the filename or a unique id - * - * @since 3.1 - */ - public static function getFilenameFromUrl($url) - { - $default = uniqid(); - - if (!\is_string($url) || !str_contains($url, '/')) { - return $default; - } - - // Get last part of the url (after the last slash). - $parts = explode('/', $url); - $filename = array_pop($parts); - - // Replace special characters with underscores. - $filename = preg_replace('/[^a-z0-9\_\-\.]/i', '_', $filename); - - // Replace multiple underscores with just one. - $filename = preg_replace('/__+/', '_', trim($filename, '_')); - - // Return the cleaned filename or, if it is empty, a unique id. - return $filename ?: $default; - } - - /** - * Clean up temporary uploaded package and unpacked extension - * - * @param string $package Path to the uploaded package file - * @param string $resultdir Path to the unpacked extension - * - * @return void - * - * @since 3.1 - */ - public static function cleanupInstall($package, $resultdir) - { - // Does the unpacked extension directory exist? - if ($resultdir && is_dir($resultdir)) { - Folder::delete($resultdir); - } - - // Is the package file a valid file? - if (is_file($package)) { - File::delete($package); - } elseif (is_file(Path::clean(Factory::getApplication()->get('tmp_path') . '/' . $package))) { - // It might also be just a base filename - File::delete(Path::clean(Factory::getApplication()->get('tmp_path') . '/' . $package)); - } - } - - /** - * 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 integer one if the hashes match, zero if hashes doesn't match, two if hashes not found - * - * @since 3.9.0 - */ - public static function isChecksumValid($packagefile, $updateObject) - { - $hashes = ['sha256', 'sha384', 'sha512']; - $hashOnFile = false; - - foreach ($hashes as $hash) { - if ($updateObject->get($hash, false)) { - $hashPackage = hash_file($hash, $packagefile); - $hashRemote = $updateObject->$hash->_data; - $hashOnFile = true; - - if ($hashPackage !== strtolower($hashRemote)) { - return self::HASH_NOT_VALIDATED; - } - } - } - - if ($hashOnFile) { - return self::HASH_VALIDATED; - } - - return self::HASH_NOT_PROVIDED; - } + Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $exception->getMessage()), Log::WARNING, 'jerror'); + + return false; + } + + // Convert keys of headers to lowercase, to accommodate for case variations + $headers = array_change_key_case($response->headers, CASE_LOWER); + + if (302 == $response->code && !empty($headers['location'])) { + return self::downloadPackage($headers['location']); + } + + if (200 != $response->code) { + Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->code), Log::WARNING, 'jerror'); + + return false; + } + + // Parse the Content-Disposition header to get the file name + if ( + !empty($headers['content-disposition']) + && preg_match("/\s*filename\s?=\s?(.*)/", $headers['content-disposition'][0], $parts) + ) { + $flds = explode(';', $parts[1]); + $target = trim($flds[0], '"'); + } + + $tmpPath = Factory::getApplication()->get('tmp_path'); + + // Set the target path if not given + if (!$target) { + $target = $tmpPath . '/' . self::getFilenameFromUrl($url); + } else { + $target = $tmpPath . '/' . basename($target); + } + + // Fix Indirect Modification of Overloaded Property + $body = $response->body; + + // Write buffer to file + File::write($target, $body); + + // Bump the max execution time because not using built in php zip libs are slow + if (\function_exists('set_time_limit')) { + set_time_limit(\ini_get('max_execution_time')); + } + + // Return the name of the downloaded package + return basename($target); + } + + /** + * Unpacks a file and verifies it as a Joomla element package + * Supports .gz .tar .tar.gz and .zip + * + * @param string $packageFilename The uploaded package filename or install directory + * @param boolean $alwaysReturnArray If should return false (and leave garbage behind) or return $retval['type']=false + * + * @return array|boolean Array on success or boolean false on failure + * + * @since 3.1 + */ + public static function unpack($packageFilename, $alwaysReturnArray = false) + { + // Path to the archive + $archivename = $packageFilename; + + // Temporary folder to extract the archive into + $tmpdir = uniqid('install_'); + + // Clean the paths to use for archive extraction + $extractdir = Path::clean(\dirname($packageFilename) . '/' . $tmpdir); + $archivename = Path::clean($archivename); + + // Do the unpacking of the archive + try { + $archive = new Archive(['tmp_path' => Factory::getApplication()->get('tmp_path')]); + $extract = $archive->extract($archivename, $extractdir); + } catch (\Exception) { + if ($alwaysReturnArray) { + return [ + 'extractdir' => null, + 'packagefile' => $archivename, + 'type' => false, + ]; + } + + return false; + } + + if (!$extract) { + if ($alwaysReturnArray) { + return [ + 'extractdir' => null, + 'packagefile' => $archivename, + 'type' => false, + ]; + } + + return false; + } + + /* + * Let's set the extraction directory and package file in the result array so we can + * cleanup everything properly later on. + */ + $retval = []; + $retval['extractdir'] = $extractdir; + $retval['packagefile'] = $archivename; + + /* + * Try to find the correct install directory. In case the package is inside a + * subdirectory detect this and set the install directory to the correct path. + * + * List all the items in the installation directory. If there is only one, and + * it is a folder, then we will set that folder to be the installation folder. + */ + $dirList = array_merge((array) Folder::files($extractdir, ''), (array) Folder::folders($extractdir, '')); + + if (\count($dirList) === 1) { + if (is_dir(Path::clean($extractdir . '/' . $dirList[0]))) { + $extractdir = Path::clean($extractdir . '/' . $dirList[0]); + } + } + + /* + * We have found the install directory so lets set it and then move on + * to detecting the extension type. + */ + $retval['dir'] = $extractdir; + + /* + * Get the extension type and return the directory/type array on success or + * false on fail. + */ + $retval['type'] = self::detectType($extractdir); + + if ($alwaysReturnArray || $retval['type']) { + return $retval; + } + + return false; + } + + /** + * Method to detect the extension type from a package directory + * + * @param string $packageDirectory Path to package directory + * + * @return mixed Extension type string or boolean false on fail + * + * @since 3.1 + */ + public static function detectType($packageDirectory) + { + // Search the install dir for an XML file + $files = Folder::files($packageDirectory, '\.xml$', 1, true); + + if (!$files || !\count($files)) { + Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDXMLSETUPFILE'), Log::WARNING, 'jerror'); + + return false; + } + + foreach ($files as $file) { + $xml = simplexml_load_file($file); + + if (!$xml) { + continue; + } + + if ($xml->getName() !== 'extension') { + unset($xml); + continue; + } + + $type = (string) $xml->attributes()->type; + + // Free up memory + unset($xml); + + return $type; + } + + Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE'), Log::WARNING, 'jerror'); + + return false; + } + + /** + * Gets a file name out of a url + * + * @param string $url URL to get name from + * + * @return string Clean version of the filename or a unique id + * + * @since 3.1 + */ + public static function getFilenameFromUrl($url) + { + $default = uniqid(); + + if (!\is_string($url) || !str_contains($url, '/')) { + return $default; + } + + // Get last part of the url (after the last slash). + $parts = explode('/', $url); + $filename = array_pop($parts); + + // Replace special characters with underscores. + $filename = preg_replace('/[^a-z0-9\_\-\.]/i', '_', $filename); + + // Replace multiple underscores with just one. + $filename = preg_replace('/__+/', '_', trim($filename, '_')); + + // Return the cleaned filename or, if it is empty, a unique id. + return $filename ?: $default; + } + + /** + * Clean up temporary uploaded package and unpacked extension + * + * @param string $package Path to the uploaded package file + * @param string $resultdir Path to the unpacked extension + * + * @return void + * + * @since 3.1 + */ + public static function cleanupInstall($package, $resultdir) + { + // Does the unpacked extension directory exist? + if ($resultdir && is_dir($resultdir)) { + Folder::delete($resultdir); + } + + // Is the package file a valid file? + if (is_file($package)) { + File::delete($package); + } elseif (is_file(Path::clean(Factory::getApplication()->get('tmp_path') . '/' . $package))) { + // It might also be just a base filename + File::delete(Path::clean(Factory::getApplication()->get('tmp_path') . '/' . $package)); + } + } + + /** + * 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 integer one if the hashes match, zero if hashes doesn't match, two if hashes not found + * + * @since 3.9.0 + */ + public static function isChecksumValid($packagefile, $updateObject) + { + $hashes = ['sha256', 'sha384', 'sha512']; + $hashOnFile = false; + + foreach ($hashes as $hash) { + if ($updateObject->get($hash, false)) { + $hashPackage = hash_file($hash, $packagefile); + $hashRemote = $updateObject->$hash->_data; + $hashOnFile = true; + + if ($hashPackage !== strtolower($hashRemote)) { + return self::HASH_NOT_VALIDATED; + } + } + } + + if ($hashOnFile) { + return self::HASH_VALIDATED; + } + + return self::HASH_NOT_PROVIDED; + } } From e077f9ae4c90b69b396532539fbe079b88e1b1d9 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:17:18 +0100 Subject: [PATCH 06/54] Update UpdateModel.php --- .../com_installer/src/Model/UpdateModel.php | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index df23cd1fcc6d2..eaf34f4a5b205 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -10,6 +10,7 @@ namespace Joomla\Component\Installer\Administrator\Model; +use Joomla\CMS\Event\Installer\AfterPackageDownloadFailedEvent; use Joomla\CMS\Extension\ExtensionHelper; use Joomla\CMS\Factory; use Joomla\CMS\Installer\Installer; @@ -440,7 +441,24 @@ private function install($update) // Was the package downloaded? if (!$p_file) { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); + $dispatcher = $this->getDispatcher(); + PluginHelper::importPlugin('installer', null, true, $dispatcher); + $event = new AfterPackageDownloadFailedEvent('onInstallerPackageDownloadFailed', [ + 'url' => $url, + 'message' => true, + 'type' => 'error', + ]); + $dispatcher->dispatch('onInstallerPackageDownloadFailed', $event); + $message = $event->getArgument('message', true); + $type = $event->getArgument('type', 'error'); + + if ($message === true) + { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); + } elseif ($message !== false) + { + Factory::getApplication()->enqueueMessage($message, $type); + } return false; } From 75684179b0d52405e98637eb49a34652cac339e2 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 00:12:51 +0100 Subject: [PATCH 07/54] Update InstallerHelper.php --- libraries/src/Installer/InstallerHelper.php | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 4f444d4f479e6..bebda5a8d5095 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -102,7 +102,6 @@ public static function downloadPackage($url, $target = false) } // Load installer plugins, and check response - $headers = []; $dispatcher = Factory::getApplication()->getDispatcher(); PluginHelper::importPlugin('installer', null, true, $dispatcher); $event = new AfterPackageDownloadEvent('onInstallerAfterPackageDownload', [ From c66e160fde3fc60afefc760a0e686d89aaa6ecd2 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 07:39:28 +0100 Subject: [PATCH 08/54] Update ExtensionAdapter.php Avoid PHP warning on missing target platform information. For instance: https://raw.githubusercontent.com/trananhmanh89/ff-update-server/master/com_ffexplorer.xml --- libraries/src/Updater/Adapter/ExtensionAdapter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/src/Updater/Adapter/ExtensionAdapter.php b/libraries/src/Updater/Adapter/ExtensionAdapter.php index 78f5db5808f09..38174dcfc3c13 100644 --- a/libraries/src/Updater/Adapter/ExtensionAdapter.php +++ b/libraries/src/Updater/Adapter/ExtensionAdapter.php @@ -109,7 +109,8 @@ protected function _endElement($parser, $name) // Check that the product matches and that the version matches (optionally a regexp) if ( - $product == $this->currentUpdate->targetplatform['NAME'] + !empty($this->currentUpdate->targetplatform) + && $product == $this->currentUpdate->targetplatform['NAME'] && preg_match('/^' . $this->currentUpdate->targetplatform['VERSION'] . '/', JVERSION) ) { // Check if PHP version supported via tag, assume true if tag isn't present From d7e5e1e6dfb30adf84cece3965683b45113946ef Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:40:49 +0100 Subject: [PATCH 09/54] Update InstallerHelper.php --- libraries/src/Installer/InstallerHelper.php | 98 ++++++++++++++++----- 1 file changed, 77 insertions(+), 21 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index bebda5a8d5095..0e648b206d8cb 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -58,6 +58,14 @@ abstract class InstallerHelper */ public const HASH_NOT_PROVIDED = 2; + /** + * Message displayed when download failes. + * + * @var string + * @since 6.1.0 + */ + protected static $downloadFailMessage = 'COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED'; + /** * Downloads a package * @@ -74,8 +82,10 @@ public static function downloadPackage($url, $target = false) $version = new Version(); ini_set('user_agent', $version->getUserAgent('Installer')); - // Load installer plugins, and allow URL and headers modification $headers = []; + $headers['accept'] = 'application/json, application/zip'; + + // Load installer plugins, and allow URL and headers modification $dispatcher = Factory::getApplication()->getDispatcher(); PluginHelper::importPlugin('installer', null, true, $dispatcher); $event = new BeforePackageDownloadEvent('onInstallerBeforePackageDownload', [ @@ -130,39 +140,68 @@ public static function downloadPackage($url, $target = false) return false; } - // Convert keys of headers to lowercase, to accommodate for case variations - $headers = array_change_key_case($response->headers, CASE_LOWER); - if (302 == $response->code && !empty($headers['location'])) { return self::downloadPackage($headers['location']); } if (200 != $response->code) { - Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->code), Log::WARNING, 'jerror'); + Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->getStatusCode()), Log::WARNING, 'jerror'); return false; } - // Parse the Content-Disposition header to get the file name - if ( - !empty($headers['content-disposition']) - && preg_match("/\s*filename\s?=\s?(.*)/", $headers['content-disposition'][0], $parts) - ) { - $flds = explode(';', $parts[1]); - $target = trim($flds[0], '"'); - } + if (!empty($headers['content-type']) + && !empty($headers['content-type'][0]) + && strpos($headers['content-type'][0], 'application/json') !== false) { + $response = json_decode($response->body, true); + + if (isset($response['downloadfailmessage'])) { + // Empty string will disable the download fail message. + // The response message may be used on errors. Allows the message type to be customised. + self::$downloadFailMessage = $response['downloadfailmessage']; + } + + if (!empty($response['message'])) { + Factory::getApplication()->enqueueMessage($response['message'], $response['type'] ?? 'info'); - $tmpPath = Factory::getApplication()->get('tmp_path'); + if (!empty($response['error'])) { + return false; + } + } - // Set the target path if not given - if (!$target) { - $target = $tmpPath . '/' . self::getFilenameFromUrl($url); - } else { - $target = $tmpPath . '/' . basename($target); + if (!empty($response['error']) + || empty($response['package']) + || empty($response['target'])) { + Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', ''), Log::WARNING, 'jerror'); + + return false; + } + + $body = $response['package']; + $target = $response['target']; } + else { + // Parse the Content-Disposition header to get the file name + if ( + !empty($headers['content-disposition']) + && preg_match("/\s*filename\s?=\s?(.*)/", $headers['content-disposition'][0], $parts) + ) { + $flds = explode(';', $parts[1]); + $target = trim($flds[0], '"'); + } + + $tmpPath = Factory::getApplication()->get('tmp_path'); + + // Set the target path if not given + if (!$target) { + $target = $tmpPath . '/' . self::getFilenameFromUrl($url); + } else { + $target = $tmpPath . '/' . basename($target); + } - // Fix Indirect Modification of Overloaded Property - $body = $response->body; + // Fix Indirect Modification of Overloaded Property + $body = $response->body; + } // Write buffer to file File::write($target, $body); @@ -176,6 +215,23 @@ public static function downloadPackage($url, $target = false) return basename($target); } + /* + * Enqueues the download fail message (if any). + * + * @param string $url URL of file to download + * + * @return void + * + * @since 6.1 + */ + public static function enqueueDownloadFailMessage($url) { + if (empty(self::$downloadFailMessage)) { + return; + } + + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); + } + /** * Unpacks a file and verifies it as a Joomla element package * Supports .gz .tar .tar.gz and .zip From 8bea8b1c784ac773a2caab48d0171c24b05b6c7d Mon Sep 17 00:00:00 2001 From: Jonathan Brain Date: Tue, 9 Sep 2025 12:53:35 +0100 Subject: [PATCH 10/54] Revert "Update InstallerHelper.php" This reverts commit d7e5e1e6dfb30adf84cece3965683b45113946ef. --- libraries/src/Installer/InstallerHelper.php | 98 +++++---------------- 1 file changed, 21 insertions(+), 77 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 0e648b206d8cb..bebda5a8d5095 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -58,14 +58,6 @@ abstract class InstallerHelper */ public const HASH_NOT_PROVIDED = 2; - /** - * Message displayed when download failes. - * - * @var string - * @since 6.1.0 - */ - protected static $downloadFailMessage = 'COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED'; - /** * Downloads a package * @@ -82,10 +74,8 @@ public static function downloadPackage($url, $target = false) $version = new Version(); ini_set('user_agent', $version->getUserAgent('Installer')); - $headers = []; - $headers['accept'] = 'application/json, application/zip'; - // Load installer plugins, and allow URL and headers modification + $headers = []; $dispatcher = Factory::getApplication()->getDispatcher(); PluginHelper::importPlugin('installer', null, true, $dispatcher); $event = new BeforePackageDownloadEvent('onInstallerBeforePackageDownload', [ @@ -140,69 +130,40 @@ public static function downloadPackage($url, $target = false) return false; } + // Convert keys of headers to lowercase, to accommodate for case variations + $headers = array_change_key_case($response->headers, CASE_LOWER); + if (302 == $response->code && !empty($headers['location'])) { return self::downloadPackage($headers['location']); } if (200 != $response->code) { - Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->getStatusCode()), Log::WARNING, 'jerror'); + Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->code), Log::WARNING, 'jerror'); return false; } - if (!empty($headers['content-type']) - && !empty($headers['content-type'][0]) - && strpos($headers['content-type'][0], 'application/json') !== false) { - $response = json_decode($response->body, true); - - if (isset($response['downloadfailmessage'])) { - // Empty string will disable the download fail message. - // The response message may be used on errors. Allows the message type to be customised. - self::$downloadFailMessage = $response['downloadfailmessage']; - } - - if (!empty($response['message'])) { - Factory::getApplication()->enqueueMessage($response['message'], $response['type'] ?? 'info'); - - if (!empty($response['error'])) { - return false; - } - } - - if (!empty($response['error']) - || empty($response['package']) - || empty($response['target'])) { - Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', ''), Log::WARNING, 'jerror'); - - return false; - } - - $body = $response['package']; - $target = $response['target']; + // Parse the Content-Disposition header to get the file name + if ( + !empty($headers['content-disposition']) + && preg_match("/\s*filename\s?=\s?(.*)/", $headers['content-disposition'][0], $parts) + ) { + $flds = explode(';', $parts[1]); + $target = trim($flds[0], '"'); } - else { - // Parse the Content-Disposition header to get the file name - if ( - !empty($headers['content-disposition']) - && preg_match("/\s*filename\s?=\s?(.*)/", $headers['content-disposition'][0], $parts) - ) { - $flds = explode(';', $parts[1]); - $target = trim($flds[0], '"'); - } - - $tmpPath = Factory::getApplication()->get('tmp_path'); - // Set the target path if not given - if (!$target) { - $target = $tmpPath . '/' . self::getFilenameFromUrl($url); - } else { - $target = $tmpPath . '/' . basename($target); - } + $tmpPath = Factory::getApplication()->get('tmp_path'); - // Fix Indirect Modification of Overloaded Property - $body = $response->body; + // Set the target path if not given + if (!$target) { + $target = $tmpPath . '/' . self::getFilenameFromUrl($url); + } else { + $target = $tmpPath . '/' . basename($target); } + // Fix Indirect Modification of Overloaded Property + $body = $response->body; + // Write buffer to file File::write($target, $body); @@ -215,23 +176,6 @@ public static function downloadPackage($url, $target = false) return basename($target); } - /* - * Enqueues the download fail message (if any). - * - * @param string $url URL of file to download - * - * @return void - * - * @since 6.1 - */ - public static function enqueueDownloadFailMessage($url) { - if (empty(self::$downloadFailMessage)) { - return; - } - - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); - } - /** * Unpacks a file and verifies it as a Joomla element package * Supports .gz .tar .tar.gz and .zip From 639c4fce2d17b71aad9f47d49dcee8b0010e12d7 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:01:19 +0100 Subject: [PATCH 11/54] Update InstallerHelper.php --- libraries/src/Installer/InstallerHelper.php | 93 +++++++++++++++++---- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index bebda5a8d5095..5db09d72cc5b7 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -58,6 +58,14 @@ abstract class InstallerHelper */ public const HASH_NOT_PROVIDED = 2; + /** + * Message displayed when download failes. + * + * @var string + * @since 6.1.0 + */ + protected static $downloadFailMessage = 'COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED'; + /** * Downloads a package * @@ -76,6 +84,9 @@ public static function downloadPackage($url, $target = false) // Load installer plugins, and allow URL and headers modification $headers = []; + $headers['accept'] = 'application/json, application/zip'; + + // Load installer plugins, and allow URL and headers modification $dispatcher = Factory::getApplication()->getDispatcher(); PluginHelper::importPlugin('installer', null, true, $dispatcher); $event = new BeforePackageDownloadEvent('onInstallerBeforePackageDownload', [ @@ -130,39 +141,68 @@ public static function downloadPackage($url, $target = false) return false; } - // Convert keys of headers to lowercase, to accommodate for case variations - $headers = array_change_key_case($response->headers, CASE_LOWER); - if (302 == $response->code && !empty($headers['location'])) { return self::downloadPackage($headers['location']); } if (200 != $response->code) { - Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->code), Log::WARNING, 'jerror'); + Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->getStatusCode()), Log::WARNING, 'jerror'); return false; } - // Parse the Content-Disposition header to get the file name - if ( - !empty($headers['content-disposition']) - && preg_match("/\s*filename\s?=\s?(.*)/", $headers['content-disposition'][0], $parts) - ) { - $flds = explode(';', $parts[1]); - $target = trim($flds[0], '"'); - } + if (!empty($headers['content-type']) + && !empty($headers['content-type'][0]) + && strpos($headers['content-type'][0], 'application/json') !== false) { + $response = json_decode($response->body, true); - $tmpPath = Factory::getApplication()->get('tmp_path'); + if (isset($response['downloadfailmessage'])) { + // Empty string will disable the download fail message. + // The response message may be used on errors. Allows the message type to be customised. + self::$downloadFailMessage = $response['downloadfailmessage']; + } + + if (!empty($response['message'])) { + Factory::getApplication()->enqueueMessage($response['message'], $response['type'] ?? 'info'); - // Set the target path if not given - if (!$target) { - $target = $tmpPath . '/' . self::getFilenameFromUrl($url); - } else { - $target = $tmpPath . '/' . basename($target); + if (!empty($response['error'])) { + return false; + } + } + + if (!empty($response['error']) + || empty($response['package']) + || empty($response['target'])) { + Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', ''), Log::WARNING, 'jerror'); + + return false; + } + + $body = $response['package']; + $target = $response['target']; + } + else { + // Parse the Content-Disposition header to get the file name + if ( + !empty($headers['content-disposition']) + && preg_match("/\s*filename\s?=\s?(.*)/", $headers['content-disposition'][0], $parts) + ) { + $flds = explode(';', $parts[1]); + $target = trim($flds[0], '"'); + } + + $tmpPath = Factory::getApplication()->get('tmp_path'); + + // Set the target path if not given + if (!$target) { + $target = $tmpPath . '/' . self::getFilenameFromUrl($url); + } else { + $target = $tmpPath . '/' . basename($target); } // Fix Indirect Modification of Overloaded Property $body = $response->body; + } // Write buffer to file File::write($target, $body); @@ -176,6 +216,23 @@ public static function downloadPackage($url, $target = false) return basename($target); } + /* + * Enqueues the download fail message (if any). + * + * @param string $url URL of file to download + * + * @return void + * + * @since 6.1 + */ + public static function enqueueDownloadFailMessage($url) { + if (empty(self::$downloadFailMessage)) { + return; + } + + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); + } + /** * Unpacks a file and verifies it as a Joomla element package * Supports .gz .tar .tar.gz and .zip From 90321af6e9c60ac53ef6225f088edc293df28be0 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:06:25 +0100 Subject: [PATCH 12/54] Update UpdateModel.php --- .../components/com_installer/src/Model/UpdateModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index eaf34f4a5b205..f8425ddb14e3c 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -454,7 +454,7 @@ private function install($update) if ($message === true) { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); + InstallerHelper::enqueueDownloadFailMessage($url); } elseif ($message !== false) { Factory::getApplication()->enqueueMessage($message, $type); From dea94b8371d437c2d5d91796ca3d615b10ec6cf2 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:39:37 +0100 Subject: [PATCH 13/54] Update InstallerHelper.php --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 5db09d72cc5b7..0513991ed4c96 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -178,7 +178,7 @@ public static function downloadPackage($url, $target = false) return false; } - $body = $response['package']; + $body = base64_decode($response['package']); $target = $response['target']; } else { From e95aaa0387c74709b02639aa7486295330f8136d Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:45:36 +0100 Subject: [PATCH 14/54] Update InstallerHelper.php --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 0513991ed4c96..29fc81a64f818 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -59,7 +59,7 @@ abstract class InstallerHelper public const HASH_NOT_PROVIDED = 2; /** - * Message displayed when download failes. + * Message displayed when download fails. * * @var string * @since 6.1.0 From c0d89885aac2fe06c2134d272d7b3bdce0bae04c Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:24:27 +0100 Subject: [PATCH 15/54] Update administrator/components/com_installer/src/Model/UpdateModel.php Co-authored-by: Richard Fath --- .../components/com_installer/src/Model/UpdateModel.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index f8425ddb14e3c..4218b3485d7cc 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -452,11 +452,9 @@ private function install($update) $message = $event->getArgument('message', true); $type = $event->getArgument('type', 'error'); - if ($message === true) - { + if ($message === true) { InstallerHelper::enqueueDownloadFailMessage($url); - } elseif ($message !== false) - { + } elseif ($message !== false) { Factory::getApplication()->enqueueMessage($message, $type); } From 2f038211e41463dddb0e8dbf31c5121e73adb357 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:24:50 +0100 Subject: [PATCH 16/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 29fc81a64f818..91491b62f440c 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -97,8 +97,7 @@ public static function downloadPackage($url, $target = false) $url = $event->getArgument('url', $url); $headers = $event->getArgument('headers', $headers); - if (empty($url)) - { + if (empty($url)) { // Any logging and messaging of this are the responsibility of the event handlers. return false; } From e5927dae0c0a3ee9bd975edccc309a0b98cfffce Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:25:07 +0100 Subject: [PATCH 17/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 91491b62f440c..6b7b05db774a4 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -123,8 +123,7 @@ public static function downloadPackage($url, $target = false) $dispatcher->dispatch('onInstallerAfterPackageDownload', $event); $return = $event->getArgument('return'); - if ($return === false) - { + if ($return === false) { return false; } From 12eafccd3ceb1e54b0d01cb4f4328ade7dd9e6a5 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:25:21 +0100 Subject: [PATCH 18/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 6b7b05db774a4..853b3fdd94d0a 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -127,8 +127,7 @@ public static function downloadPackage($url, $target = false) return false; } - if ($return !== true) - { + if ($return !== true) { return $return; } From d39184c76458460bcd4ff2fcd5a27a6ea8e1f18d Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:25:38 +0100 Subject: [PATCH 19/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 853b3fdd94d0a..6e8a8a8fd167f 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -131,8 +131,7 @@ public static function downloadPackage($url, $target = false) return $return; } - if ($response instanceof \Exception) - { + if ($response instanceof \Exception) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $exception->getMessage()), Log::WARNING, 'jerror'); return false; From a5f0dcac69860343bdb99288202444b0bc27afad Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:25:53 +0100 Subject: [PATCH 20/54] Update administrator/components/com_installer/src/Model/UpdateModel.php Co-authored-by: Richard Fath --- .../components/com_installer/src/Model/UpdateModel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index 4218b3485d7cc..82765ca9c1f54 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -449,8 +449,8 @@ private function install($update) 'type' => 'error', ]); $dispatcher->dispatch('onInstallerPackageDownloadFailed', $event); - $message = $event->getArgument('message', true); - $type = $event->getArgument('type', 'error'); + $message = $event->getArgument('message', true); + $type = $event->getArgument('type', 'error'); if ($message === true) { InstallerHelper::enqueueDownloadFailMessage($url); From 1f668ca6a5dc43267a6dedd2ddb17dc8535508ca Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:26:09 +0100 Subject: [PATCH 21/54] Update libraries/src/Updater/UpdateAdapter.php Co-authored-by: Richard Fath --- libraries/src/Updater/UpdateAdapter.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/src/Updater/UpdateAdapter.php b/libraries/src/Updater/UpdateAdapter.php index e7f040facd634..78cfebeae140c 100644 --- a/libraries/src/Updater/UpdateAdapter.php +++ b/libraries/src/Updater/UpdateAdapter.php @@ -300,8 +300,7 @@ protected function getUpdateSiteResponse($options = []) $newUrl = $event->getArgument('url', $url); $headers = $event->getArgument('headers', $headers); - if (empty($newUrl)) - { + if (empty($newUrl)) { // Any logging and messaging of this are the responsibility of the event handlers. return false; } From 916ef5b41ec0a3f1d15f328dc57dea769415ef22 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:26:41 +0100 Subject: [PATCH 22/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 6e8a8a8fd167f..8f26dfe5e8085 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -83,7 +83,7 @@ public static function downloadPackage($url, $target = false) ini_set('user_agent', $version->getUserAgent('Installer')); // Load installer plugins, and allow URL and headers modification - $headers = []; + $headers = []; $headers['accept'] = 'application/json, application/zip'; // Load installer plugins, and allow URL and headers modification From 27200a4cb62ac5029841fceb8d8425ff2cdced2a Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:26:55 +0100 Subject: [PATCH 23/54] Update libraries/src/Event/Installer/AfterPackageDownloadEvent.php Co-authored-by: Richard Fath --- libraries/src/Event/Installer/AfterPackageDownloadEvent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Event/Installer/AfterPackageDownloadEvent.php b/libraries/src/Event/Installer/AfterPackageDownloadEvent.php index 67f94cae3b858..551c3d0489dc1 100644 --- a/libraries/src/Event/Installer/AfterPackageDownloadEvent.php +++ b/libraries/src/Event/Installer/AfterPackageDownloadEvent.php @@ -18,7 +18,7 @@ /** * Class for Installer events * - * @since 6.1.0 + * @since __DEPLOY_VERSION__ */ class AfterPackageDownloadEvent extends Event { From fb81db3d445ec9e766a3bcae57580da4be50a732 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:27:09 +0100 Subject: [PATCH 24/54] Update libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php Co-authored-by: Richard Fath --- .../src/Event/Installer/AfterPackageDownloadFailedEvent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php b/libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php index 99e7736808b27..da90899ed86c9 100644 --- a/libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php +++ b/libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php @@ -18,7 +18,7 @@ /** * Class for Installer events * - * @since 6.1.0 + * @since __DEPLOY_VERSION__ */ class AfterPackageDownloadFailedEvent extends Event { From fcfd17071e938148015db592e50f5412ff6c013d Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:27:25 +0100 Subject: [PATCH 25/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 8f26dfe5e8085..d114dddb8e511 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -115,10 +115,10 @@ public static function downloadPackage($url, $target = false) $dispatcher = Factory::getApplication()->getDispatcher(); PluginHelper::importPlugin('installer', null, true, $dispatcher); $event = new AfterPackageDownloadEvent('onInstallerAfterPackageDownload', [ - 'url' => &$url, - 'response' => &$response, - 'headers' => &$headers, - 'return' => true, + 'url' => &$url, + 'response' => &$response, + 'headers' => &$headers, + 'return' => true, ]); $dispatcher->dispatch('onInstallerAfterPackageDownload', $event); $return = $event->getArgument('return'); From 377b48cc6ccec7dd063df5ff2cf0b5ffca3fb87e Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:27:42 +0100 Subject: [PATCH 26/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index d114dddb8e511..acb8d2dd44b23 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -121,7 +121,7 @@ public static function downloadPackage($url, $target = false) 'return' => true, ]); $dispatcher->dispatch('onInstallerAfterPackageDownload', $event); - $return = $event->getArgument('return'); + $return = $event->getArgument('return'); if ($return === false) { return false; From 71210a780f925755c32f8da62546da94ede5cb62 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:28:03 +0100 Subject: [PATCH 27/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index acb8d2dd44b23..7836f7e57c6d8 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -147,9 +147,11 @@ public static function downloadPackage($url, $target = false) return false; } - if (!empty($headers['content-type']) + if ( + !empty($headers['content-type']) && !empty($headers['content-type'][0]) - && strpos($headers['content-type'][0], 'application/json') !== false) { + && strpos($headers['content-type'][0], 'application/json') !== false + ) { $response = json_decode($response->body, true); if (isset($response['downloadfailmessage'])) { From 733f46c421a03e1687d387492411207ba3227b0e Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:28:19 +0100 Subject: [PATCH 28/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 7836f7e57c6d8..87e5b0cae4013 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -168,9 +168,11 @@ public static function downloadPackage($url, $target = false) } } - if (!empty($response['error']) + if ( + !empty($response['error']) || empty($response['package']) - || empty($response['target'])) { + || empty($response['target']) + ) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', ''), Log::WARNING, 'jerror'); return false; From 4fee8645ec84023224c644aca8c03aaada999d22 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:28:59 +0100 Subject: [PATCH 29/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 87e5b0cae4013..f9fa436805027 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -175,8 +175,8 @@ public static function downloadPackage($url, $target = false) ) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', ''), Log::WARNING, 'jerror'); - return false; - } + return false; + } $body = base64_decode($response['package']); $target = $response['target']; From d8a7107469d80a97b7bb4fa95993c970034e5804 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:29:17 +0100 Subject: [PATCH 30/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index f9fa436805027..1e761ac5ab5c5 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -198,7 +198,7 @@ public static function downloadPackage($url, $target = false) $target = $tmpPath . '/' . self::getFilenameFromUrl($url); } else { $target = $tmpPath . '/' . basename($target); - } + } // Fix Indirect Modification of Overloaded Property $body = $response->body; From 2ba9f376c4bf1139910e6f589e907ff2387ebb88 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:29:42 +0100 Subject: [PATCH 31/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 1e761ac5ab5c5..3fb07867d4d91 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -200,7 +200,7 @@ public static function downloadPackage($url, $target = false) $target = $tmpPath . '/' . basename($target); } - // Fix Indirect Modification of Overloaded Property + // Fix Indirect Modification of Overloaded Property $body = $response->body; } From 265a5aaf77e2b34c74fc1ca29e5ecf7023b2952b Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:30:02 +0100 Subject: [PATCH 32/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 3fb07867d4d91..c63e35e598bde 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -225,7 +225,8 @@ public static function downloadPackage($url, $target = false) * * @since 6.1 */ - public static function enqueueDownloadFailMessage($url) { + public static function enqueueDownloadFailMessage($url) + { if (empty(self::$downloadFailMessage)) { return; } From 37def21739568b978753e96e37aa4a1b8a0a9372 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:30:18 +0100 Subject: [PATCH 33/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index c63e35e598bde..2f8d3954cdc07 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -201,7 +201,7 @@ public static function downloadPackage($url, $target = false) } // Fix Indirect Modification of Overloaded Property - $body = $response->body; + $body = $response->body; } // Write buffer to file From f7fec30e1872541fa515b5a64c6802be9cd356ae Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:30:40 +0100 Subject: [PATCH 34/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 2f8d3954cdc07..1cc433e2a4c8e 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -62,7 +62,8 @@ abstract class InstallerHelper * Message displayed when download fails. * * @var string - * @since 6.1.0 + + * @since __DEPLOY_VERSION__ */ protected static $downloadFailMessage = 'COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED'; From cd64500e4706a1e7f881931dab73983d21db25cd Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:34:15 +0100 Subject: [PATCH 35/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 1cc433e2a4c8e..f22b4d0480f67 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -181,8 +181,7 @@ public static function downloadPackage($url, $target = false) $body = base64_decode($response['package']); $target = $response['target']; - } - else { + } else { // Parse the Content-Disposition header to get the file name if ( !empty($headers['content-disposition']) From 6e6c7b8a5a0ee2d2ba6b78db81f398d50940c235 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:34:40 +0100 Subject: [PATCH 36/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index f22b4d0480f67..97162c22e6e5a 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -179,7 +179,7 @@ public static function downloadPackage($url, $target = false) return false; } - $body = base64_decode($response['package']); + $body = base64_decode($response['package']); $target = $response['target']; } else { // Parse the Content-Disposition header to get the file name From 674cc567caa5de407ae0a33076b1018e4f496fd3 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:35:16 +0100 Subject: [PATCH 37/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 97162c22e6e5a..db4c79cde9991 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -190,9 +190,9 @@ public static function downloadPackage($url, $target = false) $flds = explode(';', $parts[1]); $target = trim($flds[0], '"'); } - + $tmpPath = Factory::getApplication()->get('tmp_path'); - + // Set the target path if not given if (!$target) { $target = $tmpPath . '/' . self::getFilenameFromUrl($url); From 03b3a66cc2d89748662c34e9f69005a8b2feb69f Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:16:34 +0100 Subject: [PATCH 38/54] Update InstallerHelper.php --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index db4c79cde9991..17bd220a9885a 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -13,7 +13,6 @@ use Joomla\CMS\Event\Installer\AfterPackageDownloadEvent; use Joomla\CMS\Event\Installer\BeforePackageDownloadEvent; use Joomla\CMS\Factory; -use Joomla\CMS\Http\HttpFactory; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Plugin\PluginHelper; @@ -22,6 +21,7 @@ use Joomla\Filesystem\File; use Joomla\Filesystem\Folder; use Joomla\Filesystem\Path; +use Joomla\Http\HttpFactory; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; From ca448e44f8c81bb005500dee4ae0b13315e60d72 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:00:17 +0100 Subject: [PATCH 39/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 17bd220a9885a..d34bc1e1b5f70 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -91,8 +91,8 @@ public static function downloadPackage($url, $target = false) $dispatcher = Factory::getApplication()->getDispatcher(); PluginHelper::importPlugin('installer', null, true, $dispatcher); $event = new BeforePackageDownloadEvent('onInstallerBeforePackageDownload', [ - 'url' => &$url, // @todo: Remove reference in Joomla 6, see BeforePackageDownloadEvent::__constructor() - 'headers' => &$headers, // @todo: Remove reference in Joomla 6, see BeforePackageDownloadEvent::__constructor() + 'url' => &$url, // @todo: Remove reference in Joomla 7, see BeforePackageDownloadEvent::__constructor() + 'headers' => &$headers, // @todo: Remove reference in Joomla 7, see BeforePackageDownloadEvent::__constructor() ]); $dispatcher->dispatch('onInstallerBeforePackageDownload', $event); $url = $event->getArgument('url', $url); From f6c38c61bc3a7901b97bb604148774e1209222f4 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:00:58 +0100 Subject: [PATCH 40/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Richard Fath --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index d34bc1e1b5f70..615e174334a1d 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -104,7 +104,7 @@ public static function downloadPackage($url, $target = false) } try { - $response = HttpFactory::getHttp()->get($url, $headers); + $response = (new HttpFactory())->getHttp()->get($url, $headers); // Convert keys of headers to lowercase, to accommodate for case variations $headers = array_change_key_case($response->headers, CASE_LOWER); From e5f009de6230b1a86db90093329b4cbb23916212 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:22:13 +0100 Subject: [PATCH 41/54] Update libraries/src/Event/Installer/AfterPackageDownloadEvent.php Co-authored-by: Brian Teeman --- libraries/src/Event/Installer/AfterPackageDownloadEvent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Event/Installer/AfterPackageDownloadEvent.php b/libraries/src/Event/Installer/AfterPackageDownloadEvent.php index 551c3d0489dc1..ecbc4e0bc23df 100644 --- a/libraries/src/Event/Installer/AfterPackageDownloadEvent.php +++ b/libraries/src/Event/Installer/AfterPackageDownloadEvent.php @@ -3,7 +3,7 @@ /** * Joomla! Content Management System * - * @copyright (C) 2023 Open Source Matters, Inc. + * @copyright (C) 2025 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt */ From 059df3781dcb4038c88aa50bf6fb130417a638eb Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:22:29 +0100 Subject: [PATCH 42/54] Update libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php Co-authored-by: Brian Teeman --- .../src/Event/Installer/AfterPackageDownloadFailedEvent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php b/libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php index da90899ed86c9..1ef53004bde99 100644 --- a/libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php +++ b/libraries/src/Event/Installer/AfterPackageDownloadFailedEvent.php @@ -3,7 +3,7 @@ /** * Joomla! Content Management System * - * @copyright (C) 2023 Open Source Matters, Inc. + * @copyright (C) 2025 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt */ From e4683cabbfc6b564b0e76391c7b77dc6fee57c97 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:07:10 +0000 Subject: [PATCH 43/54] Refactor response code checks to use getStatusCode --- libraries/src/Installer/InstallerHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 615e174334a1d..5f1c98b914d5a 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -138,11 +138,11 @@ public static function downloadPackage($url, $target = false) return false; } - if (302 == $response->code && !empty($headers['location'])) { + if (302 == $response->getStatusCode() && !empty($headers['location'])) { return self::downloadPackage($headers['location']); } - if (200 != $response->code) { + if (200 != $response->getStatusCode()) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->getStatusCode()), Log::WARNING, 'jerror'); return false; From f6ab3be7c8a1c871bd2b810fbefda460f0f32be0 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:59:07 +0000 Subject: [PATCH 44/54] Update header retrieval method in InstallerHelper --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 5f1c98b914d5a..e3ef5a4566bee 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -107,7 +107,7 @@ public static function downloadPackage($url, $target = false) $response = (new HttpFactory())->getHttp()->get($url, $headers); // Convert keys of headers to lowercase, to accommodate for case variations - $headers = array_change_key_case($response->headers, CASE_LOWER); + $headers = array_change_key_case($response->getHeaders(), CASE_LOWER); } catch (\RuntimeException $exception) { $response = $exception; } From 6a61602e06cb1ef2f4847c350f5211fdc123ca6a Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 4 Nov 2025 21:00:55 +0000 Subject: [PATCH 45/54] Update libraries/src/Installer/InstallerHelper.php Co-authored-by: Brian Teeman --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index e3ef5a4566bee..ec00e3c8e6199 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -223,7 +223,7 @@ public static function downloadPackage($url, $target = false) * * @return void * - * @since 6.1 + * @since __DEPLOY_VERSION__ */ public static function enqueueDownloadFailMessage($url) { From 9866580420d16f9f238acdff747e80f97c51abf4 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 4 Nov 2025 21:23:50 +0000 Subject: [PATCH 46/54] Fix body assignment to use getBody method --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index ec00e3c8e6199..92ca60c776ac7 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -201,7 +201,7 @@ public static function downloadPackage($url, $target = false) } // Fix Indirect Modification of Overloaded Property - $body = $response->body; + $body = (string) $response->getBody(); } // Write buffer to file From 2f268f2ed942804ef5cc60457d89bccfcfef9619 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 4 Nov 2025 21:37:51 +0000 Subject: [PATCH 47/54] Fix json_decode to use getBody method --- libraries/src/Installer/InstallerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 92ca60c776ac7..fda8f98bacfc8 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -153,7 +153,7 @@ public static function downloadPackage($url, $target = false) && !empty($headers['content-type'][0]) && strpos($headers['content-type'][0], 'application/json') !== false ) { - $response = json_decode($response->body, true); + $response = json_decode((string)$response->getBody(), true); if (isset($response['downloadfailmessage'])) { // Empty string will disable the download fail message. From 8f575bb04db691f77d4ea961c488ef09bc6d4260 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:24:52 +0000 Subject: [PATCH 48/54] Enhance URL handling in InstallerHelper, added comments. Add support for dynamic URL generation with Joomla and PHP versions. --- libraries/src/Installer/InstallerHelper.php | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index fda8f98bacfc8..cdfcecd2b81f7 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -103,6 +103,21 @@ public static function downloadPackage($url, $target = false) return false; } + // In the XML returned by the update server the downloadurl value may include optional values using escape fields. + // In the simplest case these values would be the Joomla version and the PHP version. + // Additionally, the value of a Joomla Text Override label could be used as a security token. + // https://...?task=product.download&element=plg_test_test2&file_id=400&updatetype=1&j={joomla_version}&p={php_version}&t={DEVELOPER_UPDATE_TOKEN} + $url = str_replace( + ['{joomla_version}', '{php_version}'], + [JVERSION, PHP_VERSION], + $url); + + preg_match('/{[^}]+}/', $url, $matches); + foreach($matches as $match) + { + $url = str_replace($match, Text::_(trim($match, '{}')), $url); + } + try { $response = (new HttpFactory())->getHttp()->get($url, $headers); @@ -155,6 +170,15 @@ public static function downloadPackage($url, $target = false) ) { $response = json_decode((string)$response->getBody(), true); + /* + Typically this is used for an error response which will look like this: + array ( + 'message' => 'plg_test_test2: The subscription key you provided is expired or invalid. Please purchase a new subscription key.', + 'type' => 'info', 'success' => false, 'error' => true, 'downloadfailmessage' => '', + ) + Json data can be used as an alternative success response in which case it will include package and target data. + */ + if (isset($response['downloadfailmessage'])) { // Empty string will disable the download fail message. // The response message may be used on errors. Allows the message type to be customised. From d3c8ac13927695137a1a0b7b08707c1b595f1155 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Wed, 5 Nov 2025 12:43:51 +0000 Subject: [PATCH 49/54] Refactor URL replacement and formatting in InstallerHelper --- libraries/src/Installer/InstallerHelper.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index cdfcecd2b81f7..f100cd301e004 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -110,10 +110,11 @@ public static function downloadPackage($url, $target = false) $url = str_replace( ['{joomla_version}', '{php_version}'], [JVERSION, PHP_VERSION], - $url); + $url + ); preg_match('/{[^}]+}/', $url, $matches); - foreach($matches as $match) + foreach ($matches as $match) { $url = str_replace($match, Text::_(trim($match, '{}')), $url); } From 07ba211f31a5e7924e97587e98ad499c5ebde4af Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:06:33 +0000 Subject: [PATCH 50/54] Refactor foreach loop syntax in InstallerHelper.php --- libraries/src/Installer/InstallerHelper.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index f100cd301e004..876bb4a55fe38 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -114,8 +114,7 @@ public static function downloadPackage($url, $target = false) ); preg_match('/{[^}]+}/', $url, $matches); - foreach ($matches as $match) - { + foreach ($matches as $match) { $url = str_replace($match, Text::_(trim($match, '{}')), $url); } From 5a636070a07ee5fadd971590e9d09bf197de6d63 Mon Sep 17 00:00:00 2001 From: Richard Fath Date: Sun, 9 Nov 2025 10:37:41 +0100 Subject: [PATCH 51/54] Update phpstan-baseline.neon --- phpstan-baseline.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 85f36c8bda2e4..d3fbc1f19280c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -11875,7 +11875,7 @@ parameters: This interface will be removed without replacement as the Joomla 3\.x compatibility layer will be removed$# ''' identifier: method.deprecatedInterface - count: 1 + count: 2 path: libraries/src/Installer/InstallerHelper.php - From a46cdb4b91b293d411442bf53f85ea397a3e7ffb Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:04:57 +0000 Subject: [PATCH 52/54] Add error message for downloading package --- administrator/language/en-GB/lib_joomla.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index f9ea658e0291d..cd631fbda8f79 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -581,6 +581,7 @@ JLIB_INSTALLER_ERROR_DEPRECATED_FORMAT="Deprecated install format (client=\"both JLIB_INSTALLER_ERROR_DISCOVER_INSTALL_UNSUPPORTED="A %s extension can not be installed using the discover method. Please install this extension from Extension Manager: Install." JLIB_INSTALLER_ERROR_DOWNGRADE="Sorry! You cannot downgrade from version %s to %s" JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT="Error connecting to the server: %s" +JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_MESSAGE="Error downloading the packge: %1$s. %2$s" JLIB_INSTALLER_ERROR_EXTENSION_INVALID_CLIENT_IDENTIFIER="Invalid client identifier specified in extension manifest." JLIB_INSTALLER_ERROR_FAIL_COPY_FILE="JInstaller: :Install: Failed to copy file %1$s to %2$s" JLIB_INSTALLER_ERROR_FAIL_COPY_FOLDER="JInstaller: :Install: Failed to copy folder %1$s to %2$s" From b039143bf281e469badac0320d30f56b6192d9ee Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:32:16 +0000 Subject: [PATCH 53/54] Enhance error handling for 403 response status Add logging for 403 response status with custom message. --- libraries/src/Installer/InstallerHelper.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 876bb4a55fe38..cf80968a2d720 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -160,6 +160,15 @@ public static function downloadPackage($url, $target = false) if (200 != $response->getStatusCode()) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->getStatusCode()), Log::WARNING, 'jerror'); + if (403 === $response->getStatusCode()) { + $body = (string) $response->getBody(); + + // Show response message, but ignore default server error pages + if ($body && !str_contains($body, '')) { + Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_MESSAGE', $url, $body), Log::WARNING, 'jerror'); + } + } + return false; } From 50a0fcd74101a7d4199a5e50be45d762579310f7 Mon Sep 17 00:00:00 2001 From: Jonathan Brain <3941269+BrainforgeUK@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:36:37 +0000 Subject: [PATCH 54/54] Fix typo in download error message --- administrator/language/en-GB/lib_joomla.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index cd631fbda8f79..06543b05dd274 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -581,7 +581,7 @@ JLIB_INSTALLER_ERROR_DEPRECATED_FORMAT="Deprecated install format (client=\"both JLIB_INSTALLER_ERROR_DISCOVER_INSTALL_UNSUPPORTED="A %s extension can not be installed using the discover method. Please install this extension from Extension Manager: Install." JLIB_INSTALLER_ERROR_DOWNGRADE="Sorry! You cannot downgrade from version %s to %s" JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT="Error connecting to the server: %s" -JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_MESSAGE="Error downloading the packge: %1$s. %2$s" +JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_MESSAGE="Error downloading the package: %1$s. %2$s" JLIB_INSTALLER_ERROR_EXTENSION_INVALID_CLIENT_IDENTIFIER="Invalid client identifier specified in extension manifest." JLIB_INSTALLER_ERROR_FAIL_COPY_FILE="JInstaller: :Install: Failed to copy file %1$s to %2$s" JLIB_INSTALLER_ERROR_FAIL_COPY_FOLDER="JInstaller: :Install: Failed to copy folder %1$s to %2$s"