944 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			944 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * @package   FOF
 | |
|  * @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
 | |
|  * @license   GNU General Public License version 2, or later
 | |
|  */
 | |
| 
 | |
| namespace FOF30\Update;
 | |
| 
 | |
| defined('_JEXEC') || die;
 | |
| 
 | |
| use Exception;
 | |
| use FOF30\Container\Container;
 | |
| use FOF30\Model\Model;
 | |
| use Joomla\CMS\Component\ComponentHelper;
 | |
| use Joomla\CMS\Updater\Updater;
 | |
| use SimpleXMLElement;
 | |
| 
 | |
| /**
 | |
|  * A helper Model to interact with Joomla!'s extensions update feature
 | |
|  */
 | |
| class Update extends Model
 | |
| {
 | |
| 	/** @var Updater The Joomla! updater object */
 | |
| 	protected $updater = null;
 | |
| 
 | |
| 	/** @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 = null;
 | |
| 
 | |
| 	/** @var string The name to the component's update site (description of the update XML stream) */
 | |
| 	protected $updateSiteName = null;
 | |
| 
 | |
| 	/** @var string The extra query to append to (commercial) components' download URLs */
 | |
| 	protected $extraQuery = null;
 | |
| 
 | |
| 	/** @var string The component Options key which stores a copy of the Download ID */
 | |
| 	protected $paramsKey = 'update_dlid';
 | |
| 
 | |
| 	/**
 | |
| 	 * 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  $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 = Updater::getInstance();
 | |
| 
 | |
| 		// Get the component name
 | |
| 		if (isset($config['update_component']))
 | |
| 		{
 | |
| 			$this->component = $config['update_component'];
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			$this->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($forceLegacy = false)
 | |
| 	{
 | |
| 		$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
 | |
| 	 */
 | |
| 	public function getUpdateSites()
 | |
| 	{
 | |
| 		$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($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()
 | |
| 	{
 | |
| 		// 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(false);
 | |
| 
 | |
| 		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()
 | |
| 	{
 | |
| 		$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(false);
 | |
| 
 | |
| 		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($licenseKey)
 | |
| 	{
 | |
| 		// 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($force = false)
 | |
| 	{
 | |
| 		$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 = ComponentHelper::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()
 | |
| 	{
 | |
| 		$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()
 | |
| 	{
 | |
| 		return $this->version;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Override the currently installed version as reported by the #__extensions table
 | |
| 	 *
 | |
| 	 * @param   string  $version
 | |
| 	 */
 | |
| 	public function setVersion($version)
 | |
| 	{
 | |
| 		$this->version = $version;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Refreshes the Joomla! update sites for this extension as needed
 | |
| 	 *
 | |
| 	 * @return  void
 | |
| 	 */
 | |
| 	public function refreshUpdateSite()
 | |
| 	{
 | |
| 		if (empty($this->extension_id))
 | |
| 		{
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Create the update site definition we want to store to the database
 | |
| 		$update_site = [
 | |
| 			'name'                 => $this->updateSiteName,
 | |
| 			'type'                 => 'extension',
 | |
| 			'location'             => $this->updateSite,
 | |
| 			'enabled'              => 1,
 | |
| 			'last_check_timestamp' => 0,
 | |
| 			'extra_query'          => $this->extraQuery,
 | |
| 		];
 | |
| 
 | |
| 		// Get a reference to the db driver
 | |
| 		$db = $this->container->db;
 | |
| 
 | |
| 		// Get the #__update_sites columns
 | |
| 		$columns = $db->getTableColumns('#__update_sites', true);
 | |
| 
 | |
| 		if (version_compare(JVERSION, '3.0.0', 'lt') || !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 is now up-to-date, don't need to refresh it anymore.
 | |
| 				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)
 | |
| 				{
 | |
| 					$db->updateObject('#__update_sites', $aSite, 'update_site_id', true);
 | |
| 				}
 | |
| 
 | |
| 				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
 | |
| 				return;
 | |
| 			}
 | |
| 
 | |
| 		}
 | |
| 
 | |
| 		// 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);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * 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()
 | |
| 	{
 | |
| 		$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()
 | |
| 	{
 | |
| 		$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  mixed
 | |
| 	 */
 | |
| 	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($licenseKey)
 | |
| 	{
 | |
| 		return preg_match('/^([0-9]{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($licenseKey)
 | |
| 	{
 | |
| 		return strtolower(preg_replace("/[^a-zA-Z0-9:]/", "", $licenseKey));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns the component Options key which holds a copy of the license key
 | |
| 	 *
 | |
| 	 * @return  string
 | |
| 	 */
 | |
| 	protected function getLegacyParamsKey()
 | |
| 	{
 | |
| 		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($extra_query)
 | |
| 	{
 | |
| 		$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 downlaod URL to implement the license key
 | |
| 	 */
 | |
| 	protected function getExtraQueryStringLegacy($licenseKey)
 | |
| 	{
 | |
| 		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($element, $type, $client_id = 1, $folder = null)
 | |
| 	{
 | |
| 		$path = $client_id ? 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);
 | |
| 	}
 | |
| }
 |