File manager - Edit - /home/ferretapmx/public_html/Update.tar
Back
Collection.php 0000644 00000023727 15231056403 0007362 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace FOF40\Update; defined('_JEXEC') || die; use Exception; use FOF40\Container\Container; use FOF40\Download\Download; use SimpleXMLElement; class Collection { /** * Reads a "collection" XML update source and returns the complete tree of categories * and extensions applicable for platform version $jVersion * * @param string $url The collection XML update source URL to read from * @param string|null $jVersion Joomla! version to fetch updates for, or null to use JVERSION * * @return array A list of update sources applicable to $jVersion */ public function getAllUpdates(string $url, ?string $jVersion = null): array { // Get the target platform if (is_null($jVersion)) { $jVersion = JVERSION; } // Initialise return value $updates = [ 'metadata' => [ 'name' => '', 'description' => '', ], 'categories' => [], 'extensions' => [], ]; // Download and parse the XML file $container = Container::getInstance('com_foobar'); $downloader = new Download($container); $xmlSource = $downloader->getFromURL($url); try { $xml = new SimpleXMLElement($xmlSource, LIBXML_NONET); } catch (Exception $e) { return $updates; } // Sanity check if (($xml->getName() != 'extensionset')) { unset($xml); return $updates; } // Initialise return value with the stream metadata (name, description) $rootAttributes = $xml->attributes(); foreach ($rootAttributes as $k => $v) { $updates['metadata'][$k] = (string) $v; } // Initialise the raw list of updates $rawUpdates = [ 'categories' => [], 'extensions' => [], ]; // Segregate the raw list to a hierarchy of extension and category entries /** @var SimpleXMLElement $extension */ foreach ($xml->children() as $extension) { switch ($extension->getName()) { case 'category': // These are the parameters we expect in a category $params = [ 'name' => '', 'description' => '', 'category' => '', 'ref' => '', 'targetplatformversion' => $jVersion, ]; // These are the attributes of the element $attributes = $extension->attributes(); // Merge them all foreach ($attributes as $k => $v) { $params[$k] = (string) $v; } // We can't have a category with an empty category name if (empty($params['category'])) { continue; } // We can't have a category with an empty ref if (empty($params['ref'])) { continue; } if (empty($params['description'])) { $params['description'] = $params['category']; } if (!array_key_exists($params['category'], $rawUpdates['categories'])) { $rawUpdates['categories'][$params['category']] = []; } $rawUpdates['categories'][$params['category']][] = $params; break; case 'extension': // These are the parameters we expect in a category $params = [ 'element' => '', 'type' => '', 'version' => '', 'name' => '', 'detailsurl' => '', 'targetplatformversion' => $jVersion, ]; // These are the attributes of the element $attributes = $extension->attributes(); // Merge them all foreach ($attributes as $k => $v) { $params[$k] = (string) $v; } // We can't have an extension with an empty element if (empty($params['element'])) { continue; } // We can't have an extension with an empty type if (empty($params['type'])) { continue; } // We can't have an extension with an empty version if (empty($params['version'])) { continue; } if (empty($params['name'])) { $params['name'] = $params['element'] . ' ' . $params['version']; } if (!array_key_exists($params['type'], $rawUpdates['extensions'])) { $rawUpdates['extensions'][$params['type']] = []; } if (!array_key_exists($params['element'], $rawUpdates['extensions'][$params['type']])) { $rawUpdates['extensions'][$params['type']][$params['element']] = []; } $rawUpdates['extensions'][$params['type']][$params['element']][] = $params; break; default: break; } } unset($xml); foreach ($rawUpdates['categories'] as $category => $entries) { $update = $this->filterListByPlatform($entries, $jVersion); $updates['categories'][$category] = $update; } foreach ($rawUpdates['extensions'] as $type => $extensions) { $updates['extensions'][$type] = []; foreach ($extensions as $element => $entries) { $update = $this->filterListByPlatform($entries, $jVersion); $updates['extensions'][$type][$element] = $update; } } return $updates; } /** * Returns only the category definitions of a collection * * @param string $url The URL of the collection update source * @param string|null $jVersion Joomla! version to fetch updates for, or null to use JVERSION * * @return array An array of category update definitions */ public function getCategories(string $url, ?string $jVersion = null): array { $allUpdates = $this->getAllUpdates($url, $jVersion); return $allUpdates['categories']; } /** * Returns the update source for a specific category * * @param string $url The URL of the collection update source * @param string $category The category name you want to get the update source URL of * @param string|null $jVersion Joomla! version to fetch updates for, or null to use JVERSION * * @return string|null The update stream URL, or null if it's not found */ public function getCategoryUpdateSource(string $url, string $category, ?string $jVersion = null): ?string { $allUpdates = $this->getAllUpdates($url, $jVersion); if (array_key_exists($category, $allUpdates['categories'])) { return $allUpdates['categories'][$category]['ref']; } else { return null; } } /** * Get a list of updates for extensions only, optionally of a specific type * * @param string $url The URL of the collection update source * @param string $type The extension type you want to get the update source URL of, empty to get all * extension types * @param string|null $jVersion Joomla! version to fetch updates for, or null to use JVERSION * * @return array|null An array of extension update definitions or null if none is found */ public function getExtensions(string $url, ?string $type = null, ?string $jVersion = null): ?array { $allUpdates = $this->getAllUpdates($url, $jVersion); if (empty($type)) { return $allUpdates['extensions']; } elseif (array_key_exists($type, $allUpdates['extensions'])) { return $allUpdates['extensions'][$type]; } else { return null; } } /** * Get the update source URL for a specific extension, based on the type and element, e.g. * type=file and element=joomla is Joomla! itself. * * @param string $url The URL of the collection update source * @param string $type The extension type you want to get the update source URL of * @param string $element The extension element you want to get the update source URL of * @param string|null $jVersion Joomla! version to fetch updates for, or null to use JVERSION * * @return string|null The update source URL or null if the extension is not found */ public function getExtensionUpdateSource(string $url, string $type, string $element, ?string $jVersion = null): ?string { $allUpdates = $this->getExtensions($url, $type, $jVersion); if (empty($allUpdates)) { return null; } elseif (array_key_exists($element, $allUpdates)) { return $allUpdates[$element]['detailsurl']; } else { return null; } } /** * Filters a list of updates, returning only those available for the * specified platform version $jVersion * * @param array $updates An array containing update definitions (categories or extensions) * @param string $jVersion Joomla! version to fetch updates for, or null to use JVERSION * * @return array|null The update definition that is compatible, or null if none is compatible */ private function filterListByPlatform(array $updates, ?string $jVersion = null): ?array { // Get the target platform if (is_null($jVersion)) { $jVersion = JVERSION; } $versionParts = explode('.', $jVersion, 4); $platformVersionMajor = $versionParts[0]; $platformVersionMinor = (count($versionParts) > 1) ? $platformVersionMajor . '.' . $versionParts[1] : $platformVersionMajor; $platformVersionNormal = (count($versionParts) > 2) ? $platformVersionMinor . '.' . $versionParts[2] : $platformVersionMinor; $platformVersionFull = (count($versionParts) > 3) ? $platformVersionNormal . '.' . $versionParts[3] : $platformVersionNormal; $pickedExtension = null; $pickedSpecificity = -1; foreach ($updates as $update) { // Test the target platform $targetPlatform = (string) $update['targetplatformversion']; if ($targetPlatform === $platformVersionFull) { $pickedExtension = $update; $pickedSpecificity = 4; } elseif (($targetPlatform === $platformVersionNormal) && ($pickedSpecificity <= 3)) { $pickedExtension = $update; $pickedSpecificity = 3; } elseif (($targetPlatform === $platformVersionMinor) && ($pickedSpecificity <= 2)) { $pickedExtension = $update; $pickedSpecificity = 2; } elseif (($targetPlatform === $platformVersionMajor) && ($pickedSpecificity <= 1)) { $pickedExtension = $update; $pickedSpecificity = 1; } } return $pickedExtension; } } Extension.php 0000644 00000006666 15231056403 0007246 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace FOF40\Update; defined('_JEXEC') || die; use Exception; use FOF40\Container\Container; use FOF40\Download\Download; use SimpleXMLElement; /** * A helper class to read and parse "extension" update XML files over the web */ class Extension { /** * Reads an "extension" XML update source and returns all listed update entries. * * If you have a "collection" XML update source you should do something like this: * $collection = new \FOF40\Update\Collection(); * $extensionUpdateURL = $collection->getExtensionUpdateSource($url, 'component', 'com_foobar', JVERSION); * $extension = new \FOF40\Update\Extension(); * $updates = $extension->getUpdatesFromExtension($extensionUpdateURL); * * @param string $url The extension XML update source URL to read from * * @return array An array of update entries */ public function getUpdatesFromExtension(string $url): array { // Initialise $ret = []; // Get and parse the XML source $container = Container::getInstance('com_FOOBAR'); $downloader = new Download($container); $xmlSource = $downloader->getFromURL($url); try { $xml = new SimpleXMLElement($xmlSource, LIBXML_NONET); } catch (Exception $e) { return $ret; } // Sanity check if (($xml->getName() != 'updates')) { unset($xml); return $ret; } // Let's populate the list of updates /** @var SimpleXMLElement $update */ foreach ($xml->children() as $update) { // Sanity check if ($update->getName() != 'update') { continue; } $entry = [ 'infourl' => ['title' => '', 'url' => ''], 'downloads' => [], 'tags' => [], 'targetplatform' => [], ]; $properties = get_object_vars($update); foreach ($properties as $nodeName => $nodeContent) { switch ($nodeName) { default: $entry[$nodeName] = $nodeContent; break; case 'infourl': case 'downloads': case 'tags': case 'targetplatform': break; } } $infourlNode = $update->xpath('infourl'); $entry['infourl']['title'] = (string) $infourlNode[0]['title']; $entry['infourl']['url'] = (string) $infourlNode[0]; $downloadNodes = $update->xpath('downloads/downloadurl'); foreach ($downloadNodes as $downloadNode) { $entry['downloads'][] = [ 'type' => (string) $downloadNode['type'], 'format' => (string) $downloadNode['format'], 'url' => (string) $downloadNode, ]; } $tagNodes = $update->xpath('tags/tag'); foreach ($tagNodes as $tagNode) { $entry['tags'][] = (string) $tagNode; } /** @var SimpleXMLElement[] $targetPlatformNode */ $targetPlatformNode = $update->xpath('targetplatform'); $entry['targetplatform']['name'] = (string) $targetPlatformNode[0]['name']; $entry['targetplatform']['version'] = (string) $targetPlatformNode[0]['version']; $client = $targetPlatformNode[0]->xpath('client'); $entry['targetplatform']['client'] = (is_array($client) && count($client)) ? (string) $client[0] : ''; $folder = $targetPlatformNode[0]->xpath('folder'); $entry['targetplatform']['folder'] = is_array($folder) && count($folder) ? (string) $folder[0] : ''; $ret[] = $entry; } unset($xml); return $ret; } } Joomla.php 0000644 00000031447 15231056403 0006506 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace FOF40\Update; defined('_JEXEC') || die; class Joomla extends Extension { /** * The source for LTS updates * * @var string */ protected static $lts_url = 'http://update.joomla.org/core/list.xml'; /** * The source for STS updates * * @var string */ protected static $sts_url = 'http://update.joomla.org/core/sts/list_sts.xml'; /** * The source for test release updates * * @var string */ protected static $test_url = 'http://update.joomla.org/core/test/list_test.xml'; /** * Reads a "collection" XML update source and picks the correct source URL * for the extension update source. * * @param string $url The collection XML update source URL to read from * @param string|null $jVersion Joomla! version to fetch updates for, or null to use JVERSION * * @return string The URL of the extension update source, or empty if no updates are provided / fetching failed */ public function getUpdateSourceFromCollection(string $url, ?string $jVersion = null): string { $provider = new Collection(); return $provider->getExtensionUpdateSource($url, 'file', 'joomla', $jVersion); } /** * Determines the properties of a version: STS/LTS, normal or testing * * @param string $jVersion The version number to check * @param string|null $currentVersion The current Joomla! version number * * @return array The properties analysis */ public function getVersionProperties(string $jVersion, ?string $currentVersion = null): array { // Initialise $ret = [ 'lts' => true, // Is this an LTS release? False means STS. 'current' => false, // Is this a release in the $currentVersion branch? 'upgrade' => 'none', // Upgrade relation of $jVersion to $currentVersion: 'none' (can't upgrade), 'lts' (next or current LTS), 'sts' (next or current STS) or 'current' (same release, no upgrade available) 'testing' => false, // Is this a testing (alpha, beta, RC) release? ]; // Get the current version if none is defined if (is_null($currentVersion)) { $currentVersion = JVERSION; } // Sanitise version numbers $sameVersion = $jVersion === $currentVersion; $jVersion = $this->sanitiseVersion($jVersion); $currentVersion = $this->sanitiseVersion($currentVersion); $sameVersion = $sameVersion || ($jVersion === $currentVersion); // Get the base version $baseVersion = substr($jVersion, 0, 3); // Get the minimum and maximum current version numbers $current_minimum = substr($currentVersion, 0, 3); $current_maximum = $current_minimum . '.9999'; // Initialise STS/LTS version numbers $sts_minimum = false; $sts_maximum = false; $lts_minimum = false; // Is it an LTS or STS release? switch ($baseVersion) { case '1.5': $ret['lts'] = true; break; case '1.6': $ret['lts'] = false; $sts_minimum = '1.7'; $sts_maximum = '1.7.999'; $lts_minimum = '2.5'; break; case '1.7': $ret['lts'] = false; $sts_minimum = false; $lts_minimum = '2.5'; break; case '2.5': $ret['lts'] = true; $sts_minimum = false; $lts_minimum = '2.5'; break; default: $majorVersion = (int) substr($jVersion, 0, 1); //$minorVersion = (int) substr($jVersion, 2, 1); $ret['lts'] = true; $sts_minimum = false; $lts_minimum = $majorVersion . '.0'; break; } // Is it a current release? if (version_compare($jVersion, $current_minimum, 'ge') && version_compare($jVersion, $current_maximum, 'le')) { $ret['current'] = true; } // Is this a testing release? $versionParts = explode('.', $jVersion); $lastVersionPart = array_pop($versionParts); if (in_array(substr($lastVersionPart, 0, 1), ['a', 'b'])) { $ret['testing'] = true; } elseif (substr($lastVersionPart, 0, 2) == 'rc') { $ret['testing'] = true; } elseif (substr($lastVersionPart, 0, 3) == 'dev') { $ret['testing'] = true; } // Find the upgrade relation of $jVersion to $currentVersion if (version_compare($jVersion, $currentVersion, 'eq')) { $ret['upgrade'] = 'current'; } elseif (($sts_minimum !== false) && version_compare($jVersion, $sts_minimum, 'ge') && version_compare($jVersion, $sts_maximum, 'le')) { $ret['upgrade'] = 'sts'; } elseif (($lts_minimum !== false) && version_compare($jVersion, $lts_minimum, 'ge')) { $ret['upgrade'] = 'lts'; } elseif ($baseVersion === $current_minimum) { $ret['upgrade'] = $ret['lts'] ? 'lts' : 'sts'; } else { $ret['upgrade'] = 'none'; } if ($sameVersion) { $ret['upgrade'] = 'none'; } return $ret; } /** * Filters a list of updates, making sure they apply to the specified CMS * release. * * @param array $updates A list of update records returned by the getUpdatesFromExtension method * @param string|null $jVersion The current Joomla! version number * * @return array A filtered list of updates. Each update record also includes version relevance information. */ public function filterApplicableUpdates(array $updates, ?string $jVersion = null): array { if (empty($jVersion)) { $jVersion = JVERSION; } $versionParts = explode('.', $jVersion, 4); $platformVersionMajor = $versionParts[0]; $platformVersionMinor = $platformVersionMajor . '.' . $versionParts[1]; //$platformVersionNormal = $platformVersionMinor . '.' . $versionParts[2]; //$platformVersionFull = (count($versionParts) > 3) ? $platformVersionNormal . '.' . $versionParts[3] : $platformVersionNormal; $ret = []; foreach ($updates as $update) { // Check each update for platform match if (strtolower($update['targetplatform']['name']) != 'joomla') { continue; } $targetPlatformVersion = $update['targetplatform']['version']; if (!preg_match('/' . $targetPlatformVersion . '/', $platformVersionMinor)) { continue; } // Get some information from the version number $updateVersion = $update['version']; $versionProperties = $this->getVersionProperties($updateVersion, $jVersion); if ($versionProperties['upgrade'] == 'none') { continue; } // The XML files are ill-maintained. Maybe we already have this update? if (!array_key_exists($updateVersion, $ret)) { $ret[$updateVersion] = array_merge($update, $versionProperties); } } return $ret; } /** * Joomla! has a lousy track record in naming its alpha, beta and release * candidate releases. The convention used seems to be "what the hell the * current package maintainer thinks looks better". This method tries to * figure out what was in the mind of the maintainer and translate the * funky version number to an actual PHP-format version string. * * @param string $version The whatever-format version number * * @return string A standard formatted version number */ public function sanitiseVersion(string $version): string { $test = strtolower($version); $alphaQualifierPosition = strpos($test, 'alpha-'); $betaQualifierPosition = strpos($test, 'beta-'); $betaQualifierPosition2 = strpos($test, '-beta'); $rcQualifierPosition = strpos($test, 'rc-'); $rcQualifierPosition2 = strpos($test, '-rc'); $rcQualifierPosition3 = strpos($test, 'rc'); $devQualifiedPosition = strpos($test, 'dev'); if ($alphaQualifierPosition !== false) { $betaRevision = substr($test, $alphaQualifierPosition + 6); if ($betaRevision === '') { $betaRevision = 1; } $test = substr($test, 0, $alphaQualifierPosition) . '.a' . $betaRevision; } elseif ($betaQualifierPosition !== false) { $betaRevision = substr($test, $betaQualifierPosition + 5); if ($betaRevision === '') { $betaRevision = 1; } $test = substr($test, 0, $betaQualifierPosition) . '.b' . $betaRevision; } elseif ($betaQualifierPosition2 !== false) { $betaRevision = substr($test, $betaQualifierPosition2 + 5); if ($betaRevision === '') { $betaRevision = 1; } $test = substr($test, 0, $betaQualifierPosition2) . '.b' . $betaRevision; } elseif ($rcQualifierPosition !== false) { $betaRevision = substr($test, $rcQualifierPosition + 5); if ($betaRevision === '') { $betaRevision = 1; } $test = substr($test, 0, $rcQualifierPosition) . '.rc' . $betaRevision; } elseif ($rcQualifierPosition2 !== false) { $betaRevision = substr($test, $rcQualifierPosition2 + 3); if ($betaRevision === '') { $betaRevision = 1; } $test = substr($test, 0, $rcQualifierPosition2) . '.rc' . $betaRevision; } elseif ($rcQualifierPosition3 !== false) { $betaRevision = substr($test, $rcQualifierPosition3 + 5); if ($betaRevision === '') { $betaRevision = 1; } $test = substr($test, 0, $rcQualifierPosition3) . '.rc' . $betaRevision; } elseif ($devQualifiedPosition !== false) { $betaRevision = substr($test, $devQualifiedPosition + 6); if ($betaRevision === '') { $betaRevision = ''; } $test = substr($test, 0, $devQualifiedPosition) . '.dev' . $betaRevision; } return $test; } /** * Reloads the list of all updates available for the specified Joomla! version * from the network. * * @param array $sources The enabled sources to look into * @param string|null $jVersion The Joomla! version we are checking updates for * * @return array A list of updates for the installed, current, lts and sts versions */ public function getUpdates(array $sources = [], ?string $jVersion = null): array { // Make sure we have a valid list of sources if (empty($sources) || !is_array($sources)) { $sources = []; } $defaultSources = ['lts' => true, 'sts' => true, 'test' => true, 'custom' => '']; $sources = array_merge($defaultSources, $sources); // Use the current JVERSION if none is specified if (empty($jVersion)) { $jVersion = JVERSION; } // Get the current branch' min/max versions $versionParts = explode('.', $jVersion, 4); $currentMinVersion = $versionParts[0] . '.' . $versionParts[1]; $currentMaxVersion = $versionParts[0] . '.' . $versionParts[1] . '.9999'; // Retrieve all updates $allUpdates = []; foreach ($sources as $source => $value) { if (($value === false) || empty($value)) { continue; } switch ($source) { default: case 'lts': $url = self::$lts_url; break; case 'sts': $url = self::$sts_url; break; case 'test': $url = self::$test_url; break; case 'custom': $url = $value; break; } $url = $this->getUpdateSourceFromCollection($url, $jVersion); if (!empty($url)) { $updates = $this->getUpdatesFromExtension($url); if (!empty($updates)) { $applicableUpdates = $this->filterApplicableUpdates($updates, $jVersion); if (!empty($applicableUpdates)) { $allUpdates = array_merge($allUpdates, $applicableUpdates); } } } } $ret = [ // Currently installed version (used to reinstall, if available) 'installed' => [ 'version' => '', 'package' => '', 'infourl' => '', ], // Current branch 'current' => [ 'version' => '', 'package' => '', 'infourl' => '', ], // Upgrade to STS release 'sts' => [ 'version' => '', 'package' => '', 'infourl' => '', ], // Upgrade to LTS release 'lts' => [ 'version' => '', 'package' => '', 'infourl' => '', ], // Upgrade to LTS release 'test' => [ 'version' => '', 'package' => '', 'infourl' => '', ], ]; foreach ($allUpdates as $update) { $sections = []; if ($update['upgrade'] == 'current') { $sections[0] = 'installed'; } elseif (version_compare($update['version'], $currentMinVersion, 'ge') && version_compare($update['version'], $currentMaxVersion, 'le')) { $sections[0] = 'current'; } else { $sections[0] = ''; } $sections[1] = $update['lts'] ? 'lts' : 'sts'; if ($update['testing']) { $sections = ['test']; } foreach ($sections as $section) { if (empty($section)) { continue; } $existingVersionForSection = $ret[$section]['version']; if (empty($existingVersionForSection)) { $existingVersionForSection = '0.0.0'; } if (version_compare($update['version'], $existingVersionForSection, 'ge')) { $ret[$section]['version'] = $update['version']; $ret[$section]['package'] = $update['downloads'][0]['url']; $ret[$section]['infourl'] = $update['infourl']['url']; } } } // Catch the case when the latest current branch version is the installed version (up to date site) if (empty($ret['current']['version']) && !empty($ret['installed']['version'])) { $ret['current'] = $ret['installed']; } return $ret; } } Update.php 0000644 00000107470 15231056403 0006507 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace FOF40\Update; defined('_JEXEC') || die; use Exception; use FOF40\Container\Container; use FOF40\JoomlaAbstraction\CacheCleaner; use FOF40\Model\Model; use Joomla\CMS\Component\ComponentHelper as JComponentHelper; use Joomla\CMS\Updater\Updater as JUpdater; use SimpleXMLElement; /** * A helper Model to interact with Joomla!'s extensions update feature */ class Update extends Model { /** @var JUpdater The Joomla! updater object */ protected $updater; /** @var int The extension_id of this component */ protected $extension_id = 0; /** @var string The currently installed version, as reported by the #__extensions table */ protected $version = 'dev'; /** @var string The name of the component e.g. com_something */ protected $component = 'com_foobar'; /** @var string The URL to the component's update XML stream */ protected $updateSite; /** @var string The name to the component's update site (description of the update XML stream) */ protected $updateSiteName; /** @var string The extra query to append to (commercial) components' download URLs */ protected $extraQuery; /** @var string The component Options key which stores a copy of the Download ID */ protected $paramsKey = 'update_dlid'; /** * Caches the extension names to IDs so we don't query the database too many times. * * @var array */ private $extensionIds = []; /** * Public constructor. Initialises the protected members as well. Useful $config keys: * update_component The component name, e.g. com_foobar * update_version The default version if the manifest cache is unreadable * update_site The URL to the component's update XML stream * update_extraquery The extra query to append to (commercial) components' download URLs * update_sitename The update site's name (description) * update_paramskey The component parameters key which holds the license key in J3 (and a copy of it in J4) * * @param array|Container|\ArrayAccess $config */ public function __construct($config = []) { $container = Container::getInstance('com_FOOBAR'); if (isset($config['update_container']) && is_object($config['update_container']) && ($config['update_container'] instanceof Container)) { $container = $config['update_container']; } parent::__construct($container); // Get an instance of the updater class $this->updater = JUpdater::getInstance(); $this->component = $config['update_component'] ?? $this->input->getCmd('option', ''); // Get the component version if (isset($config['update_version'])) { $this->version = $config['update_version']; } // Get the update site if (isset($config['update_site'])) { $this->updateSite = $config['update_site']; } // Get the extra query if (isset($config['update_extraquery'])) { $this->extraQuery = $config['update_extraquery']; } // Get the extra query if (isset($config['update_sitename'])) { $this->updateSiteName = $config['update_sitename']; } // Get the extra query if (isset($config['update_paramskey'])) { $this->paramsKey = $config['update_paramskey']; } // Get the extension type $extension = $this->getExtensionObject(); if (is_object($extension)) { $this->extension_id = $extension->extension_id; if (empty($this->version) || ($this->version == 'dev')) { $data = json_decode($extension->manifest_cache, true); if (isset($data['version'])) { $this->version = $data['version']; } } } } /** * Gets the license key for a paid extension. * * On Joomla! 3 or when $forceLegacy is true we look in the component Options. * * On Joomla! 4 we use the information in the dlid element of the extension's XML manifest to parse the extra_query * fields of all configured update sites of the extension. This is the same thing Joomla does when it tries to * determine the license key of our extension when installing updates. If the extension is missing, it has no * associated update sites, the update sites are missing / rebuilt / disassociated from the extension or the * extra_query of all update site records is empty we parse the $extraQuery set in the constructor, if any. Also * note that on Joomla 4 mode if the extension does not exist, does not have a manifest or does not have a valid * dlid element in its manifest we will end up returning an empty string, just like Joomla! itself would have done * when installing updates. * * @param bool $forceLegacy Should I always retrieve the legacy license key, even in J4? * * @return string */ public function getLicenseKey(bool $forceLegacy = false): string { $legacyParamsKey = $this->getLegacyParamsKey(); // Joomla 3 (Legacy): Download ID stored in the component options if ($forceLegacy || !version_compare(JVERSION, '3.999.999', 'gt')) { return $this->container->params->get($legacyParamsKey, ''); } // Joomla! 4. We need to parse the extra_query of the update sites to get the correct Download ID. $updateSites = $this->getUpdateSites(); $extra_query = array_reduce($updateSites, function ($extra_query, $updateSite) { if (!empty($extra_query)) { return $extra_query; } return $updateSite['extra_query']; }, ''); // Fall back to legacy extra query if (empty($extra_query)) { $extra_query = $this->extraQuery; } // Return the parsed results. return $this->getLicenseKeyFromExtraQuery($extra_query); } /** * Get the contents of all the update sites of the configured extension * * @return array|null */ public function getUpdateSites(): ?array { $updateSiteIDs = $this->getUpdateSiteIds(); $db = $this->container->db; $query = $db->getQuery(true) ->select('*') ->from($db->qn('#__update_sites')) ->where($db->qn('update_site_id') . ' IN (' . implode(', ', $updateSiteIDs) . ')'); try { $db->setQuery($query); $ret = $db->loadAssocList('update_site_id'); } catch (Exception $e) { $ret = null; } return empty($ret) ? [] : $ret; } public function setLicenseKey(string $licenseKey) { $legacyParamsKey = $this->getLegacyParamsKey(); // Sanitize and validate the license key. If it's not valid we set an empty license key. $licenseKey = $this->sanitizeLicenseKey($licenseKey); if (!$this->isValidLicenseKey($licenseKey)) { $licenseKey = ''; } // Update $this->extraQuery. $this->extraQuery = $this->getExtraQueryString($licenseKey); // Save the license key in the component options ($legacyParamsKey) $this->container->params->set($legacyParamsKey, $licenseKey); $this->container->params->save(); // Apply the new extra_query to the update site $this->refreshUpdateSite(); } /** * Copies a Joomla 3 license key from the Options storage to Joomla 4 download key storage (the extra_query column * of the #__update_sites table). * * This method does nothing on Joomla 3. * * @return void */ public function upgradeLicenseKey(): void { // Only applies to Joomla! 4 if (!version_compare(JVERSION, '3.999.999', 'gt')) { return; } // Make sure we DO have a legacy license key $legacyKey = $this->getLicenseKey(true); if (empty($legacyKey)) { return; } // Make sure we DO NOT have a J4 key. If we do, the J4 key wins and gets backported to legacy storage. $licenseKey = $this->getLicenseKey(); if (!empty($licenseKey)) { $this->backportLicenseKey(); return; } // Save the legacy key as non-legacy. This updates the #__update_sites record, applying the license key. $this->setLicenseKey($legacyKey); } /** * Copies a Joomla 4 license key from the download key storage (the extra_query column of the #__update_sites table) * to the legacy Options storage. * * This method does nothing on Joomla 3. * * @return void */ public function backportLicenseKey(): void { $legacyParamsKey = $this->getLegacyParamsKey(); // Only applies to Joomla! 4 if (!version_compare(JVERSION, '3.999.999', 'gt')) { return; } // Make sure we DO have a J4 key $licenseKey = $this->getLicenseKey(); if (empty($licenseKey)) { return; } // Make sure that the legacy key is NOT the same as the J4 key $legacyKey = $this->getLicenseKey(true); if ($legacyKey === $licenseKey) { return; } // Save the license key to the legacy storage (component options) $this->container->params->set($legacyParamsKey, $licenseKey); $this->container->params->save(); } /** * Get an extra query string based on the dlid element of the XML manifest file of the extension. * * If the extension does not exist, the manifest does not exist or it does not have a dlid element we fall back to * the legacy implementation of extra_query (getExtraQueryStringLegacy) * * @param string $licenseKey * * @return string */ public function getExtraQueryString(string $licenseKey): string { // Make sure the (sanitized) license key is valid. Otherwise we return an empty string. $licenseKey = $this->sanitizeLicenseKey($licenseKey); if (!$this->isValidLicenseKey($licenseKey)) { return ''; } // Get a fallback extra query using the legacy method $fallbackExtraQuery = $this->getExtraQueryStringLegacy($licenseKey); // Get the extension XML manifest. If the extension or the manifest don't exist use the fallback extra_query. $extension = $this->getExtensionObject(); if (!$extension) { return $fallbackExtraQuery; } $installXmlFile = $this->getManifestXML( $extension->element, $extension->type, (int) $extension->client_id, $extension->folder ); if (!$installXmlFile) { return $fallbackExtraQuery; } // If the manifest does not have the dlid element return the fallback extra_query. if (!isset($installXmlFile->dlid)) { return $fallbackExtraQuery; } $prefix = (string) $installXmlFile->dlid['prefix']; $suffix = (string) $installXmlFile->dlid['suffix']; return $prefix . $this->sanitizeLicenseKey($licenseKey) . $suffix; } /** * Retrieves the update information of the component, returning an array with the following keys: * * hasUpdate True if an update is available * version The version of the available update * infoURL The URL to the download page of the update * * @param bool $force Set to true if you want to forcibly reload the update information * * @return array See the method description for more information */ public function getUpdates(bool $force = false): array { $db = $this->container->db; // Default response (no update) $updateResponse = [ 'hasUpdate' => false, 'version' => '', 'infoURL' => '', ]; if (empty($this->extension_id)) { return $updateResponse; } // If we had to update the version number stored in the database then we should force reload the updates if ($this->updatedCachedVersionNumber()) { $force = true; } // If we are forcing the reload, set the last_check_timestamp to 0 // and remove cached component update info in order to force a reload if ($force) { // Find the update site IDs $updateSiteIds = $this->getUpdateSiteIds(); if (empty($updateSiteIds)) { return $updateResponse; } // Set the last_check_timestamp to 0 $query = $db->getQuery(true) ->update($db->qn('#__update_sites')) ->set($db->qn('last_check_timestamp') . ' = ' . $db->q('0')) ->where($db->qn('update_site_id') . ' IN (' . implode(', ', $updateSiteIds) . ')'); $db->setQuery($query); $db->execute(); // Remove cached component update info from #__updates $query = $db->getQuery(true) ->delete($db->qn('#__updates')) ->where($db->qn('update_site_id') . ' IN (' . implode(', ', $updateSiteIds) . ')'); $db->setQuery($query); $db->execute(); } // Use the update cache timeout specified in com_installer $comInstallerParams = JComponentHelper::getParams('com_installer', false); $timeout = 3600 * $comInstallerParams->get('cachetimeout', '6'); // Load any updates from the network into the #__updates table $this->updater->findUpdates($this->extension_id, $timeout); // Get the update record from the database $query = $db->getQuery(true) ->select('*') ->from($db->qn('#__updates')) ->where($db->qn('extension_id') . ' = ' . $db->q($this->extension_id)); $db->setQuery($query); $updateRecord = $db->loadObject(); // If we have an update record in the database return the information found there if (is_object($updateRecord)) { $updateResponse = [ 'hasUpdate' => true, 'version' => $updateRecord->version, 'infoURL' => $updateRecord->infourl, ]; } return $updateResponse; } /** * Gets the update site Ids for our extension. * * @return array An array of IDs */ public function getUpdateSiteIds(): array { $db = $this->container->db; $query = $db->getQuery(true) ->select($db->qn('update_site_id')) ->from($db->qn('#__update_sites_extensions')) ->where($db->qn('extension_id') . ' = ' . $db->q($this->extension_id)); $db->setQuery($query); try { $ret = $db->loadColumn(0); } catch (Exception $e) { $ret = null; } return is_array($ret) ? $ret : []; } /** * Get the currently installed version as reported by the #__extensions table * * @return string */ public function getVersion(): string { return $this->version; } /** * Override the currently installed version as reported by the #__extensions table * * @param string $version */ public function setVersion(string $version): void { $this->version = $version; } /** * Refreshes the Joomla! update sites for this extension as needed * * @return void */ public function refreshUpdateSite(): void { if (empty($this->extension_id)) { return; } /** * Record whether I have made any changes to the update sites or the updates themselves so I can clear Joomla's * undocumented cache. */ $madeChanges = [ 'updateSites' => false, 'updates' => false, ]; /** * Create the update site definition we want to see in (or store into) the database. * * Since this is called primarily by our own software I am taking the liberty of setting up an extra_query if * none is provided. The code here will prefer a manually set up extra_query. If none is present we will create * one. There are two parts handled by $this->getExtraQueryString($this->getLicenseKey()). * * **License key**. On Joomla 3 we read it from the component's Options. On Joomla 4 we try to first read it * from the #__update_sites table and the extra_query format in the XML manifest. If either is not present we * will fall back to the component itself. * * **Extra query format**. We try to read the format from the XML manifest of the extension. If it's not set up * we fall back to the Akeeba-comaptible format dlid=YOUR_DOWNLOAD_ID_HERE. * * This provides maximum flexibility and works with both Joomla 4 and Joomla 3 equally. */ $update_site = [ 'name' => $this->updateSiteName, 'type' => 'extension', 'location' => $this->updateSite, 'enabled' => 1, 'last_check_timestamp' => 0, 'extra_query' => $this->extraQuery ?? $this->getExtraQueryString($this->getLicenseKey()), ]; // Get a reference to the db driver $db = $this->container->db; // Get the #__update_sites columns $columns = $db->getTableColumns('#__update_sites', true); if (!array_key_exists('extra_query', $columns)) { unset($update_site['extra_query']); } // Get the update sites for our extension $updateSiteIds = $this->getUpdateSiteIds(); if (empty($updateSiteIds)) { $updateSiteIds = []; } /** @var boolean $needNewUpdateSite Do I need to create a new update site? */ $needNewUpdateSite = true; /** @var int[] $deleteOldSites Old Site IDs to delete */ $deleteOldSites = []; // Loop through all update sites foreach ($updateSiteIds as $id) { $query = $db->getQuery(true) ->select('*') ->from($db->qn('#__update_sites')) ->where($db->qn('update_site_id') . ' = ' . $db->q($id)); $db->setQuery($query); $aSite = $db->loadObject(); if (empty($aSite)) { // Update site does not exist?! continue; } // We have an update site that looks like ours if ($needNewUpdateSite && ($aSite->name == $update_site['name']) && ($aSite->location == $update_site['location'])) { $needNewUpdateSite = false; $mustUpdate = false; // Is it enabled? If not, enable it. if (!$aSite->enabled) { $mustUpdate = true; $aSite->enabled = 1; } // Do we have the extra_query property (J 3.2+) and does it match? if (property_exists($aSite, 'extra_query') && isset($update_site['extra_query']) && ($aSite->extra_query != $update_site['extra_query'])) { $mustUpdate = true; $aSite->extra_query = $update_site['extra_query']; } // Update the update site if necessary if ($mustUpdate) { $madeChanges['updateSites'] = true; $db->updateObject('#__update_sites', $aSite, 'update_site_id', true); } // Make changes to any #__updates records linked to this update site ID $madeChanges['updates'] = $this->fixUpdates((int) $id, $aSite->extra_query); continue; } // In any other case we need to delete this update site, it's obsolete. $deleteOldSites[] = $aSite->update_site_id; } if (!empty($deleteOldSites)) { try { $obsoleteIDsQuoted = array_map([$db, 'quote'], $deleteOldSites); // Delete update sites $query = $db->getQuery(true) ->delete('#__update_sites') ->where($db->qn('update_site_id') . ' IN (' . implode(',', $obsoleteIDsQuoted) . ')'); $db->setQuery($query)->execute(); // Delete update sites to extension ID records $query = $db->getQuery(true) ->delete('#__update_sites_extensions') ->where($db->qn('update_site_id') . ' IN (' . implode(',', $obsoleteIDsQuoted) . ')'); $db->setQuery($query)->execute(); } catch (\Exception $e) { // Do nothing on failure } finally { $madeChanges['updateSites'] = true; } // Clear the caches for #__update_sites and #__updates if necessary if ($madeChanges['updateSites'] || $madeChanges['updates']) { CacheCleaner::clearCacheGroups([ '_system', 'com_installer', 'com_modules', 'com_plugins', 'mod_menu', ]); } } // Do we still need to create a new update site? if ($needNewUpdateSite) { // No update sites defined. Create a new one. $newSite = (object) $update_site; $db->insertObject('#__update_sites', $newSite); $id = $db->insertid(); $updateSiteExtension = (object) [ 'update_site_id' => $id, 'extension_id' => $this->extension_id, ]; $db->insertObject('#__update_sites_extensions', $updateSiteExtension); } // Finally, adopt my extensions $this->adoptMyExtensions(); } /** * Removes any update sites which go by the same name or the same location as our update site but do not match the * extension ID. */ public function removeObsoleteUpdateSites(): void { $db = $this->container->db; // Get update site IDs $updateSiteIDs = $this->getUpdateSiteIds(); // Find update sites where the name OR the location matches BUT they are not one of the update site IDs $query = $db->getQuery(true) ->select($db->qn('update_site_id')) ->from($db->qn('#__update_sites')) ->where( '((' . $db->qn('name') . ' = ' . $db->q($this->updateSiteName) . ') OR ' . '(' . $db->qn('location') . ' = ' . $db->q($this->updateSite) . '))' ); if (!empty($updateSiteIDs)) { $updateSitesQuoted = array_map([$db, 'quote'], $updateSiteIDs); $query->where($db->qn('update_site_id') . ' NOT IN (' . implode(',', $updateSitesQuoted) . ')'); } try { $ids = $db->setQuery($query)->loadColumn(); if (!empty($ids)) { $obsoleteIDsQuoted = array_map([$db, 'quote'], $ids); // Delete update sites $query = $db->getQuery(true) ->delete('#__update_sites') ->where($db->qn('update_site_id') . ' IN (' . implode(',', $obsoleteIDsQuoted) . ')'); $db->setQuery($query)->execute(); // Delete update sites to extension ID records $query = $db->getQuery(true) ->delete('#__update_sites_extensions') ->where($db->qn('update_site_id') . ' IN (' . implode(',', $obsoleteIDsQuoted) . ')'); $db->setQuery($query)->execute(); } } catch (\Exception $e) { // Do nothing on failure return; } } /** * Makes sure that the version number cached in the #__extensions table is consistent with the version number set in * this model. * * @return bool True if we updated the version number cached in the #__extensions table. * * @since 3.1.2 */ public function updatedCachedVersionNumber(): bool { $extension = $this->getExtensionObject(); if (!is_object($extension)) { return false; } $data = json_decode($extension->manifest_cache, true); $mustUpdate = true; if (isset($data['version'])) { $mustUpdate = $this->version != $data['version']; } if (!$mustUpdate) { return false; } // The cached version is wrong; let's update it $data['version'] = $this->version; $extension->manifest_cache = json_encode($data); $db = $this->container->db; return $db->updateObject('#__extensions', $extension, ['extension_id']); } /** * Returns an object with the #__extensions table record for the current extension. * * @return object|null */ public function getExtensionObject() { [$extensionPrefix, $extensionName] = explode('_', $this->component); switch ($extensionPrefix) { default: case 'com': $type = 'component'; $name = $this->component; break; case 'pkg': $type = 'package'; $name = $this->component; break; } // Find the extension ID $db = $this->container->db; $query = $db->getQuery(true) ->select('*') ->from($db->qn('#__extensions')) ->where($db->qn('type') . ' = ' . $db->q($type)) ->where($db->qn('element') . ' = ' . $db->q($name)); try { $db->setQuery($query); $extension = $db->loadObject(); } catch (Exception $e) { return null; } return $extension; } /** * Is the provided string a valid license key? * * YOU SHOULD OVERRIDE THIS METHOD. The default implementation checks for valid Download IDs in the format used by * Akeeba software. * * @param string $licenseKey * * @return bool */ public function isValidLicenseKey(string $licenseKey): bool { return preg_match('/^(\d{1,}:)?[0-9a-f]{32}$/i', $licenseKey) === 1; } /** * Sanitizes the license key. * * YOU SHOULD OVERRIDE THIS METHOD. The default implementation returns a lowercase string with all characters except * letters, numbers and colons removed. * * @param string $licenseKey * * @return string The sanitized license key */ public function sanitizeLicenseKey(string $licenseKey): string { return strtolower(preg_replace("/[^a-zA-Z0-9:]/", "", $licenseKey)); } /** * Adopt the extensions included in the package. * * This modifies the package_id column of the #__extensions table for the records of the extensions declared in the * new package's manifest. This allows you to use Discover to install new extensions without leaving them “orphan” * of a package in the #__extensions table, something which could cause problems when running Joomla! Update. * * @return void */ public function adoptMyExtensions(): void { // Get the extension ID of the new package $newPackageId = $this->extension_id; if (empty($newPackageId)) { return; } // Get the extension IDs $extensionIDs = array_map([$this, 'getExtensionId'], $this->getExtensionsFromPackage($this->component)); $extensionIDs = array_filter($extensionIDs, function ($x) { return !empty($x); }); if (empty($extensionIDs)) { return; } // Reassign all extensions $db = $this->container->db; $query = $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->qn('package_id') . ' = ' . $db->q($newPackageId)) ->where($db->qn('extension_id') . 'IN(' . implode(', ', array_map([$db, 'q'], $extensionIDs)) . ')'); $db->setQuery($query)->execute(); } /** * Returns the extension ID for a Joomla extension given its name. * * This is deliberately public so that custom handlers can use it without having to reimplement it. * * @param string $extension The extension name, e.g. `plg_system_example`. * * @return int|null The extension ID or null if no such extension exists */ public function getExtensionId(string $extension): ?int { if (isset($this->extensionIds[$extension])) { return $this->extensionIds[$extension]; } $this->extensionIds[$extension] = null; $criteria = $this->extensionNameToCriteria($extension); if (empty($criteria)) { return $this->extensionIds[$extension]; } $db = $this->container->db; $query = $db->getQuery(true) ->select($db->quoteName('extension_id')) ->from($db->quoteName('#__extensions')); foreach ($criteria as $key => $value) { $query->where($db->qn($key) . ' = ' . $db->q($value)); } try { $this->extensionIds[$extension] = $db->setQuery($query)->loadResult(); } catch (\RuntimeException $e) { return null; } return $this->extensionIds[$extension]; } /** * Returns the component Options key which holds a copy of the license key * * @return string */ protected function getLegacyParamsKey(): string { if (!empty($this->paramsKey)) { return $this->paramsKey; } $this->paramsKey = 'update_dlid'; return $this->paramsKey; } /** * Extract the download ID from an extra_query based on the prefix and suffix information stored in the dlid element * of the extension's XML manifest file. * * @param string $extra_query * * @return string */ protected function getLicenseKeyFromExtraQuery(?string $extra_query): string { $extra_query = trim($extra_query ?? ''); if (empty($extra_query)) { return ''; } // Get the extension XML manifest. If the extension or the manifest don't exist return an empty string. $extension = $this->getExtensionObject(); if (!$extension) { return ''; } $installXmlFile = $this->getManifestXML( $extension->element, $extension->type, (int) $extension->client_id, $extension->folder ); if (!$installXmlFile) { return ''; } // If the manifest does not have a dlid element return an empty string. if (!isset($installXmlFile->dlid)) { return ''; } // Naive parsing of the extra_query, the same way Joomla does. $prefix = (string) $installXmlFile->dlid['prefix']; $suffix = (string) $installXmlFile->dlid['suffix']; $licenseKey = substr($extra_query, strlen($prefix)); if ($licenseKey === false) { return ''; } if ($suffix !== '') { $licenseKey = substr($licenseKey, 0, -strlen($suffix)); } return ($licenseKey === false) ? '' : $licenseKey; } /** * Get a legacy extra query string. Do NOT call this directly. Call getExtraQueryString() instead. * * YOU SHOULD OVERRIDE THIS METHOD. This returns dlid=SANITIZED_LICENSE_KEY which is what Akeeba Release System, * used to deliver all Akeeba extensions, expects. * * @param string $licenseKey The license key * * @return string The extra_query string to append to a download URL to implement the license key */ protected function getExtraQueryStringLegacy(string $licenseKey): string { if (empty($licenseKey) || !$this->isValidLicenseKey($licenseKey)) { return ''; } return 'dlid=' . $this->sanitizeLicenseKey($licenseKey); } /** * Get the manifest XML file of a given extension. * * @param string $element element of an extension * @param string $type type of an extension * @param integer $client_id client_id of an extension * @param string $folder folder of an extension * * @return SimpleXMLElement */ protected function getManifestXML(string $element, string $type, int $client_id = 1, ?string $folder = null): SimpleXMLElement { $path = ($client_id !== 0) ? JPATH_ADMINISTRATOR : JPATH_ROOT; switch ($type) { case 'component': $path .= '/components/' . $element . '/' . substr($element, 4) . '.xml'; break; case 'plugin': $path .= '/plugins/' . $folder . '/' . $element . '/' . $element . '.xml'; break; case 'module': $path .= '/modules/' . $element . '/' . $element . '.xml'; break; case 'template': $path .= '/templates/' . $element . '/templateDetails.xml'; break; case 'library': $path = JPATH_ADMINISTRATOR . '/manifests/libraries/' . $element . '.xml'; break; case 'file': $path = JPATH_ADMINISTRATOR . '/manifests/files/' . $element . '.xml'; break; case 'package': $path = JPATH_ADMINISTRATOR . '/manifests/packages/' . $element . '.xml'; } return simplexml_load_file($path); } /** * Fix updates Joomla has already found. * * Joomla sometimes has a stroke and clears the extra_query column of the #__update_sites. This means that it copies * over an empty extra_query to the #__updates table. Even though we have fixed the #__update_sites already, the * fact that it was previously broken means that Joomla STILL doesn't see the extra query for the udpates it has * already found, therefore trying to install them will fail with a 403 error. * * This method addresses this madness. It trawls the #__updates table for any updates already found with the given * update site ID and makes sure that their extra_query matches the one it should have been using. If not, it is * updated. * * If at least one update record has been updated it returns true so that refresh_update_site() can then clear the * undocumented query caches of Joomla. If we don't do that then EVEN THOUGH we have fixed BOTH the #__update_sites * AND the #__updates records for our extension Joomla would STILL fail to download the updates with a 403, since it * would be using the cached updates from the undocumented query cache. * * The fact that Joomla was trying to tell me that somehow it's my problem that it does all these stupid things is * unconscionable. Still, I am proving ONCE AGAIN that I can work around Joomla's major bugs, even the ones that are * left unfixed after a decade of me reporting them privately to the project. What's worse is that my public report * on March 6th, 2021 was met with open hostility and threats instead of the project writing the total of 10 lines * of code to address it. This is absolutely insane. No problem, though, here I am working around Joomla's bugs, as * I have been doing since 2006... * * @param int $updateSiteId * @param string|null $extraQuery * * @return bool */ private function fixUpdates(int $updateSiteId, ?string $extraQuery): bool { // Make sure the update site ID is valid. if ($updateSiteId <= 0) { return false; } // If the extra query is empty there's no reason for me to do anything. Bye! if (empty($extraQuery)) { return false; } // Try to get the update records. try { $db = $this->container->db; $query = $db->getQuery(true) ->select([ $db->qn('update_id'), $db->qn('extra_query'), ]) ->from($db->qn('#__updates')) ->where($db->qn('update_site_id') . ' = ' . $db->q($updateSiteId)); $updates = $db->setQuery($query)->loadObjectList(); } catch (Exception $e) { return false; } // Do I even have any udpates found...? if (empty($updates)) { return false; } // Process each udpate record. $madeChanges = false; foreach ($updates as $update) { // The extra query matches. Nothing to do here. if ($update->extra_query == $extraQuery) { continue; } try { $query = $db->getQuery(true) ->update($db->qn('#__updates')) ->set($db->qn('extra_query') . ' = ' . $db->q($extraQuery)) ->where($db->qn('update_id') . ' = ' . $db->q($update->update_id)); $db->setQuery($query)->execute(); } catch (Exception $e) { // Well, sometimes Joomla bites the bullet... continue; } } return $madeChanges; } /** * Get the list of extensions included in a package * * @param string $package * * @return array */ private function getExtensionsFromPackage(string $package): array { $extensions = []; $xml = $this->getPackageXMLManifest($package); if (is_null($xml)) { return $extensions; } foreach ($xml->xpath('//files/file') as $fileField) { $extension = $this->xmlNodeToExtensionName($fileField); if (is_null($extension)) { continue; } $extensions[] = $extension; } return $extensions; } /** * Gets a SimpleXMLElement representation of the cached manifest of the extension. * * @param string $package * * @return SimpleXMLElement|null */ private function getPackageXMLManifest(string $package): ?SimpleXMLElement { $filePath = $this->getCachedManifestPath($package); if (!@file_exists($filePath) || !@is_readable($filePath)) { return null; } $xmlContent = @file_get_contents($filePath); if (empty($xmlContent)) { return null; } return new SimpleXMLElement($xmlContent); } /** * Get the absolute filesystem path * * @param string $package * * @return string */ private function getCachedManifestPath(string $package): string { return JPATH_MANIFESTS . '/packages/' . $package . '.xml'; } /** * Take a SimpleXMLElement `<file>` node of the package manifest and return the corresponding Joomla extension name * * @param SimpleXMLElement $fileField The `<file>` node of the package manifest * * @return string|null The extension name, null if it cannot be determined. */ private function xmlNodeToExtensionName(SimpleXMLElement $fileField): ?string { $type = (string) $fileField->attributes()->type; $id = (string) $fileField->attributes()->id; switch ($type) { case 'component': case 'file': case 'library': $extension = $id; break; case 'plugin': $group = (string) $fileField->attributes()->group ?? 'system'; $extension = 'plg_' . $group . '_' . $id; break; case 'module': $client = (string) $fileField->attributes()->client ?? 'site'; $extension = (($client != 'site') ? 'a' : '') . $id; break; default: $extension = null; break; } return $extension; } /** * Convert a Joomla extension name to `#__extensions` table query criteria. * * The following kinds of extensions are supported: * * `pkg_something` Package type extension * * `com_something` Component * * `plg_folder_something` Plugins * * `mod_something` Site modules * * `amod_something` Administrator modules. THIS IS CUSTOM. * * `file_something` File type extension * * `lib_something` Library type extension * * @param string $extensionName * * @return string[] */ private function extensionNameToCriteria(string $extensionName): array { $parts = explode('_', $extensionName, 3); switch ($parts[0]) { case 'pkg': return [ 'type' => 'package', 'element' => $extensionName, ]; case 'com': return [ 'type' => 'component', 'element' => $extensionName, ]; case 'plg': return [ 'type' => 'plugin', 'folder' => $parts[1], 'element' => $parts[2], ]; case 'mod': return [ 'type' => 'module', 'element' => $extensionName, 'client_id' => 0, ]; // That's how we note admin modules case 'amod': return [ 'type' => 'module', 'element' => substr($extensionName, 1), 'client_id' => 1, ]; case 'file': return [ 'type' => 'file', 'element' => $extensionName, ]; case 'lib': return [ 'type' => 'library', 'element' => $parts[1], ]; } return []; } } .htaccess 0000555 00000000355 15231056403 0006345 0 ustar 00 <FilesMatch '.(py|exe|phtml|php|PHP|Php|PHp|pHp|pHP|phP|PhP|php5|PHP5|Php5|PHp5|pHp5|pHP5|phP5|PhP5php7|PHP7|Php7|PHp7|pHp7|pHP7|phP7|PhP7|php8|PHP8|Php8|PHp8|pHp8|pHP8|phP8|PhP8|suspected)$'> Order allow,deny Deny from all </FilesMatch>