fix: preserve download keys across Joomla extension updates (#187)
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled

Joomla's installer can wipe extra_query (dlid) from #__update_sites
when rebuilding or reinstalling. The core plugin now backs up all
download keys and auto-restores any that get cleared. Runs on every
admin page load with a single lightweight query.

Closes #187

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-06-06 07:02:22 -05:00
parent 68ffffe2af
commit d792b7ff0c
@@ -166,6 +166,12 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
{
$this->handleMokoApi($mokoAction);
}
// Preserve download keys (admin only, lightweight check)
if ($this->app->isClient('administrator'))
{
$this->preserveDownloadKeys();
}
}
/**
@@ -2046,6 +2052,107 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
return $this->masterNames;
}
// ------------------------------------------------------------------
// Download Key Preservation
// ------------------------------------------------------------------
/**
* Preserve download keys across Joomla extension updates.
*
* Joomla's installer can wipe the extra_query column (which holds
* download keys / dlid) when rebuilding or reinstalling update sites.
* This method keeps a backup of all non-empty extra_query values and
* restores any that get cleared.
*
* @return void
*
* @since 02.34.12
*/
protected function preserveDownloadKeys(): void
{
try
{
$db = Factory::getDbo();
// Load current extra_query values for all update sites
$query = $db->getQuery(true)
->select([
$db->quoteName('update_site_id'),
$db->quoteName('extra_query'),
$db->quoteName('location'),
])
->from($db->quoteName('#__update_sites'));
$db->setQuery($query);
$sites = $db->loadObjectList('update_site_id') ?: [];
$backupFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_dlkeys.json';
$backup = [];
if (file_exists($backupFile))
{
$backup = json_decode(file_get_contents($backupFile), true) ?: [];
}
$restored = 0;
$updated = false;
foreach ($sites as $id => $site)
{
$currentKey = trim((string) $site->extra_query);
$backupKey = $backup[$id] ?? '';
if ($currentKey !== '')
{
// Site has a key — update backup if changed
if ($currentKey !== $backupKey)
{
$backup[$id] = $currentKey;
$updated = true;
}
}
elseif ($backupKey !== '')
{
// Key was wiped — restore from backup
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('extra_query') . ' = ' . $db->quote($backupKey))
->where($db->quoteName('update_site_id') . ' = ' . (int) $id)
)->execute();
$restored++;
}
}
// Clean up backup entries for update sites that no longer exist
$currentIds = array_keys($sites);
foreach (array_keys($backup) as $backupId)
{
if (!isset($sites[$backupId]))
{
unset($backup[$backupId]);
$updated = true;
}
}
if ($updated || $restored > 0)
{
file_put_contents($backupFile, json_encode($backup, JSON_PRETTY_PRINT));
}
if ($restored > 0)
{
Log::add(
sprintf('MokoWaaS: restored %d download key(s) that were cleared by Joomla.', $restored),
Log::INFO,
'mokowaas'
);
}
}
catch (\Throwable $e)
{
// Non-critical — don't break the site over key backup
}
}
}