diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ec2df58..090b7693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 58bad838..626896b6 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -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( + 'MokoWaaS License Key Required — ' + . 'No download key is configured. Updates will not be available until a valid license key is entered. ' + . 'Go to System → Update Sites ' + . '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( + 'MokoWaaS License Key Invalid — ' + . 'Your license key could not be validated. Please verify your key in ' + . 'System → Update Sites.', + '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 or non-200 means invalid key + $isValid = ($httpCode === 200 && $response && strpos($response, '') !== false); + + $session->set('mokowaas.license_invalid', !$isValid); + + if (!$isValid) + { + $this->app->enqueueMessage( + 'MokoWaaS License Key Invalid — ' + . 'Your license key could not be validated. Updates will not be available. ' + . 'Please verify your key in ' + . 'System → Update Sites.', + 'error' + ); + } + } + catch (\Throwable $e) + { + // Silent — license check is non-critical + } + } + // ------------------------------------------------------------------ /** diff --git a/src/script.php b/src/script.php index a022b809..03eace5a 100644 --- a/src/script.php +++ b/src/script.php @@ -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.