-
Notifications
You must be signed in to change notification settings - Fork 26
Download IDs
Some software (categories) or download items may have access restrictions. For example, you may have a free of charge and a paid edition of your software for each published version. You want the former to be freely downloadable for everyone but the latter only available to paying customers.
ARS already allows you to restrict access to categories, releases or individual items based on Joomla access levels. The problem is that when an automatic update takes place, e.g. through Joomla's Extensions Update page, the download client is NOT logged into your site. Making updates available without authentication is not a realistic possibility because everyone could download your software. So what do you do?
We have contributed a feature to Joomla which has been included in it since Joomla 3.2. It allows the developer to add a set of query string parameters in the extra_query
column of the #__update_sites
page. In Joomla 4.0 this has been further refined into a user interface called Download Keys.
ARS supports this kind of Download Keys since 2010 — long before Joomla added support for them. We call them Download IDs. We were one of the first companies — if not the first company — in the Joomla extensions developer ecosystem to offer integrated updates, including updates for the paid editions of our extensions. It's no wonder that Joomla's support for commercial extensions' updates is modeled after our implementation and Akeeba Release System.
In Akeeba Release System each Joomla user is assigned a Main Download ID which is generated automatically for them. On top of that, they can create one or more Add-on Download IDs e.g. to have a different key per domain name they manage. These can be centrally managed in the backend through the Download IDs page. Moreover, the frontend Download IDs page allows the user to self-manage their own keys.
As for the extra_query
it should be in the form dlid=DOWNLOAD_KEY
where DOWNLOAD_KEY is the user-provided Download ID. ARS sees the dlid
query string parameter in the download URL and looks for the DOWNLOAD_KEY in your site's database. If it's found it temporarily logs in the corresponding user and goes through the download process, therefore taking into account the correct Joomla! user groups and access levels for this user. At the end of the process the temporary login is reversed, i.e. the user is logged out, to prevent abusing the temporary session created on your site.
Each installed extension in Joomla has a corresponding #__extensions
record. An #__extensions
record can optionally have an #__update_sites_extensions
record which links it to an #__update_sites
record. The latter is created or update by Joomla automatically every time you install or update an extension.
Please note that you should only include an update server in the XML manifest of the extension you are distributing. That is to say, if you distribute a component and its associated modules and plugins as a “package” type extension (pkg_something
) you need to set up the update server in the package's XML manifest, NOT the component's manifest.
The #__update_sites
record has an extra_query
column. The contents of this column are appended to the download URL for the update package file when Joomla is trying to install an update through its Updates page. For software distributed with Akeeba Release System the extra_query
column must have the format dlid=DOWNLOAD_ID
where DOWNLOAD_ID
is the user's Download ID.
Integrating ARS-compatible, authenticated downloads with Joomla's extensions update is a matter of populating the extra_query
column of your extension's update site. How to do that depends on the Joomla version you are using.
Joomla 3 does not have an interface for managing the download keys / download IDs. This means that you will need to have your user enter the Download ID in a user interface element and then run custom code to find the #__update_sites
record for your extension and update its extra_query
column. Typically, you have a component, module or plugin option for the Download ID and some common code running when an administrative user accesses your extension to check and, if necessary, update the extra_query
column. You can find sample code for that in FOF 4's Update model. Yes, it's very complicated.
Moreover, this approach has many caveats, least of which is the fact that Joomla may reset the extra_query
column when rebuilding the update sites (versions older than 3.9.26) or when the user updates your extension.
Another approach to this problem is ignoring the extra_query
column, instead writing a plugin in the installer
folder. In this case the plugin is responsible for altering the download URL of the update to include the Download ID. The only drawback is that updates will fail unless this plugin is enabled. A sample plugin for a package called pkg_example
, distributed as a file called pkg_example-VERSION-pro.zip
, which includes a component named com_example
with its Download ID saved to a configuration parameter called dlid
can be found below.
defined('_JEXEC') || die;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
use Joomla\Registry\Registry;
/**
* Component installer helper
*
* Adds the Download ID query string parameter to the download URL if for any reason your Joomla Update Sites had the
* extra_query column wiped (e.g. by rebuilding the update sites list).
*/
class plgInstallerExample extends CMSPlugin
{
/**
* The filename in the download URL must match this pattern for this plugin to add the Download ID.
*
* @var string
*/
private $urlFilePattern = 'pkg_example-*pro.zip';
/**
* List of URL prefixes we are allowed to work with
*
* @var array
*/
private $urlPrefixes = [
'https://www.example.com',
];
/**
* The name of the component whose options include the Download ID
*
* @var string
*/
private $componentName = 'com_example';
/**
* The name of the option in your component's `config.xml` file which contains the Download ID
*
* @var string
*/
private $downloadIDOptionKey = 'dlid';
/**
* Handles Joomla's event fired before downloading an update package.
*
* @param string $url The URL of the package Joomla is trying to install
* @param array $headers The HTTP headers used for downloading the package
*
* @return void
*/
public function onInstallerBeforePackageDownload(&$url, &$headers)
{
// This plugin only applies to Joomla! 3
if (version_compare(JVERSION, '3.999.999', 'gt'))
{
return;
}
// Make sure the URL belongs to one of our domain names
if (!$this->hasAllowedPrefix($url))
{
return;
}
// Make sure the URL seems to be for a package are meant to handle
$uri = Uri::getInstance($url);
$path = $uri->getPath();
$baseName = basename($path);
if (!fnmatch($this->urlFilePattern, $baseName))
{
return;
}
// Make sure this URL does not already have a download ID
if ($this->hasDownloadID($uri))
{
return;
}
// Get the Download ID and make sure it's not empty
try
{
$dlid = $this->getDownloadID();
}
catch (Exception $e)
{
$dlid = '';
}
if (empty($dlid))
{
return;
}
// Apply the download ID to the download URL
$uri->setVar('dlid', $dlid);
$url = $uri->toString();
}
/**
* Checks if the download URL is one we're supposed to handle. We have a whitelist of allowed URL prefixes set up
* at the top of this plugin.
*
* @param string $url The download URL to check
*
* @return bool
*/
private function hasAllowedPrefix($url)
{
$hasAllowedPrefix = false;
foreach ($this->urlPrefixes as $prefix)
{
$hasAllowedPrefix = $hasAllowedPrefix || (strpos($url, $prefix) === 0);
}
return $hasAllowedPrefix;
}
/**
* Does the download URL already have a non-empty Download ID query parameter?
*
* @param Uri $uri The download URL to check
*
* @return bool
*/
private function hasDownloadID($uri)
{
$dlid = $uri->getVar('dlid', null);
return !empty($dlid);
}
/**
* Get the applicable Download ID for the extension.
*
* @return string
*/
private function getDownloadID()
{
// Make sure the component is installed and enabled
if (!ComponentHelper::isInstalled($this->componentName) || !ComponentHelper::isEnabled($this->componentName))
{
return null;
}
// Get the component's parameters
$params = ComponentHelper::getParams($this->componentName);
// Make sure the parameters object is valid
if (is_null($params) || !is_object($params) || !($params instanceof Registry))
{
return '';
}
$value = $params->get($this->downloadIDOptionKey, null);
if (empty($value))
{
return '';
}
return empty($value) ? '' : $value;
}
}
We strongly recommend that you use BOTH methods simultaneously on Joomla 3. The extra_query
column in #__update_sites
should be your primary method and the installer
plugin your failsafe.
Joomla 4 is MUCH better in supporting commercial extensions. The extra_query
column in the #__update_sites
can be managed through the System, Update Sites page. Joomla 4 will even warn the users if a Download Key is missing, i.e. if they have an extension which says that it needs a Download Key but the extra_query
column contents are empty or invalid.
So, for Joomla 4 you only need to add the following line to the XML manifest of the extension which also defines the update server (as noted above):
<dlid prefix="dlid=" suffix=""/>
That's all there is to it. No complicated custom code, no need for installer
plugins. It's all managed by Joomla just by adding this ridiculously short line into your XML manifest.
Copyright (C) 2010-2017 Akeeba Ltd.
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section "GNU Free Documentation License".
Introducing Akeeba Release System
- Understanding the structure of a repository
- Category management
- Release management
- Item management
- Environment management
- Download IDs
- Viewing the logs
- Component options