Tutorial: Automatic updates for paid VirtueMart and WordPress extensions
Introduction
Both big CMS systems, WordPress and Joomla, have an automated plugin update mechanism, which informs site admins about new versions of the installed extensions and lets the admin install those new versions. Also, in both systems, this extension update mechanism works very well for freely available extensions (WP relies on releases on wordpress.org/plugins, while Joomla uses XML files with update information that can be placed anywhere on the internet). However, paid extensions are not very well supported by either CMS out of the box.
We at Open Tools develop and distribute both free and paid extensions, so we had to set up a proper update system also for paid extensions for WordPress and for Joomla. In this article, we will show how automatic updates can easily be added to paid extensions that need some kind of log in or access key or download password.
Parts of this Tutorial
The setup of the automatic update system consists of three parts:
- The update server: A simple php script that returns the update information in XML or JSON format.
- Implementing automatic updates for commercial WordPress plugins
- Implementing automatic updates for commercial Joomla/VirtueMart plugins
The fundamentals of our solution and the environment
Of course, the solution we present here was not developed from scratch, but we rather build on existing technology:
- The plugin-update-checker for WordPress by Janis Elsts:
- Joomla's Update Server feature in connection with Nicholas' extra_query feature (merged in Joomla 3.2)
For both approaches, we had to implement the GUI to enter the access credentials to the software download ourselves.
Both automatic extension update systems depend on an update file (XML for Joomla, JSON for WordPress) to hold the version information and the download URL. The CMS will periodically check that update file and if a new version is detected, this information is stored in the database and presented to the admin whenever the admin backend is viewed.
Our Setup to distribute Extensions
At open-tools.net we use VirtueMart to sell extensions for various e-commerce suites and make the corresponding plugin files available for download. Freely available plugins have a fixed download link like:
http://open-tools.net/index.php?option=com_virtuemart&view=plugin&name=downloads_for_sale&customfield_id=10
That download link will not change when a new version is released. For paid plugins the download link looks similar, except that the order number and the order password (sent to customers in the order confirmation mail) are included in the link, too:
http://open-tools.net/index.php?option=com_virtuemart&view=plugin&name=downloads_for_sale&customfield_id=13&order_number=VM-16-0001&order_pass=p_f9iK23w
Parts of this Tutorial
The setup of the automatic update system consists of three parts:
- The update server: A simple php script that returns the update information in XML or JSON format.
- Implementing automatic updates for commercial WordPress plugins
- Implementing automatic updates for commercial Joomla/VirtueMart plugins
The Update Server
The first step of the update system is the update server, which provides all information about the latest version of an extension in an XML or JSON file. Since the download URLs do not change for new versions, we will base our approach on static template files that are returned by the script (the directory structure can be seen in the screenshot on the right). Only the order_number and order_pass are installation-specific, so they must be inserted by the script into the URL contained in the update information file. Here is the (trivially simple) code for the update server script, which only returns the update file template with the credentials inserted:
<?php function get_query_arg($arg) { } $package = get_query_arg('package'); $extension = get_query_arg('extension'); $order_number = get_query_arg('order_number'); $order_pass = get_query_arg('order_pass'); // Log all update queries FILE_APPEND ); $tmplfile = __DIR__ . '/' . $package . '/' . $extension . '.tmpl'; echo $updatefile; }
A typical call to this update server is:
http://www.open-tools.net/UpdateServer/index.php?package=WooCommerce&extension=AdvancedOrdernumbers
Such a URL will be given in the Joomla plugin's manifest file, as well as in the WordPress plugin's update checker.
The template files have the following form:
WordPress | Joomla |
---|---|
{ "name": "Advanced Order Numbers for WooCommerce", "slug": "woocommerce-advanced-ordernumbers", "homepage" : "http://open-tools.net/woocommerce/advanced-ordernumbers-for-woocommerce.html", "download_url": "https://www.open-tools.net/index.php?option=com_virtuemart&view=plugin&name=downloads_for_sale&customfield_id=149&order_number=${order_number}&amp;order_pass=${order_pass}", "version": "1.2", "upgrade_notice" : "Minor Bug fixes", "author": "Open Tools", "author_homepage" : "http://www.open-tools.net/", "sections": { "description": "Customize order and invoice numbers!", "changelog" : "Version 1.2: <b>Bug fixes</b>" } } |
<?xml version="1.0" encoding="utf-8"?> <updates> <update> <name>Advanced Ordernumbers for Virtuemart</name> <description>Customize your order numbers!</description> <element>ordernumber</element> <type>plugin</type> <folder>vmshopper</folder> <client>0</client><!-- Plugins use 0 (FrontEnd) --> <version>3.7</version> <infourl title="Advanced Ordernumbers for VM">http://open-tools.net/documentation/ordernumber-plugin-for-virtuemart.html#versions</infourl> <downloads> <downloadurl type="full" format="zip">https://open-tools.net/index.php?option=com_virtuemart&amp;view=plugin&amp;name=downloads_for_sale&amp;customfield_id=13</downloadurl> </downloads> <tags> <tag>stable</tag> </tags> <maintainer>Open Tools</maintainer> <maintainerurl>http://open-tools.net</maintainerurl> <targetplatform name="joomla" version="3.[0123456789]" /> </update> </updates> |
You might notice that the Joomla update XML file does not even contain the order_number and order_password in the URL, so for the Joomla updates, we could directly use XML files on the server in the first place. However, we still prefer the server script, since that allows us to log the update requests and it gives us flexibility in the future (e.g. using different download locations for different users).
Automatic Updates for WordPress plugins
The built-in WordPress plugin update checker depends on plugins being distributed via wordpress.org/plugins, which is only possible for free (and publically available) extensions. So for paid (or otherwise private) plugins, the standard update checker cannot be relied on.
Janis Elsts' plugin update checker
Fortunately, Yahnis Elsts wrote a plugin update checker, which we will use for our plugins:
- https://github.com/YahnisElsts/plugin-update-checker (Github source code download)
- http://w-shadow.com/blog/2010/09/02/automatic-updates-for-any-plugin/ (general description of the checker)
- http://w-shadow.com/blog/2013/03/19/plugin-updates-securing-download-links/ (download links with some kind of password protection)
Using this update checker is very simple: Download the plugin-update-checker and save it as a subdirectory of your plugin directory. All you then need to do is to add the following lines to the beginning of your plugin:
require 'plugin-updates/plugin-update-checker.php'; $MyUpdateChecker = PucFactory::buildUpdateChecker( 'http://www.open-tools.net/UpdateServer/index.php?package=WooCommerce&extension=AdvancedOrdernumbers', __FILE__ ); //Add the download credentials to query arguments. $updateChecker->addQueryArgFilter('wsh_filter_update_checks'); public function wsh_filter_update_checks($queryArgs) { $settings = get_option('my_plugin_settings'); if ( !empty($settings['order_number']) ) { $queryArgs['order_number'] = $settings['order_number']; } if ( !empty($settings['order_pass']) ) { $queryArgs['order_pass'] = $settings['order_pass']; } return $queryArgs; }
So far so good. The update checker now works just fine (provided that you have the plugin enabled). However, it is completely left to the plugin developer how the plugin settings and in particular the order number and password are entered.
Our extension to the plugin update checker: GUI to enter the credentials via AJAX
We extended the plugin checker with exactly such a GUI to enter the download credentials, which are then automatically appended to the update server URL (which in turn appends the credentials to the download URL for the plugin). The input fields to enter the credentials are available in WordPress' plugin management page:
Notice the "Update Credentials" link directly under the plugin name. That link will display those input boxes for the order number and the order password.
Here is the source code to our extensions of the plugin update checker (Licence: MIT license):
- opentools-update-checker.php: class OpenToolsPluginUpdateChecker extends PluginUpdateChecker; place this file in your plugin's directory
- assets/css/opentools-updatecheck.css: The styling of the input fields
- assets/js/opentools-updatecheck.js: the AJAX calls to display the download credentials input fields and to button to check them.
To use our extended version of the plugin checker, use the following code in your plugin:
require 'opentools-update-checker.php'; $myUpdateChecker = new OpenToolsPluginUpdateChecker( 'http://www.open-tools.net/UpdateServer/index.php?package=WooCommerce&extension=AdvancedOrdernumbers', __FILE__, 'woocommerce-advanced-ordernumbers' ); $myUpdateChecker->declareCredentials(array( 'order_number' => __('Order Number:'), 'order_pass' => __('Order Password:'), ));
The new function declareCredentials is used to tell the update checker which download credentials are used. The keys of the array are the actual URL attribute names of the credentials, while the item values are the string displayed before the input boxes. Setting update credentials with the declareCredentials will automatically add the "Update Credentials" link to the plugin's entry in the WordPress plugin administration (the plugin MUST be enabled for this link to appear!):
When the user clicks the "Update Credentials" link, an AJAX call back to the plugin requests the row with the download credentials input boxes (the currently saved values are already prefilled) and displays that row:
The user can now enter the requested credentials and then click the button to validate the credentials. Internally, this is again done via an AJAX call to the plugin's update checker, which will store the download credentials, contact the update server with those credentials to get the download URL and then check whether a download of the given plugin download URL (including the credentials as attributes) is possible (the plugin will not actually download the plugin file, it will only request the header to save bandwith). If the download fails, a message is shown and the the input boxes will remain visible:
When the download credentials are correct and the plugin file can be downloaded, the user is shown a success message and the icon in front of the "Update Credentials" link displays a check mark:
Technical details
For those interested in technical details of our GUI implementation, here are a few key points of our code:
- All calls are done via AJAX from jQuery.
- The ajax URL, the plugin slug, the nonces and other parameters are stored in the link's and the submit button's html data attributes:
- <input type="submit" onclick="return submitUpdateCredentials(this);" data-slug="woocommerce-advanced-ordernumbers" data-nonce="f79b0fd70f" data-ajaxurl="http://example.com/wp-admin/admin-ajax.php" data-credentialvars="["order_number","order_pass"]">
- The data attributes are read out from the clicked link/button with jQuery's data function and then used in the AJAX setup.
- The AJAX calls return a 'success', a 'message' and/or a 'row' field. The row is displayed after the table row of the plugin's entry, the message is inserted via jQuery in a div before the input boxes and the 'success' field clearly indicates success or failure.
- When the credentials are submitted with the submit button (via AJAX, action submitUpdateCredentials), the plugin stores the credentials as WordPress options named 'otup_credentials_[plugin-slug]_[credential-name]', e.g. '
otup_credentials_woocommerce-advanced-ordernumbers_order_number
'. It then uses the plugin checker's requestInfo function to download the update information JSON file from the update server and extracts the download_url. Finally, the download_url is contacted (the plugin is not actually downloaded, only the http headers are requested). If that succeeds, the plugin returns success and marks the credentials as validated. - If your download link - like our VirtueMart download plugin - has an extra attribute that allows indicating that only an access check for the download is done (i.e. no actual data is downloaded to save bandwidth and the download should not count against a maximum number of allowed downloads), you can use the
addAccessCheckQueryArgFilter
function to register a callback to modify the download URL for the access check:
$myUpdateChecker->addAccessCheckQueryArgFilter('oton_addAccessCheckArg') function oton_addAccessCheckArg($downloadurl) { return (parse_url($downloadurl, PHP_URL_QUERY) ? '&' : '?') . 'check_access=1'; }
Automatic Updates for Joomla/VirtueMart plugins
Joomla's update checking system
Implementing automatic updates for password-protected downloads in Joomla turned out a bit more tricky. Joomla checks the update server given in the plugin's manifest XML file (one fixed URL for all users, no access password possible) to find new versions:
<updateservers> <server type="extension" name="Advanced Ordernumbers for VirtueMart Updates"><![CDATA[http://www.open-tools.net/UpdateServer/index.php?package=Joomla&extension=Ordernumber&file=extension.xml]]></server> </updateservers>
Notice that (1) since the file is XML. the & in the URL need to be encoded as & and (2) the appended file=extension.xml is required for Joomla 2.5, since that Joomla version explicitly checks whether the URL has a .xml extension.
If a new version is found, the download_url in the update server's response (again in XML format) is used for the download. The plugin itself is never involved and cannot append the download credentials on the fly. However, in Joomla 3.2, a workaround for this was implemented: Now Joomla adds the contents of the extra_query database table of the update servers database table to the download URL. So to append "order_number=VM-15-1234&order_pass=p_abcdef1" to the download url, you "just" have to store that string in the database table [prefix]_update_sites in the extra_query column of the entry for the plugin's update server entry. Sounds good? Well, yes. ...except that now we have the problem how we can store the extra attributes in the database whenever the user enters/updates the download credentials.
The obvious (and in many cases only) place to let the user enter the download credentials is the plugin's configuration in Joomla's plugin manager.
The Problem: Joomla plugins are not loaded by default, storing plugin params is done exclusively by Joomla itself
In contrast to WordPress, Joomla does not load all enabled plugins on startup, but loads plugin groups (system, user, vmshipment, vmpayment, vmshopper, content, etc.) only on demand when its functionality is need. E.g. vmshipment plugins are explicitly loaded by VirtueMart whenever an order is created and the shipment has to be handled. On a normal Joomla or even VirtueMart page load, the plugin is never loaded for performance reasons.
One particular place where this hurts us is the plugin configuration screen, which in Joomla is based on the config fields defined in the plugin's manifest XML file. Joomla auto-creates the form from that manifest without ever loading the plugin itself. The names of all HTML controls are set up so that they exactly reproduce the params structure of the plugin configuration. Even worse, when the plugin configuration is saved, the plugin admin controller uses JForm's functionality to directly save the values of all html fields into the database (remember the html controls' names have already the correct structure!). There is simply no way to modify or otherwise post-process the values after saving the plugin config. (Actually, there is one quirky way, but that involves using an addition plugin and the onInstallerBeforePackageDownload trigger, see Joomla's git pull request #2769)
So, now we can enter the download credentials, and Joomla automatically stores them in the plugin's params field, but Joomla's plugin updater will only check the extra_query column of the [prefix]_update_sites DB table. There is no guarantee that the plugin will ever be even loaded (e.g. a vmextended plugin will only be loaded when a view is requested that is not provided by VirtueMart itself). So we cannot even use the plugin's constructor to write the extra attributes to the extra_query column...
The only solution: Using a custom form field and AJAX calls to VirtueMart plugins
The only solution I found so far is to find a way to make AJAX calls to the plugin and trigger those AJAX calls via a custom form field. In our case, all plugins are VirtueMart extensions, and fortunately VM's plugin controller loads the appropriate plugin class and then calls the plugin's ajax handler. On the other hand, this means that the plugin MUST be enabled before the download credentials can be checked and submitted.
The form fields in the plugin manifest
<config> <fields name="params" addfieldpath="/plugins/vmshopper/ordernumber/fields"> <fieldset name="update_credentials" label="OPENTOOLS_FIELDSET_CREDENTIALS"> <field name="credentials_desc" type="spacer" label="OPENTOOLS_CREDENTIALS_DESC"></field> <field name="order_number" type="text" default="" label="OPENTOOLS_ORDERNUMBER" description="OPENTOOLS_ORDERNUMBER_DESC"></field> <field name="order_pass" type="text" default="" label="OPENTOOLS_ORDERPASS" description="OPENTOOLS_ORDERPASS_DESC"></field> <field name="update_credentials_checked" type="vmUpdateCredentialsCheck" label="" ajaxurl="index.php?option=com_virtuemart&view=plugin&type=vmshopper&name=ordernumber&format=raw"></field> </fieldset> </fields> </config>
You might notice that the update check field (will actually just be a button that triggers an AJAX call and handles the returned JSON) has the AJAX URL given as an attribute. This means that the vmUpdateCredendialsCheck field will be generic and can be simply used in any plugin you like without the need to adjust anything.
The vmUpdateCredentialsCheck form field
Download the file: fields/vmupdatecredentialscheck.php
<?php defined('_JEXEC') or die(); class JFormFieldVmUpdateCredentialsCheck extends JFormField { var $_name = 'vmUpdateCredentialsCheck'; // VM2/J2 and VM3/J2 work, but VM3/J2 does NOT properly load jQuery! public function loadjQuery() { vmJsApi::jQuery(); // TODO: jquery::ui available only in J3: if (version_compare(JVERSION, '3.0', 'lt')) { } else { JHtml::_('jquery.ui', array('core', 'sortable')); } // If we are on Joomla 2.5 and VM 3, manually add the script declarations // cached in vmJsApi to the document header: // SEE THE FULL FILE (AVAILABLE FOR DOWNLOAD ABOVE!) if (version_compare(JVERSION, '3.0', 'lt') && defined('VM_VERSION') && VM_VERSION>=3) { // SEE THE FULL FILE (AVAILABLE FOR DOWNLOAD ABOVE!) } } protected function getJavaScript() { return "[... SEE JAVASCRIPT BELOW ...]"; } protected function getCSS() { return "[... SEE CSS BELOW ...]"; } protected function getInput() { // Tell the user that automatic updates are not available in Joomla 2.5: if (version_compare(JVERSION, '3.0', 'lt')) { JFactory::getApplication()->enqueueMessage(JText::_('OPENTOOLS_COMMERCIAL_UPDATES_J25'), 'warning'); } $this->loadjQuery(); $doc = JFactory::getDocument(); $doc->addScriptDeclaration($this->getJavaScript()); $doc->addStyleDeclaration($this->getCSS()); if ($this->value!=1) { $this->value=0; } return "<input type='hidden' id=\"update_credentials_hidden_checked\" name='".$this->name."' value='".$this->value."' /><div class='credentials_checked credentials_checked_".$this->value."'><a href=\"#\" class=\"btn btn-info credentials_check\" id=\"credentials_check\" onclick=\"checkUpdateCredentials()\" >".JText::_('OPENTOOLS_CHECK_CREDENTIALS')."</a></div>"; } }
The JavaScript to handle the AJAX calls
This javascript is returned in the getJavascript method. For clarity, we copied it here to clearly separate the php code for the form field and the javascript and CSS.
var credentials_ajaxurl = "<?php echo $this->element["ajaxurl"]; ?>"; var credentials_updateMessages = function(messages, area) { jQuery( "#system-message-container #system-message ."+area+"-message").remove(); var newmessages = jQuery( messages ).find("div.alert, .message").addClass(area+"-message"); if (!jQuery( "#system-message-container #system-message").length && newmessages.length) { if (jQuery(newmessages).first().prop("tagName")=="dt") { // Joomla 2.x: jQuery( "#system-message-container" ).append( "<dl id='system-message'></div>" ); } else { jQuery( "#system-message-container" ).append( "<div id='system-message'></div>" ); } } newmessages.appendTo( "#system-message-container #system-message"); }; var checkUpdateCredentialsError = function() { alert ("<?php echo JText::_('OPENTOOLS_CHECK_CREDENTIALS_ERROR'); ?>"); } var checkUpdateCredentials = function () { var ordernumber = jQuery('#jform_params_order_number').val(); var orderpass = jQuery('#jform_params_order_pass').val(); var ajaxargs = { type: "POST", dataType: "text", url: credentials_ajaxurl, data: { action: "check_update_access", order_number: ordernumber, order_pass: orderpass }, success: function ( json ) { try { json = jQuery.parseJSON(json); credentials_updateMessages(json['messages'], 'ordernumber'); } catch (e) { checkUpdateCredentialsError(); return; } var success=0; if (json.success>0) { success=1; } jQuery('#update_credentials_hidden_checked').val(success); jQuery('.credentials_checked') .removeClass('credentials_checked_0') .removeClass('credentials_checked_1') .addClass('credentials_checked_'+success); }, error: function() { checkUpdateCredentialsError(); }, complete: function() { }, }; jQuery.ajax(ajaxargs); }; jQuery(document).ready (function () { jQuery('#jform_params_order_number').focusout(checkUpdateCredentials); jQuery('#jform_params_order_pass').focusout(checkUpdateCredentials); });
The CSS style informatoin
Again, this is returned by the corresponding member of the credentials check form field. We just copy it here separately so it does not destract from the actual php code:
div.credentials_checked { padding: 10px 5px; float: left; clear: left; display: block; width: 100%; } div.credentials_checked_0 { background-color: #FFD0D0; } div.credentials_checked_1 { background-color: #D0FFD0; }
The AJAX handling in the plugin class
The required methods of the plugin's class (respond to AJAX, check validity and store to params and as extra_query
class plgVmShopperOrdernumber extends vmShopperPlugin { [...] /** * plgVmOnSelfCallBE ... Called to execute some plugin action in the backend (e.g. set/reset dl counter, show statistics etc.) */ function plgVmOnSelfCallBE($type, $name, &$output) { if ($name != $this->_name || $type != $this->_type) return false; $user = JFactory::getUser(); $authorized = ($user->authorise('core.admin','com_virtuemart') or $user->authorise('core.manage','com_virtuemart') or $user->authorise('vm.orders','com_virtuemart')); $json = array('authorized' => $authorized); if (!$authorized) return $json; $action = vRequest::getCmd('action'); $json['action'] = $action; $json['success'] = 0; // default: unsuccessfull switch ($action) { case "check_update_access": $order_number = vRequest::getString('order_number'); $order_pass = vRequest::getString('order_pass'); $json = $this->checkUpdateAccess($order_number, $order_pass, $json); break; } // Also return all messages (in HTML format!): // Since we are in a JSON document, we have to temporarily switch the type to HTML // to make sure the html renderer is actually used $document = JFactory::getDocument (); $previoustype = $document->getType(); $document->setType('html'); $msgrenderer = $document->loadRenderer('message'); $json['messages'] = $msgrenderer->render('Message'); $document->setType($previoustype); // WORKAROUND for broken (i.e. duplicate) content-disposition headers in Joomla 2.x: // We request everything in raw and here send the headers for JSON and return // the raw output in json format $document =JFactory::getDocument(); $document->setMimeEncoding('application/json'); JResponse::setHeader('Content-Disposition','attachment;filename="ordernumber.json"'); $output = json_encode($json); } public function checkUpdateAccess($order_number, $order_pass, $json = array()) { // First, extract the update server URL from the manifest, then load // the update XML from the update server, extract the download URL, // append the order number and password and check whether access is // possible. $json['success'] = FALSE; if (isset($this->_xmlFile)) { $xmlfile = $this->_xmlFile; } else { // VM 2 does not set the _xmlFile property, so construct it manually $xmlfile = JPATH_SITE . '/plugins/' . $this->_type . '/' . $this->_name . '/' . $this->_name . '.xml'; } $xml = simplexml_load_file($xmlfile); if (!$xml || !isset($xml->updateservers)) { JFactory::getApplication()->enqueueMessage(JText::sprintf('OPENTOOLS_XMLMANIFEST_ERROR', $this->_xmlFile), 'error'); return $json; } $updateservers = $xml->updateservers; foreach ($updateservers->children() as $server) { if ($server->getName()!='server') { JFactory::getApplication()->enqueueMessage(JText::sprintf('OPENTOOLS_XMLMANIFEST_ERROR', $this->_xmlFile), 'error'); continue; } $updateurl = html_entity_decode((string)$server); $updatescript = simplexml_load_file($updateurl); if (!$updatescript) { JFactory::getApplication()->enqueueMessage(JText::sprintf('OPENTOOLS_UPDATESCRIPT_ERROR', $updateurl), 'error'); continue; } $urls = $updatescript->xpath('/updates/update/downloads/downloadurl'); while (list( , $node) = each($urls)) { $downloadurl = (string)($node); if ($order_number) { $downloadurl .= (parse_url($downloadurl, PHP_URL_QUERY) ? '&' : '?') . 'order_number=' . urlencode($order_number); } if ($order_pass) { $downloadurl .= (parse_url($downloadurl, PHP_URL_QUERY) ? '&' : '?') . 'order_pass=' . urlencode($order_pass); } $downloadurl .= (parse_url($downloadurl, PHP_URL_QUERY) ? '&' : '?') . 'check_access=1'; $headers = get_headers($downloadurl); list($version, $status_code, $msg) = explode(' ',$headers[0], 3); // Check the HTTP Status code switch($status_code) { case 200: $json['success'] = TRUE; JFactory::getApplication()->enqueueMessage($msg, 'message'); $this->setupUpdateCredentials($order_number, $order_pass); break; default: JFactory::getApplication()->enqueueMessage($msg, 'error'); // Clear the credentials... $this->setupUpdateCredentials("", ""); break; } $this->setAndSaveParams(array( 'update_credentials_checked'=>$json['success'], 'order_number' => $order_number, 'order_pass' => $order_pass, )); } } return $json; } protected function setAndSaveParams($params) { $db = JFactory::getDbo(); $query = $db->getQuery(true) ->select('extension_id') ->from('#__extensions') ->where('folder = '.$db->quote($this->_type)) ->where('element = '.$db->quote($this->_name)) ->where('type =' . $db->quote('plugin')) ->order('ordering'); $plugin = $db->setQuery($query)->loadObject(); if (!$plugin) return; $pluginId=$plugin->extension_id; foreach ($params as $param=>$parvalue) { $this->params->set($param, $parvalue); } $extensions = JTable::getInstance('extension'); $extensions->load($pluginId); $extensions->bind(array('params' => $this->params->toString())); // check and store if (!$extensions->check()) { $this->setError($extensions->getError()); return false; } if (!$extensions->store()) { $this->setError($extensions->getError()); return false; } } function setupUpdateCredentials($ordernumber, $orderpass) { $db = JFactory::getDbo(); $query = $db->getQuery(true) ->select('extension_id AS id') ->from('#__extensions') ->where('folder = '.$db->quote($this->_type)) ->where('element = '.$db->quote($this->_name)) ->where('type =' . $db->quote('plugin')) ->order('ordering'); $plugin = $db->setQuery($query)->loadObject(); if (empty($plugin)) return; $ordernumber = preg_replace("/[^-A-Za-z0-9_]/", '', $ordernumber); $orderpass = preg_replace("/[^-A-Za-z0-9_]/", '', $orderpass); $extra_query = array(); if ($ordernumber!='') { $extra_query[] = 'order_number='.preg_replace("/[^-A-Za-z0-9_]/", '', $ordernumber); } if ($orderpass!='') { $extra_query[] = 'order_pass='.preg_replace("/[^-A-Za-z0-9_]/", '', $orderpass); } // Joomla 2.5 needs the filename to end in .zip: $extra_query[] = 'filetype=.zip'; $extra_query = implode('&', $extra_query); // The following code is based on Nicholas K. Dionysopoulos' Joomla Pull request: // https://github.com/joomla/joomla-cms/pull/2508 // Load the update site record, if it exists $db = JFactory::getDbo(); $query = $db->getQuery(true) ->select('update_site_id AS id') ->from($db->qn('#__update_sites_extensions')) ->where($db->qn('extension_id').' = '.$db->q($plugin->id)); $db->setQuery($query); $updateSites = $db->loadObjectList(); foreach ($updateSites as $updateSite) { // Update the update site record $query = $db->getQuery(true) ->update($db->qn('#__update_sites')) ->set('extra_query = '.$db->q($extra_query)) ->set('last_check_timestamp = 0') ->where($db->qn('update_site_id').' = '.$db->q($updateSite->id)); $db->setQuery($query); $db->execute(); // Delete any existing updates (essentially flushes the updates cache for this update site) $query = $db->getQuery(true) ->delete($db->qn('#__updates')) ->where($db->qn('update_site_id').' = '.$db->q($updateSite->id)); $db->setQuery($query); $db->execute(); } } }
The situation in Joomla 2.5?
Unfortunately, the extra_query feature to append extra params to the download URL was introduced only in Joomla 3.2, so it cannot be used in Joomla 2.5. So for Joomla 2.5 there is - to our knowledge - no way to properly implement automatic plugin updates for commercial plugins.
There is a Joomla git pull request #2769 that adds a trigger onInstallerBeforePackageDownload for installer plugins that is called with the download URL right before a package is downloaded. Unfortunately, that (1) requires an additional plugin of type installer to handle the extra params and (2) the trigger only passes the URL, but no information about which extension is about to be updated. To figure out whether the download is for your current plugin, you would need to compare the download URL to the download URL of your plugin, which means the download URL has to be hardcoded into the plugin, which somehow defeats the purpose of update scripts on an external update server giving the download URL dynamically. In particular, whenever the download URL changes, your update system for all previous releases of the plugin will be broken...
So, in Joomla 2.5 there is simply no way (at least AFAIK) to implement automatic updates of commercial (password-protected) extensions.