feat: license key warning + heartbeat validation + stale update site cleanup
- Persistent admin warning when no download key is set on the MokoWaaS update site, with link to System → Update Sites - Daily heartbeat validates the key against MokoGitea's dynamic endpoint; shows error if key is invalid or expired - Package postflight removes stale/duplicate update site entries and orphaned #__updates rows Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,9 @@
|
||||
- License key support via Joomla's native Update Sites download key system (dlid)
|
||||
- Update server URL migrated from static XML to MokoGitea's dynamic update feed endpoint
|
||||
- Legacy static update site URLs auto-migrated to dynamic endpoint on install/update
|
||||
- Persistent admin warning when no license key is configured in Update Sites
|
||||
- Daily heartbeat validation of license key against MokoGitea — warns if key is invalid or expired
|
||||
- Stale/duplicate update site cleanup on install/update (removes old static URL entries and orphaned records)
|
||||
|
||||
### Removed
|
||||
- Static `updates.xml` — update feed is now generated dynamically by MokoGitea from git releases
|
||||
|
||||
@@ -1000,6 +1000,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$this->warnMissingLicenseKey();
|
||||
$this->enforceAdminRestrictions();
|
||||
$this->protectPlugin();
|
||||
}
|
||||
@@ -3926,6 +3927,124 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Heartbeat (called from onExtensionAfterSave)
|
||||
// ------------------------------------------------------------------
|
||||
// License key check (called from onAfterRoute)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Show a persistent admin warning if no license key is set on the
|
||||
* MokoWaaS update site.
|
||||
*
|
||||
* Checks the extra_query column in #__update_sites for a dlid value.
|
||||
* Also validates the key against MokoGitea on a heartbeat interval
|
||||
* (once per day) and warns if the key is invalid or expired.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.30.00
|
||||
*/
|
||||
protected function warnMissingLicenseKey(): void
|
||||
{
|
||||
// Only show to master users
|
||||
if (!$this->isMasterUser())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('extra_query'))
|
||||
->from($db->quoteName('#__update_sites'))
|
||||
->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoWaaS%')
|
||||
. ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoWaaS%') . ')')
|
||||
->setLimit(1);
|
||||
$db->setQuery($query);
|
||||
$extraQuery = (string) $db->loadResult();
|
||||
|
||||
if (empty($extraQuery) || strpos($extraQuery, 'dlid=') === false)
|
||||
{
|
||||
$this->app->enqueueMessage(
|
||||
'<strong>MokoWaaS License Key Required</strong> — '
|
||||
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
|
||||
. 'Go to <a href="index.php?option=com_installer&view=updatesites">System → Update Sites</a> '
|
||||
. 'and enter your license key (MOKO-XXXX-XXXX-XXXX-XXXX) in the Download Key field for the MokoWaaS update site.',
|
||||
'warning'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the key value from extra_query
|
||||
parse_str($extraQuery, $parsed);
|
||||
$licenseKey = $parsed['dlid'] ?? '';
|
||||
|
||||
if (empty($licenseKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Heartbeat validation — check once per day
|
||||
$session = Factory::getSession();
|
||||
$lastCheck = (int) $session->get('mokowaas.license_check', 0);
|
||||
$now = time();
|
||||
|
||||
if (($now - $lastCheck) < 86400)
|
||||
{
|
||||
// Show cached warning if key was invalid last check
|
||||
if ($session->get('mokowaas.license_invalid', false))
|
||||
{
|
||||
$this->app->enqueueMessage(
|
||||
'<strong>MokoWaaS License Key Invalid</strong> — '
|
||||
. 'Your license key could not be validated. Please verify your key in '
|
||||
. '<a href="index.php?option=com_installer&view=updatesites">System → Update Sites</a>.',
|
||||
'error'
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate against MokoGitea
|
||||
$session->set('mokowaas.license_check', $now);
|
||||
|
||||
$validateUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml'
|
||||
. '?dlid=' . urlencode($licenseKey)
|
||||
. '&domain=' . urlencode(Uri::root());
|
||||
|
||||
$ch = curl_init($validateUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
// Empty <updates></updates> or non-200 means invalid key
|
||||
$isValid = ($httpCode === 200 && $response && strpos($response, '<update>') !== false);
|
||||
|
||||
$session->set('mokowaas.license_invalid', !$isValid);
|
||||
|
||||
if (!$isValid)
|
||||
{
|
||||
$this->app->enqueueMessage(
|
||||
'<strong>MokoWaaS License Key Invalid</strong> — '
|
||||
. 'Your license key could not be validated. Updates will not be available. '
|
||||
. 'Please verify your key in '
|
||||
. '<a href="index.php?option=com_installer&view=updatesites">System → Update Sites</a>.',
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
// Silent — license check is non-critical
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,6 +45,9 @@ class Pkg_MokowaasInstallerScript
|
||||
// Mark MokoWaaS extensions as protected (prevents disable/uninstall at framework level)
|
||||
$this->protectExtensions();
|
||||
|
||||
// Clean up stale/duplicate update sites
|
||||
$this->cleanupStaleUpdateSites();
|
||||
|
||||
// Trigger heartbeat registration
|
||||
$this->sendHeartbeat();
|
||||
}
|
||||
@@ -218,6 +221,92 @@ class Pkg_MokowaasInstallerScript
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove stale and duplicate MokoWaaS update site entries.
|
||||
*
|
||||
* Keeps only the package-level update site pointing to the dynamic
|
||||
* MokoGitea endpoint. Removes plugin-level entries, old static URLs,
|
||||
* and orphaned #__updates rows tied to deleted update sites.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.30.00
|
||||
*/
|
||||
private function cleanupStaleUpdateSites(): void
|
||||
{
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$dynamicUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml';
|
||||
|
||||
// Find all MokoWaaS update sites
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName(['update_site_id', 'location']))
|
||||
->from($db->quoteName('#__update_sites'))
|
||||
->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoWaaS%')
|
||||
. ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoWaaS%') . ')');
|
||||
$db->setQuery($query);
|
||||
$sites = $db->loadObjectList();
|
||||
|
||||
$keepId = null;
|
||||
$removeIds = [];
|
||||
|
||||
foreach ($sites as $site)
|
||||
{
|
||||
if ($site->location === $dynamicUrl && $keepId === null)
|
||||
{
|
||||
$keepId = (int) $site->update_site_id;
|
||||
}
|
||||
else
|
||||
{
|
||||
$removeIds[] = (int) $site->update_site_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($removeIds))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$idList = implode(',', $removeIds);
|
||||
|
||||
// Remove orphaned #__updates rows
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__updates'))
|
||||
->where($db->quoteName('update_site_id') . ' IN (' . $idList . ')')
|
||||
)->execute();
|
||||
|
||||
// Remove link rows
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__update_sites_extensions'))
|
||||
->where($db->quoteName('update_site_id') . ' IN (' . $idList . ')')
|
||||
)->execute();
|
||||
|
||||
// Remove stale update sites
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__update_sites'))
|
||||
->where($db->quoteName('update_site_id') . ' IN (' . $idList . ')')
|
||||
)->execute();
|
||||
|
||||
$count = count($removeIds);
|
||||
|
||||
if ($count > 0)
|
||||
{
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
sprintf('Cleaned up %d stale MokoWaaS update site(s).', $count),
|
||||
'message'
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Error cleaning up stale update sites: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the MokoWaaS update server entry stays enabled and points
|
||||
* to the correct dynamic endpoint with the license key attached.
|
||||
|
||||
Reference in New Issue
Block a user