diff --git a/CHANGELOG.md b/CHANGELOG.md index 8650fa1a..f1cc559c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,24 @@ # Changelog ## [Unreleased] +### Fixed +- Ticket automation and offline bypass plugins not enabling on install/update — `enablePlugin()` now handles empty element columns with a fallback match by manifest name +- Heartbeat silently failing — `sendHeartbeat()` was reading config from the retired monitor plugin; now reads from core plugin params +- Cpanel module not publishing on update — `ensureAdminModule()` now does a direct DB update for existing modules (bypasses ModuleModel checked_out issues) +- Cpanel module access level changed from 6 (may not exist) to 3 (Special) +- Admin menu module ordering set to -1 to ensure it appears at the top of the menu position + +### Changed +- Retired monitor plugin config (base_url, signing_key, heartbeat_enabled) consolidated into core plugin params with one-time migration +- Runtime heartbeat moved from retired `plg_system_mokosuiteclient_monitor` into core plugin (`checkHeartbeat` on admin page load after version change) +- `DisplayController::sendHeartbeat()` reads from core plugin instead of retired monitor plugin +- Removed monitor plugin from cpanel dashboard plugin grid + +### Added +- Missing language strings for IMAP poll and auto-close ticket automation task routines +- Monitor fieldset in core plugin XML with heartbeat_enabled, monitor_base_url, and monitor_signing_key fields +- Language strings for monitor fieldset (PLG_SYSTEM_MOKOSUITECLIENT_FIELDSET_MONITOR_*) + ## [02.35.00] --- 2026-06-18 diff --git a/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php b/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php index 237c4611..be6b7714 100644 --- a/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php +++ b/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php @@ -90,22 +90,22 @@ class DisplayController extends BaseController try { - $monitorPlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokosuiteclient_monitor'); + $corePlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokosuiteclient'); - if (!$monitorPlugin) + if (!$corePlugin) { - $this->jsonResponse(['success' => false, 'message' => 'Monitor plugin not enabled.']); + $this->jsonResponse(['success' => false, 'message' => 'Core plugin not enabled.']); return; } - $params = new \Joomla\Registry\Registry($monitorPlugin->params); - $baseUrl = rtrim($params->get('base_url', ''), '/'); + $params = new \Joomla\Registry\Registry($corePlugin->params); + $baseUrl = rtrim($params->get('monitor_base_url', ''), '/'); - // Fall back to manifest XML default if not yet saved in params + // Fall back to manifest XML default if (empty($baseUrl)) { - $manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient_monitor/mokosuiteclient_monitor.xml'; + $manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient/mokosuiteclient.xml'; if (is_file($manifestFile)) { @@ -113,7 +113,7 @@ class DisplayController extends BaseController if ($xml) { - foreach ($xml->xpath('//field[@name="base_url"]') as $field) + foreach ($xml->xpath('//field[@name="monitor_base_url"]') as $field) { $baseUrl = rtrim((string) $field['default'], '/'); break; @@ -124,14 +124,12 @@ class DisplayController extends BaseController if (empty($baseUrl)) { - $this->jsonResponse(['success' => false, 'message' => 'MokoSuiteClientHQ URL not configured in monitor plugin.']); + $this->jsonResponse(['success' => false, 'message' => 'MokoSuiteClientHQ URL not configured.']); return; } - $corePlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokosuiteclient'); - $coreParams = new \Joomla\Registry\Registry($corePlugin ? $corePlugin->params : '{}'); - $healthToken = $coreParams->get('health_api_token', ''); + $healthToken = $params->get('health_api_token', ''); if (empty($healthToken)) { @@ -156,12 +154,12 @@ class DisplayController extends BaseController // RSA sign the request $headers = ['Content-Type: application/json']; - $signingKeyB64 = $params->get('signing_key', ''); + $signingKeyB64 = $params->get('monitor_signing_key', ''); - // Fall back to manifest XML default if not yet saved in params + // Fall back to manifest XML default if (empty($signingKeyB64)) { - $manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient_monitor/mokosuiteclient_monitor.xml'; + $manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient/mokosuiteclient.xml'; if (is_file($manifestFile)) { @@ -169,7 +167,7 @@ class DisplayController extends BaseController if ($xml) { - foreach ($xml->xpath('//field[@name="signing_key"]') as $field) + foreach ($xml->xpath('//field[@name="monitor_signing_key"]') as $field) { $signingKeyB64 = (string) $field['default']; break; diff --git a/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php b/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php index 8ce4b5eb..e9226ed2 100644 --- a/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php +++ b/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php @@ -48,7 +48,6 @@ $labels = [ 'mokosuiteclient_firewall' => 'Firewall', 'mokosuiteclient_tenant' => 'Tenant', 'mokosuiteclient_devtools' => 'DevTools', - 'mokosuiteclient_monitor' => 'Monitor', ]; $diskPct = ($disk->total_mb && $disk->total_mb > 0) diff --git a/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php b/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php index c7e9190f..15c58cc9 100644 --- a/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php +++ b/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php @@ -36,6 +36,7 @@ use Joomla\CMS\Factory; use Joomla\CMS\Log\Log; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Uri\Uri; +use Joomla\CMS\Version; use Psr\Container\ContainerInterface; /** @@ -164,6 +165,7 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface $this->handleOneTimeLogin(); $this->checkSetupRequired(); $this->ensureAdminModulesActive(); + $this->checkHeartbeat(); } } @@ -2392,4 +2394,256 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface ); } } + + // ── Heartbeat Monitor ───────────────────────────────────────── + + /** + * Send heartbeat to MokoSuiteClientHQ when the package version changes. + * Runs once per session on admin page load. + */ + private function checkHeartbeat(): void + { + try + { + if ($this->params->get('heartbeat_enabled', '1') === '0') + { + return; + } + + $session = Factory::getSession(); + + if ($session->get('mokosuiteclient.heartbeat_sent', false)) + { + return; + } + + $lastVersion = $this->params->get('_last_heartbeat_version', ''); + $currentVersion = $this->getPluginVersion(); + + if ($lastVersion === $currentVersion) + { + return; + } + + $session->set('mokosuiteclient.heartbeat_sent', true); + $this->sendRuntimeHeartbeat(); + + // Store version so we don't re-send every session + $this->params->set('_last_heartbeat_version', $currentVersion); + + $extension = new \Joomla\CMS\Table\Extension(Factory::getDbo()); + $extension->load(['element' => 'mokosuiteclient', 'folder' => 'system', 'type' => 'plugin']); + $extension->params = $this->params->toString(); + $extension->store(); + } + catch (\Throwable $e) + { + // Non-critical — never break admin for a heartbeat + } + } + + /** + * Send heartbeat data to MokoSuiteClientHQ. + * RSA-signed: client signs domain|timestamp|token with its private key. + */ + private function sendRuntimeHeartbeat(): void + { + $baseUrl = rtrim($this->params->get('monitor_base_url', ''), '/'); + + // Fall back to manifest XML default + if (empty($baseUrl)) + { + $manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient/mokosuiteclient.xml'; + + if (is_file($manifestFile)) + { + $xml = simplexml_load_file($manifestFile); + + if ($xml) + { + foreach ($xml->xpath('//field[@name="monitor_base_url"]') as $field) + { + $baseUrl = rtrim((string) $field['default'], '/'); + break; + } + } + } + } + + if (empty($baseUrl)) + { + return; + } + + $healthToken = $this->params->get('health_api_token', ''); + + if (empty($healthToken)) + { + return; + } + + $siteUrl = rtrim(Uri::root(), '/'); + $domain = parse_url($siteUrl, PHP_URL_HOST) ?: ''; + + if (empty($domain)) + { + return; + } + + $config = Factory::getConfig(); + $timestamp = time(); + + $payload = [ + 'token' => $healthToken, + 'domain' => $domain, + 'site_name' => $config->get('sitename', 'Joomla'), + 'site_url' => $siteUrl, + 'joomla_version' => (new Version())->getShortVersion(), + 'php_version' => PHP_VERSION, + 'mokosuiteclient_version' => $this->getPluginVersion(), + 'timestamp' => $timestamp, + 'client_info' => [ + 'company' => $config->get('sitename', ''), + 'email' => $config->get('mailfrom', ''), + ], + ]; + + // Include live health data + $healthData = $this->fetchLocalHealth($siteUrl, $healthToken); + + if ($healthData !== null) + { + $payload['health'] = $healthData; + } + + // RSA sign the request + $headers = ['Content-Type: application/json']; + $signature = $this->signHeartbeatRequest($domain, $timestamp, $healthToken); + + if ($signature !== null) + { + $headers[] = 'X-MokoSuiteClient-Signature: ' . $signature; + $headers[] = 'X-MokoSuiteClient-Timestamp: ' . $timestamp; + } + + $endpoint = $baseUrl . '/api/index.php/v1/mokosuiteclienthq/heartbeat'; + $json = json_encode($payload, JSON_UNESCAPED_SLASHES); + + try + { + $http = \Joomla\CMS\Http\HttpFactory::getHttp( + new \Joomla\Registry\Registry(['follow_location' => true, 'transport.curl' => ['certpath' => false]]), + ['curl', 'stream'] + ); + + $headerMap = []; + + foreach ($headers as $h) + { + [$key, $val] = explode(': ', $h, 2); + $headerMap[$key] = $val; + } + + $response = $http->post($endpoint, $json, $headerMap, 15); + + if ($response->code >= 200 && $response->code < 300) + { + $this->app->enqueueMessage('MokoSuiteClientHQ heartbeat: site registered', 'message'); + } + else + { + Log::add( + \sprintf('Heartbeat HTTP %d: %s', $response->code, $response->body), + Log::WARNING, + 'mokosuiteclient' + ); + } + } + catch (\Throwable $e) + { + Log::add('Heartbeat failed: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient'); + } + } + + private function signHeartbeatRequest(string $domain, int $timestamp, string $token): ?string + { + $signingKeyB64 = $this->params->get('monitor_signing_key', ''); + + // Fall back to manifest XML default + if (empty($signingKeyB64)) + { + $manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient/mokosuiteclient.xml'; + + if (is_file($manifestFile)) + { + $xml = simplexml_load_file($manifestFile); + + if ($xml) + { + foreach ($xml->xpath('//field[@name="monitor_signing_key"]') as $field) + { + $signingKeyB64 = (string) $field['default']; + break; + } + } + } + } + + if (empty($signingKeyB64)) + { + return null; + } + + $privateKeyPem = base64_decode($signingKeyB64); + + if (empty($privateKeyPem)) + { + return null; + } + + $privateKey = openssl_pkey_get_private($privateKeyPem); + + if ($privateKey === false) + { + return null; + } + + $message = $domain . '|' . $timestamp . '|' . $token; + $signature = ''; + + if (openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256)) + { + return base64_encode($signature); + } + + return null; + } + + private function fetchLocalHealth(string $siteUrl, string $healthToken): ?array + { + try + { + $http = \Joomla\CMS\Http\HttpFactory::getHttp( + new \Joomla\Registry\Registry(['follow_location' => true, 'transport.curl' => ['certpath' => false]]), + ['curl', 'stream'] + ); + + $response = $http->get( + $siteUrl . '/?mokosuiteclient=health', + ['Authorization' => 'Bearer ' . $healthToken, 'Accept' => 'application/json'], + 10 + ); + + if ($response->code !== 200 || empty($response->body)) + { + return null; + } + + return json_decode($response->body, true) ?: null; + } + catch (\Throwable $e) + { + return null; + } + } } diff --git a/source/packages/plg_system_mokosuiteclient/language/en-GB/plg_system_mokosuiteclient.ini b/source/packages/plg_system_mokosuiteclient/language/en-GB/plg_system_mokosuiteclient.ini index 0406d8b3..e7ce9267 100644 --- a/source/packages/plg_system_mokosuiteclient/language/en-GB/plg_system_mokosuiteclient.ini +++ b/source/packages/plg_system_mokosuiteclient/language/en-GB/plg_system_mokosuiteclient.ini @@ -39,3 +39,11 @@ PLG_SYSTEM_MOKOSUITECLIENT_ALIAS_ROBOTS_LABEL="Robots" PLG_SYSTEM_MOKOSUITECLIENT_ALIAS_ROBOTS_DESC="Meta robots directive for this alias domain. Use 'noindex, nofollow' to prevent search engines from indexing the alias." PLG_SYSTEM_MOKOSUITECLIENT_ALIAS_REDIRECT_BACKEND_LABEL="Redirect Backend" PLG_SYSTEM_MOKOSUITECLIENT_ALIAS_REDIRECT_BACKEND_DESC="Redirect admin panel requests on this alias to the primary domain. Frontend stays on the alias domain." + +; ===== Heartbeat Monitor fieldset ===== +PLG_SYSTEM_MOKOSUITECLIENT_FIELDSET_MONITOR_LABEL="Heartbeat Monitor" +PLG_SYSTEM_MOKOSUITECLIENT_FIELDSET_MONITOR_DESC="Settings for the MokoSuiteClientHQ heartbeat registration." +PLG_SYSTEM_MOKOSUITECLIENT_HEARTBEAT_LABEL="Enable Heartbeat" +PLG_SYSTEM_MOKOSUITECLIENT_HEARTBEAT_DESC="Send heartbeat data to MokoSuiteClientHQ on install, update, and admin login." +PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_BASE_URL_LABEL="HQ Base URL" +PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_BASE_URL_DESC="Base URL of the MokoSuiteClientHQ instance that receives heartbeat data." diff --git a/source/packages/plg_system_mokosuiteclient/mokosuiteclient.xml b/source/packages/plg_system_mokosuiteclient/mokosuiteclient.xml index a04936dd..76b9efad 100644 --- a/source/packages/plg_system_mokosuiteclient/mokosuiteclient.xml +++ b/source/packages/plg_system_mokosuiteclient/mokosuiteclient.xml @@ -80,6 +80,29 @@ readonly="true" /> + +
+ + + + + + + + + +
diff --git a/source/packages/plg_task_mokosuiteclient_tickets/language/en-GB/plg_task_mokosuiteclient_tickets.ini b/source/packages/plg_task_mokosuiteclient_tickets/language/en-GB/plg_task_mokosuiteclient_tickets.ini index a319dcc2..f796d882 100644 --- a/source/packages/plg_task_mokosuiteclient_tickets/language/en-GB/plg_task_mokosuiteclient_tickets.ini +++ b/source/packages/plg_task_mokosuiteclient_tickets/language/en-GB/plg_task_mokosuiteclient_tickets.ini @@ -2,3 +2,7 @@ PLG_TASK_MOKOSUITECLIENT_TICKETS="Task - MokoSuiteClient Ticket Automation" PLG_TASK_MOKOSUITECLIENT_TICKETS_DESC="Runs scheduled helpdesk automation rules." PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOMATION_TITLE="MokoSuiteClient: Ticket Automation" PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOMATION_DESC="Runs time-based automation rules against open tickets (auto-close, SLA escalation, etc.)." +PLG_TASK_MOKOSUITECLIENT_TICKETS_IMAP_POLL_TITLE="MokoSuiteClient: IMAP Email Polling" +PLG_TASK_MOKOSUITECLIENT_TICKETS_IMAP_POLL_DESC="Polls an IMAP inbox for new emails and creates tickets or replies from unread messages." +PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOCLOSE_TITLE="MokoSuiteClient: Auto-Close Resolved Tickets" +PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOCLOSE_DESC="Automatically closes tickets that have been in resolved status longer than the configured number of days." diff --git a/source/script.php b/source/script.php index 46408385..ffc3cb0b 100644 --- a/source/script.php +++ b/source/script.php @@ -78,7 +78,6 @@ class Pkg_MokosuiteclientInstallerScript $this->enablePlugin('system', 'mokosuiteclient_devtools'); $this->enablePlugin('system', 'mokosuiteclient_offline'); $this->enablePlugin('system', 'mokosuiteclient_dbip'); - $this->enablePlugin('system', 'mokosuiteclient_monitor'); $this->enablePlugin('webservices', 'mokosuiteclient'); $this->enablePlugin('task', 'mokosuiteclientdemo'); $this->enablePlugin('task', 'mokosuiteclientsync'); @@ -87,6 +86,9 @@ class Pkg_MokosuiteclientInstallerScript // Migrate params from core plugin to feature plugins (one-time) $this->migrateFeatureParams(); + // Migrate monitor params into core plugin before monitor is retired + $this->migrateMonitorParams(); + // Set up cpanel module on the admin dashboard $this->setupCpanelModule(); @@ -469,6 +471,31 @@ class Pkg_MokosuiteclientInstallerScript ->where($db->quoteName('element') . ' = ' . $db->quote($element)); $db->setQuery($query); $db->execute(); + + if ($db->getAffectedRows() > 0) + { + return; + } + + // Row may exist with empty element (DEFAULT '' from preflight ALTER). + // Fix the element value and enable in one pass. + $manifestName = 'plg_' . $group . '_' . $element; + $fix = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('element') . ' = ' . $db->quote($element)) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote($group)) + ->where('(' . $db->quoteName('element') . ' = ' . $db->quote('') + . ' OR ' . $db->quoteName('element') . ' IS NULL)') + ->where($db->quoteName('name') . ' = ' . $db->quote($manifestName)); + $db->setQuery($fix); + $db->execute(); + + if ($db->getAffectedRows() > 0) + { + Log::add('Fixed empty element for plugin ' . $group . '/' . $element, Log::NOTICE, 'mokosuiteclient'); + } } catch (\Throwable $e) { @@ -801,7 +828,7 @@ class Pkg_MokosuiteclientInstallerScript { $db = Factory::getDbo(); - // Get health token from core plugin + // All heartbeat config now lives in the core plugin params $query = $db->getQuery(true) ->select($db->quoteName('params')) ->from($db->quoteName('#__extensions')) @@ -816,20 +843,17 @@ class Pkg_MokosuiteclientInstallerScript return; } - // Get base URL and signing key from monitor plugin - $query = $db->getQuery(true) - ->select($db->quoteName('params')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient_monitor')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')); - $monitorParams = json_decode((string) $db->setQuery($query)->loadResult()); - $baseUrl = rtrim($monitorParams->base_url ?? '', '/'); + if (($coreParams->heartbeat_enabled ?? '1') === '0') + { + return; + } - // Fall back to manifest XML default if not yet saved in params + $baseUrl = rtrim($coreParams->monitor_base_url ?? '', '/'); + + // Fall back to manifest XML default if (empty($baseUrl)) { - $manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient_monitor/mokosuiteclient_monitor.xml'; + $manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient/mokosuiteclient.xml'; if (is_file($manifestFile)) { @@ -837,7 +861,7 @@ class Pkg_MokosuiteclientInstallerScript if ($xml) { - foreach ($xml->xpath('//field[@name="base_url"]') as $field) + foreach ($xml->xpath('//field[@name="monitor_base_url"]') as $field) { $baseUrl = rtrim((string) $field['default'], '/'); break; @@ -867,12 +891,12 @@ class Pkg_MokosuiteclientInstallerScript $headers = ['Content-Type: application/json']; - // RSA sign the request — fall back to manifest XML default - $signingKeyB64 = $monitorParams->signing_key ?? ''; + $signingKeyB64 = $coreParams->monitor_signing_key ?? ''; + // Fall back to manifest XML default if (empty($signingKeyB64)) { - $manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient_monitor/mokosuiteclient_monitor.xml'; + $manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient/mokosuiteclient.xml'; if (is_file($manifestFile)) { @@ -880,7 +904,7 @@ class Pkg_MokosuiteclientInstallerScript if ($xml) { - foreach ($xml->xpath('//field[@name="signing_key"]') as $field) + foreach ($xml->xpath('//field[@name="monitor_signing_key"]') as $field) { $signingKeyB64 = (string) $field['default']; break; @@ -945,12 +969,12 @@ class Pkg_MokosuiteclientInstallerScript */ private function setupCpanelModule(): void { - $this->ensureAdminModule('mod_mokosuiteclient_cpanel', 'MokoSuiteClient', 'top', 6, 0, '{"show_health":"1","show_plugins":"1"}'); + $this->ensureAdminModule('mod_mokosuiteclient_cpanel', 'MokoSuiteClient', 'top', 3, 0, '{"show_health":"1","show_plugins":"1"}'); } private function setupAdminMenuModule(): void { - $this->ensureAdminModule('mod_mokosuiteclient_menu', 'MokoSuiteClient Menu', 'menu', 3, 0); + $this->ensureAdminModule('mod_mokosuiteclient_menu', 'MokoSuiteClient Menu', 'menu', 3, -1); } private function setupCacheModule(): void @@ -989,28 +1013,55 @@ class Pkg_MokosuiteclientInstallerScript ); $moduleId = (int) $db->loadResult(); - // Build save data — Joomla's ModuleModel expects this format - $data = [ - 'title' => $title, - 'module' => $element, - 'position' => $position, - 'published' => 1, - 'access' => $access, - 'ordering' => $ordering, - 'showtitle' => 0, - 'client_id' => 1, - 'language' => '*', - 'params' => $params, - 'assignment' => 0, // 0 = all pages - ]; - if ($moduleId > 0) { - $data['id'] = $moduleId; + // Module exists — ensure it stays published with correct position + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__modules')) + ->set($db->quoteName('published') . ' = 1') + ->set($db->quoteName('position') . ' = ' . $db->quote($position)) + ->set($db->quoteName('ordering') . ' = ' . (int) $ordering) + ->set($db->quoteName('access') . ' = ' . (int) $access) + ->set($db->quoteName('checked_out') . ' = NULL') + ->set($db->quoteName('checked_out_time') . ' = NULL') + ->where($db->quoteName('id') . ' = ' . $moduleId) + )->execute(); + + // Ensure module-menu mapping exists (0 = all pages) + $db->setQuery( + $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = ' . $moduleId) + ); + + if ((int) $db->loadResult() === 0) + { + $db->setQuery( + "INSERT IGNORE INTO " . $db->quoteName('#__modules_menu') + . " (moduleid, menuid) VALUES (" . $moduleId . ", 0)" + )->execute(); + } + + return; } - // Use Joomla's ModuleModel to handle save + menu assignment - \Joomla\CMS\MVC\Factory\MVCFactory::class; + // Module doesn't exist — create via ModuleModel + $data = [ + 'title' => $title, + 'module' => $element, + 'position' => $position, + 'published' => 1, + 'access' => $access, + 'ordering' => $ordering, + 'showtitle' => 0, + 'client_id' => 1, + 'language' => '*', + 'params' => $params, + 'assignment' => 0, + ]; + $app = Factory::getApplication(); /** @var \Joomla\Component\Modules\Administrator\Model\ModuleModel $model */ @@ -1494,6 +1545,71 @@ class Pkg_MokosuiteclientInstallerScript } } + /** + * Migrate monitor plugin params (base_url, signing_key) into the core plugin. + * The monitor plugin is retired but its config must survive in the core plugin. + */ + private function migrateMonitorParams(): void + { + try + { + $db = Factory::getDbo(); + + // Read core plugin params + $query = $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')); + $coreParams = json_decode((string) $db->setQuery($query)->loadResult(), true) ?: []; + + if (!empty($coreParams['_monitor_migrated'])) + { + return; + } + + // Read monitor plugin params (may already be gone) + $query = $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient_monitor')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')); + $monitorJson = (string) $db->setQuery($query)->loadResult(); + $monitorParams = json_decode($monitorJson, true) ?: []; + + $keyMap = [ + 'base_url' => 'monitor_base_url', + 'signing_key' => 'monitor_signing_key', + 'heartbeat_enabled' => 'heartbeat_enabled', + ]; + + foreach ($keyMap as $old => $new) + { + if (!empty($monitorParams[$old]) && empty($coreParams[$new])) + { + $coreParams[$new] = $monitorParams[$old]; + } + } + + $coreParams['_monitor_migrated'] = 1; + + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($coreParams))) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + )->execute(); + } + catch (\Throwable $e) + { + Log::add('Monitor param migration error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient'); + } + } + /** * Warn after install/update if no license key (dlid) is configured on the update site. */