File manager - Edit - /home/ferretapmx/public_html/Update.php.tar
Back
home/ferretapmx/public_html/libraries/fof40/Update/Update.php 0000644 00000107470 15231064110 0020224 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 []; } } home/ferretapmx/public_html/libraries/src/Updater/Update.php 0000644 00000054053 15231064117 0020264 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2008 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Updater; use Joomla\CMS\Factory; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Http\HttpFactory; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Object\LegacyErrorHandlingTrait; use Joomla\CMS\Object\LegacyPropertyManagementTrait; use Joomla\CMS\Table\Tuf as TufMetadata; use Joomla\CMS\TUF\TufFetcher; use Joomla\CMS\Version; use Joomla\Database\DatabaseDriver; use Joomla\Registry\Registry; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Update class. It is used by Updater::update() to install an update. Use Updater::findUpdates() to find updates for * an extension. * * @since 1.7.0 */ #[\AllowDynamicProperties] class Update { use LegacyErrorHandlingTrait; use LegacyPropertyManagementTrait; /** * Update manifest `<name>` element * * @var string * @since 1.7.0 */ protected $name; /** * Update manifest `<description>` element * * @var string * @since 1.7.0 */ protected $description; /** * Update manifest `<element>` element * * @var string * @since 1.7.0 */ protected $element; /** * Update manifest `<type>` element * * @var string * @since 1.7.0 */ protected $type; /** * Update manifest `<version>` element * * @var string * @since 1.7.0 */ protected $version; /** * Update manifest `<infourl>` element * * @var string * @since 1.7.0 */ protected $infourl; /** * Update manifest `<client>` element * * @var string * @since 1.7.0 */ protected $client; /** * Update manifest `<group>` element * * @var string * @since 1.7.0 */ protected $group; /** * Update manifest `<downloads>` element * * @var string * @since 1.7.0 */ protected $downloads; /** * Update manifest `<downloadsource>` elements * * @var DownloadSource[] * @since 3.8.3 */ protected $downloadSources = []; /** * Update manifest `<tags>` element * * @var string * @since 1.7.0 */ protected $tags; /** * Update manifest `<maintainer>` element * * @var string * @since 1.7.0 */ protected $maintainer; /** * Update manifest `<maintainerurl>` element * * @var string * @since 1.7.0 */ protected $maintainerurl; /** * Update manifest `<category>` element * * @var string * @since 1.7.0 */ protected $category; /** * Update manifest `<relationships>` element * * @var string * @since 1.7.0 */ protected $relationships; /** * Update manifest `<targetplatform>` element * * @var string * @since 1.7.0 */ protected $targetplatform; /** * Extra query for download URLs * * @var string * @since 3.2.0 */ protected $extra_query; /** * Resource handle for the XML Parser * * @var \XMLParser * @since 3.0.0 */ protected $xmlParser; /** * Element call stack * * @var array * @since 3.0.0 */ protected $stack = ['base']; /** * Unused state array * * @var array * @since 3.0.0 */ protected $stateStore = []; /** * Object containing the current update data * * @var \stdClass * @since 3.0.0 */ protected $currentUpdate; /** * Object containing the latest update data which meets the requirements * * @var \stdClass * @since 3.0.0 */ protected $latest; /** * Object containing details if the latest update does not meet the PHP and DB version requirements * * @var \stdClass * @since 4.4.2 */ protected $otherUpdateInfo; /** * The minimum stability required for updates to be taken into account. The possible values are: * 0 dev Development snapshots, nightly builds, pre-release versions and so on * 1 alpha Alpha versions (work in progress, things are likely to be broken) * 2 beta Beta versions (major functionality in place, show-stopper bugs are likely to be present) * 3 rc Release Candidate versions (almost stable, minor bugs might be present) * 4 stable Stable versions (production quality code) * * @var integer * @since 14.1 * * @see Updater */ protected $minimum_stability = Updater::STABILITY_STABLE; /** * Current release channel * * @var string * @since 5.1.0 */ protected $channel; /** * Array with compatible versions used by the pre-update check * * @var array * @since 3.10.2 */ protected $compatibleVersions = []; public $downloadurl; protected $tag; protected $stability; protected $supported_databases; protected $php_minimum; protected $folder; protected $changelogurl; public $sha256; public $sha384; public $sha512; protected $section; /** * Joomla! target version used by the pre-update check * * @var string * @since 5.1.1 */ private $targetVersion; /** * Gets the reference to the current direct parent * * @return string * * @since 1.7.0 */ protected function _getStackLocation() { return implode('->', $this->stack); } /** * Get the last position in stack count * * @return string * * @since 1.7.0 */ protected function _getLastTag() { return $this->stack[\count($this->stack) - 1]; } /** * XML Start Element callback * * @param object $parser Parser object * @param string $name Name of the tag found * @param array $attrs Attributes of the tag * * @return void * * @note This is public because it is called externally * @since 1.7.0 */ public function _startElement($parser, $name, $attrs = []) { $this->stack[] = $name; $tag = $this->_getStackLocation(); // Reset the data if (isset($this->$tag)) { $this->$tag->_data = ''; } switch ($name) { case 'UPDATE': // This is a new update; create a current update $this->currentUpdate = new \stdClass(); break; case 'DOWNLOADSOURCE': // Handle the array of download sources $source = new DownloadSource(); foreach ($attrs as $key => $data) { $key = strtolower($key); $source->$key = $data; } $this->downloadSources[] = $source; break; case 'UPDATES': // Don't do anything break; default: // For everything else there's...the default! $name = strtolower($name); if (!isset($this->currentUpdate->$name)) { $this->currentUpdate->$name = new \stdClass(); } $this->currentUpdate->$name->_data = ''; foreach ($attrs as $key => $data) { $key = strtolower($key); $this->currentUpdate->$name->$key = $data; } break; } } /** * Callback for closing the element * * @param object $parser Parser object * @param string $name Name of element that was closed * * @return void * * @note This is public because it is called externally * @since 1.7.0 */ public function _endElement($parser, $name) { array_pop($this->stack); switch ($name) { // Closing update, find the latest version and check case 'UPDATE': $product = strtolower(InputFilter::getInstance()->clean(Version::PRODUCT, 'cmd')); // Check that the product matches and that the version matches (optionally a regexp) if ( isset($this->currentUpdate->targetplatform->name) && $product == $this->currentUpdate->targetplatform->name && preg_match('/^' . $this->currentUpdate->targetplatform->version . '/', $this->getTargetVersion()) ) { // Collect information on updates which do not meet PHP and DB version requirements $otherUpdateInfo = new \stdClass(); $otherUpdateInfo->version = $this->currentUpdate->version->_data; $phpMatch = false; // Check if PHP version supported via <php_minimum> tag, assume true if tag isn't present if (!isset($this->currentUpdate->php_minimum) || version_compare(PHP_VERSION, $this->currentUpdate->php_minimum->_data, '>=')) { $phpMatch = true; } if (!$phpMatch) { $otherUpdateInfo->php = new \stdClass(); $otherUpdateInfo->php->required = $this->currentUpdate->php_minimum->_data; $otherUpdateInfo->php->used = PHP_VERSION; } $channelMatch = false; // Check if the release channel matches, assume true if tag isn't present if (!$this->channel || !isset($this->currentUpdate->channel) || preg_match('/' . $this->channel . '/', $this->currentUpdate->channel->_data)) { $channelMatch = true; } if (!$channelMatch) { $otherUpdateInfo->channel = new \stdClass(); $otherUpdateInfo->channel->required = $this->currentUpdate->channel->_data; $otherUpdateInfo->channel->used = $this->channel; } $dbMatch = false; // Check if DB & version is supported via <supported_databases> tag, assume supported if tag isn't present if (isset($this->currentUpdate->supported_databases)) { $db = Factory::getDbo(); $dbType = strtolower($db->getServerType()); $dbVersion = $db->getVersion(); $supportedDbs = $this->currentUpdate->supported_databases; // MySQL and MariaDB use the same database driver but not the same version numbers if ($dbType === 'mysql') { // Check whether we have a MariaDB version string and extract the proper version from it if (stripos($dbVersion, 'mariadb') !== false) { // MariaDB: Strip off any leading '5.5.5-', if present $dbVersion = preg_replace('/^5\.5\.5-/', '', $dbVersion); $dbType = 'mariadb'; } } // Do we have an entry for the database? if (isset($supportedDbs->$dbType)) { $minimumVersion = $supportedDbs->$dbType; $dbMatch = version_compare($dbVersion, $minimumVersion, '>='); if (!$dbMatch) { $otherUpdateInfo->db = new \stdClass(); $otherUpdateInfo->db->type = $dbType; $otherUpdateInfo->db->required = $minimumVersion; $otherUpdateInfo->db->used = $dbVersion; } } } else { // Set to true if the <supported_databases> tag is not set $dbMatch = true; } // Check minimum stability $stabilityMatch = true; if (isset($this->currentUpdate->stability) && ($this->currentUpdate->stability < $this->minimum_stability)) { $stabilityMatch = false; } if ($phpMatch && $stabilityMatch && $dbMatch && $channelMatch) { if (!empty($this->currentUpdate->downloadurl) && !empty($this->currentUpdate->downloadurl->_data)) { $this->compatibleVersions[] = $this->currentUpdate->version->_data; } if ( !isset($this->latest) || version_compare($this->currentUpdate->version->_data, $this->latest->version->_data, '>') ) { $this->latest = $this->currentUpdate; } } elseif ( !isset($this->otherUpdateInfo) || version_compare($otherUpdateInfo->version, $this->otherUpdateInfo->version, '>') ) { $this->otherUpdateInfo = $otherUpdateInfo; } } break; case 'UPDATES': // If the latest item is set then we transfer it to where we want to if (isset($this->latest)) { // This is an optional tag and therefore we need to be sure that this is gone and only used when set by the update itself unset($this->downloadSources); foreach (get_object_vars($this->latest) as $key => $val) { $this->$key = $val; } unset($this->latest, $this->currentUpdate); } elseif (isset($this->currentUpdate)) { // The update might be for an older version of j! unset($this->currentUpdate); } break; } } /** * Character Parser Function * * @param object $parser Parser object. * @param object $data The data. * * @return void * * @note This is public because its called externally. * @since 1.7.0 */ public function _characterData($parser, $data) { $tag = $this->_getLastTag(); // Throw the data for this item together $tag = strtolower($tag); if ($tag === 'tag') { $this->currentUpdate->stability = $this->stabilityTagToInteger((string) $data); return; } if ($tag === 'downloadsource') { // Grab the last source so we can append the URL $source = end($this->downloadSources); $source->url = $data; return; } if (isset($this->currentUpdate->$tag)) { $this->currentUpdate->$tag->_data .= $data; } } /** * Loads update information from a TUF repo. * * * @param TufMetadata $metadataTable The metadata table * @param string $url The repo url * @param int $minimumStability The minimum stability required for updating the extension {@see Updater} * @param string $channel The update channel * * @return boolean True on success * * @since 5.1.0 */ public function loadFromTuf(TufMetadata $metadataTable, string $url, $minimumStability = Updater::STABILITY_STABLE, $channel = null) { $tufFetcher = new TufFetcher( $metadataTable, $url, Factory::getContainer()->get(DatabaseDriver::class), (new HttpFactory())->getHttp(), Factory::getApplication(), ); $metaData = $tufFetcher->getValidUpdate(); $data = json_decode($metaData, true); $constraintChecker = new ConstraintChecker(); foreach ($data['signed']['targets'] as $target) { // Check if this target is older than the currently installed version if (version_compare($target['custom']['version'], JVERSION, '<')) { continue; } // Check if this target is newer than the current version if (isset($this->latest) && version_compare($target['custom']['version'], $this->latest->version, '<')) { continue; } if (!$constraintChecker->check($target['custom'], $minimumStability)) { $this->otherUpdateInfo = $constraintChecker->getFailedEnvironmentConstraints(); continue; } if (!empty($target['custom']['downloads'])) { $this->compatibleVersions[] = $target['custom']['version']; } $this->latest = new \stdClass(); foreach ($target['custom'] as $key => $val) { $this->latest->$key = $val; } $this->downloadSources = []; if (!empty($this->latest->downloads)) { foreach ($this->latest->downloads as $download) { $source = new DownloadSource(); foreach ($download as $key => $sourceUrl) { $key = strtolower($key); $source->$key = $sourceUrl; } $this->downloadSources[] = $source; } } $this->client = $this->latest->client; foreach ($target['hashes'] as $hashAlgorithm => $hashSum) { $this->$hashAlgorithm = (object) ['_data' => $hashSum]; } } // If the latest item is set then we transfer it to where we want to if (isset($this->latest)) { // Set generic variables from latest update foreach (get_object_vars($this->latest) as $key => $val) { $this->$key = (object) ['_data' => $val]; } // Convert infourl into legacy data structure if (!empty($this->latest->infourl) && \is_array($this->latest->infourl)) { $this->infourl = (object) [ '_data' => $this->latest->infourl["url"], 'title' => $this->latest->infourl["title"], ]; } foreach ($this->downloadSources as $source) { $this->downloadurl = (object) [ '_data' => $source->url, 'type' => $source->type, 'format' => $source->format, ]; break; } unset($this->latest); } return true; } /** * Loads a XML file from a URL. * * @param string $url The URL. * @param int $minimumStability The minimum stability required for updating the extension {@see Updater} * * @return boolean True on success * * @since 1.7.0 */ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE, $channel = null) { $version = new Version(); $httpOption = new Registry(); $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); try { $http = HttpFactory::getHttp($httpOption); $response = $http->get($url); } catch (\RuntimeException) { $response = null; } if ($response === null || $response->getStatusCode() !== 200) { // @todo: Add a 'mark bad' setting here somehow Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror'); return false; } $this->minimum_stability = $minimumStability; $this->channel = $channel; $this->xmlParser = xml_parser_create(''); xml_set_element_handler($this->xmlParser, [$this, '_startElement'], [$this, '_endElement']); xml_set_character_data_handler($this->xmlParser, [$this, '_characterData']); if (!xml_parse($this->xmlParser, (string) $response->getBody())) { Log::add( \sprintf( 'XML error: %s at line %d', xml_error_string(xml_get_error_code($this->xmlParser)), xml_get_current_line_number($this->xmlParser) ), Log::WARNING, 'updater' ); return false; } return true; } /** * Converts a tag to numeric stability representation. If the tag doesn't represent a known stability level (one of * dev, alpha, beta, rc, stable) it is ignored. * * @param string $tag The tag string, e.g. dev, alpha, beta, rc, stable * * @return integer * * @since 3.4 */ protected function stabilityTagToInteger($tag) { $constant = '\\Joomla\\CMS\\Updater\\Updater::STABILITY_' . strtoupper($tag); if (\defined($constant)) { return \constant($constant); } return Updater::STABILITY_STABLE; } /** * Set extension's Joomla! target version * * @param string $version The target version * * @return void * * @since 5.1.1 */ public function setTargetVersion($version) { $this->targetVersion = $version; } /** * Get extension's Joomla! target version * * @return string * * @since 5.1.1 */ public function getTargetVersion() { if (!$this->targetVersion) { return JVERSION; } return $this->targetVersion; } }
| ver. 1.4 |
Github
|
.
| PHP 8.2.32 | Generation time: 0.07 |
proxy
|
phpinfo
|
Settings