cleanupLegacyExtensions(); $this->removeRetiredExtensions(); $this->migrateStandalonePlugins(); $this->enablePlugin('system', 'mokowaas'); $this->enablePlugin('system', 'mokowaas_firewall'); $this->enablePlugin('system', 'mokowaas_tenant'); $this->enablePlugin('system', 'mokowaas_devtools'); $this->enablePlugin('system', 'mokowaas_offline'); $this->enablePlugin('webservices', 'mokowaas'); $this->enablePlugin('task', 'mokowaasdemo'); $this->enablePlugin('task', 'mokowaassync'); // Migrate params from core plugin to feature plugins (one-time) $this->migrateFeatureParams(); // Set up cpanel module on the admin dashboard $this->setupCpanelModule(); // Create Support portal menu item on frontend $this->setupSupportMenuItem(); // 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(); } /** * Remove legacy/stale extension entries and filesystem remnants. * * The old standalone plugin was named "mokowaasbrand" (plg_system_mokowaasbrand). * After the rewrite into the pkg_mokowaas package, the old entries and files * may linger — especially on sites restored from old backups. * * @return void * * @since 02.21.00 */ private function cleanupLegacyExtensions(): void { try { $db = Factory::getDbo(); // Legacy element names to remove from #__extensions $legacy = [ $db->quote('mokowaasbrand'), $db->quote('plg_system_mokowaasbrand'), ]; // Delete from #__extensions $query = $db->getQuery(true) ->delete($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' IN (' . implode(',', $legacy) . ')'); $db->setQuery($query); $affected = $db->execute(); $count = $db->getAffectedRows(); // Remove legacy plugin files from the filesystem $legacyDirs = [ JPATH_PLUGINS . '/system/mokowaasbrand', ]; foreach ($legacyDirs as $dir) { if (is_dir($dir)) { $this->rmdirRecursive($dir); } } if ($count > 0) { Factory::getApplication()->enqueueMessage( sprintf('Removed %d legacy MokoWaaS extension(s).', $count), 'message' ); Log::add( sprintf('Cleaned up %d legacy MokoWaaS extension entries', $count), Log::INFO, 'mokowaas' ); } } catch (\Throwable $e) { Log::add('Legacy cleanup error: ' . $e->getMessage(), Log::WARNING, 'jerror'); } } /** * Remove extensions that have been retired and merged into core. * * plg_system_mokowaas_monitor was merged into the core plugin in 02.32.00. * Health monitoring is now built into plg_system_mokowaas directly. * * @return void * * @since 02.32.00 */ private function migrateStandalonePlugins(): void { // Migrate standalone MokoJoomTOS plugin to MokoWaaS Offline Bypass $migrations = [ ['old_element' => 'mokojoomtos', 'old_folder' => 'system', 'new_element' => 'mokowaas_offline', 'new_folder' => 'system'], ]; try { $db = Factory::getDbo(); foreach ($migrations as $m) { // Check if old plugin exists $query = $db->getQuery(true) ->select([$db->quoteName('extension_id'), $db->quoteName('params')]) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = ' . $db->quote($m['old_element'])) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('folder') . ' = ' . $db->quote($m['old_folder'])); $db->setQuery($query); $old = $db->loadObject(); if (!$old) { continue; } $oldParams = $old->params ?? '{}'; // Copy params to new plugin (only if new plugin has empty params) $query = $db->getQuery(true) ->select($db->quoteName('params')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = ' . $db->quote($m['new_element'])) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('folder') . ' = ' . $db->quote($m['new_folder'])); $db->setQuery($query); $newParams = (string) $db->loadResult(); if (empty($newParams) || $newParams === '{}' || $newParams === '[]') { $db->setQuery( $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('params') . ' = ' . $db->quote($oldParams)) ->where($db->quoteName('element') . ' = ' . $db->quote($m['new_element'])) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('folder') . ' = ' . $db->quote($m['new_folder'])) )->execute(); Factory::getApplication()->enqueueMessage( sprintf('Migrated settings from %s to %s.', $m['old_element'], $m['new_element']), 'message' ); } // Unprotect old plugin $db->setQuery( $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('protected') . ' = 0') ->where($db->quoteName('extension_id') . ' = ' . (int) $old->extension_id) )->execute(); // Remove old extension record $db->setQuery( $db->getQuery(true) ->delete($db->quoteName('#__extensions')) ->where($db->quoteName('extension_id') . ' = ' . (int) $old->extension_id) )->execute(); // Remove old update site entries $db->setQuery( $db->getQuery(true) ->delete($db->quoteName('#__update_sites_extensions')) ->where($db->quoteName('extension_id') . ' = ' . (int) $old->extension_id) )->execute(); // Remove old files $dir = JPATH_PLUGINS . '/' . $m['old_folder'] . '/' . $m['old_element']; if (is_dir($dir)) { $this->rmdirRecursive($dir); } Factory::getApplication()->enqueueMessage( sprintf('Removed standalone %s plugin (replaced by %s).', $m['old_element'], $m['new_element']), 'message' ); Log::add( sprintf('Migrated %s → %s and removed old plugin', $m['old_element'], $m['new_element']), Log::INFO, 'mokowaas' ); } } catch (\Throwable $e) { Log::add('Standalone plugin migration error: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); } } /** * Remove extensions that have been retired and merged into core. * * @return void * * @since 02.32.00 */ private function removeRetiredExtensions(): void { $retired = [ ['type' => 'plugin', 'folder' => 'system', 'element' => 'mokowaas_monitor'], ]; try { $db = Factory::getDbo(); foreach ($retired as $ext) { // Check if installed $query = $db->getQuery(true) ->select($db->quoteName('extension_id')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('type') . ' = ' . $db->quote($ext['type'])) ->where($db->quoteName('folder') . ' = ' . $db->quote($ext['folder'])) ->where($db->quoteName('element') . ' = ' . $db->quote($ext['element'])); $db->setQuery($query); $extId = (int) $db->loadResult(); if (!$extId) { continue; } // Unprotect so Joomla allows removal $db->setQuery( $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('protected') . ' = 0') ->where($db->quoteName('extension_id') . ' = ' . $extId) )->execute(); // Remove extension record $db->setQuery( $db->getQuery(true) ->delete($db->quoteName('#__extensions')) ->where($db->quoteName('extension_id') . ' = ' . $extId) )->execute(); // Remove files $dir = JPATH_PLUGINS . '/' . $ext['folder'] . '/' . $ext['element']; if (is_dir($dir)) { $this->rmdirRecursive($dir); } Factory::getApplication()->enqueueMessage( sprintf('Removed retired extension: %s/%s', $ext['folder'], $ext['element']), 'message' ); Log::add( sprintf('Removed retired extension %s/%s (ID %d)', $ext['folder'], $ext['element'], $extId), Log::INFO, 'mokowaas' ); } } catch (\Throwable $e) { Log::add('Retired extension cleanup error: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); } } /** * Recursively remove a directory. * * @param string $dir Directory path * * @return void * * @since 02.21.00 */ private function rmdirRecursive(string $dir): void { $items = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($items as $item) { if ($item->isDir()) { @rmdir($item->getPathname()); } else { @unlink($item->getPathname()); } } @rmdir($dir); } /** * Enable a plugin by group and element. * * @param string $group Plugin group * @param string $element Plugin element name * * @return void * * @since 2.2.0 */ private function enablePlugin(string $group, string $element): void { try { $db = Factory::getDbo(); $query = $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->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($element)); $db->setQuery($query); $db->execute(); } catch (\Throwable $e) { Log::add('Error enabling plugin ' . $group . '/' . $element . ': ' . $e->getMessage(), Log::WARNING, 'jerror'); } } /** * Set the protected flag on all MokoWaaS extensions. * * Joomla's protected flag prevents disabling and uninstalling at the * framework level — no plugin-side interception needed. * * @return void * * @since 02.03.10 */ private function protectExtensions(): void { try { $db = Factory::getDbo(); // All MokoWaaS elements: package, system plugin, component, // webservices plugins, task plugin $elements = [ $db->quote('pkg_mokowaas'), $db->quote('mokowaas'), $db->quote('mokowaas_firewall'), $db->quote('mokowaas_tenant'), $db->quote('mokowaas_devtools'), $db->quote('mokowaas_offline'), $db->quote('com_mokowaas'), $db->quote('mod_mokowaas_cpanel'), $db->quote('mokowaasdemo'), $db->quote('mokowaassync'), $db->quote('perfectpublisher'), $db->quote('mokoonyx'), ]; $query = $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('protected') . ' = 1') ->set($db->quoteName('locked') . ' = 0') ->where($db->quoteName('element') . ' IN (' . implode(',', $elements) . ')'); $db->setQuery($query); $db->execute(); // Ensure update server stays enabled $this->enableUpdateServer(); } catch (\Throwable $e) { Log::add('Error protecting MokoWaaS extensions: ' . $e->getMessage(), Log::WARNING, 'jerror'); } } /** * 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.31.00 */ private function cleanupStaleUpdateSites(): void { try { $db = Factory::getDbo(); $dynamicUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/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. * * Migrates legacy static URLs (raw/branch/main/updates.xml) to the * dynamic MokoGitea update feed, and syncs the license key from * plugin params into extra_query so Joomla sends it as dlid. * * @return void * * @since 02.21.00 */ private function enableUpdateServer(): void { try { $db = Factory::getDbo(); $staticUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml'; // Migrate old dynamic URL to static raw file URL $db->setQuery( $db->getQuery(true) ->update($db->quoteName('#__update_sites')) ->set($db->quoteName('location') . ' = ' . $db->quote($staticUrl)) ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoWaaS%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoWaaS%') . ')') ->where($db->quoteName('location') . ' != ' . $db->quote($staticUrl)) ); $db->execute(); // Enable all MokoWaaS update sites $query = $db->getQuery(true) ->update($db->quoteName('#__update_sites')) ->set($db->quoteName('enabled') . ' = 1') ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoWaaS%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoWaaS%') . ')'); $db->setQuery($query); $db->execute(); } catch (\Throwable $e) { Log::add('Error enabling update server: ' . $e->getMessage(), Log::WARNING, 'jerror'); } } /** * Send heartbeat to the MokoWaaS monitoring receiver. * * @return void * * @since 02.03.08 */ private function sendHeartbeat(): void { try { $db = Factory::getDbo(); $query = $db->getQuery(true) ->select($db->quoteName('params')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('folder') . ' = ' . $db->quote('system')); $params = json_decode((string) $db->setQuery($query)->loadResult()); $healthToken = $params->health_api_token ?? ''; if (empty($healthToken)) { return; } $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/'); $siteName = Factory::getConfig()->get('sitename', 'Joomla'); $payload = json_encode([ 'site_url' => $siteUrl, 'site_name' => $siteName, 'health_token' => $healthToken, 'action' => 'register', ], JSON_UNESCAPED_SLASHES); $ch = curl_init('https://bench.mokoconsulting.tech/api/waas-heartbeat/register'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'X-MokoWaaS-Key: moko-waas-hb-2026-x9k4m', ]); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $response = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code >= 200 && $code < 300) { Factory::getApplication()->enqueueMessage('Grafana heartbeat: site registered', 'message'); } } catch (\Throwable $e) { // Silent failure — heartbeat is non-critical } } /** * One-time migration of params from the monolithic core plugin to * the new feature plugins. Copies security, tenant, and dev params. * * @return void * * @since 02.32.00 */ private function setupCpanelModule(): void { try { $db = Factory::getDbo(); // Enable the module $query = $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('enabled') . ' = 1') ->where($db->quoteName('type') . ' = ' . $db->quote('module')) ->where($db->quoteName('element') . ' = ' . $db->quote('mod_mokowaas_cpanel')); $db->setQuery($query); $db->execute(); // Check if a module instance already exists in #__modules $query = $db->getQuery(true) ->select('COUNT(*)') ->from($db->quoteName('#__modules')) ->where($db->quoteName('module') . ' = ' . $db->quote('mod_mokowaas_cpanel')); $db->setQuery($query); if ((int) $db->loadResult() > 0) { return; } // Create the module instance on the cpanel position $module = (object) [ 'title' => 'MokoWaaS', 'note' => '', 'content' => '', 'ordering' => 0, 'position' => 'top', 'checked_out' => null, 'checked_out_time' => null, 'publish_up' => null, 'publish_down' => null, 'published' => 1, 'module' => 'mod_mokowaas_cpanel', 'access' => 6, // Super Users only 'showtitle' => 0, 'params' => '{"show_health":"1","show_plugins":"1"}', 'client_id' => 1, // Administrator 'language' => '*', ]; $db->insertObject('#__modules', $module, 'id'); $moduleId = (int) $module->id; if ($moduleId) { // Assign to all admin pages $map = (object) [ 'moduleid' => $moduleId, 'menuid' => 0, // 0 = all pages ]; $db->insertObject('#__modules_menu', $map); } } catch (\Throwable $e) { Log::add('CPanel module setup error: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); } } /** * Create a "Support" menu item on the frontend main menu. */ private function setupSupportMenuItem(): void { try { $db = Factory::getDbo(); $db->setQuery( $db->getQuery(true) ->select('COUNT(*)') ->from($db->quoteName('#__menu')) ->where($db->quoteName('link') . ' LIKE ' . $db->quote('%com_mokowaas&view=tickets%')) ->where($db->quoteName('client_id') . ' = 0') ); if ((int) $db->loadResult() > 0) { return; } $db->setQuery( $db->getQuery(true) ->select($db->quoteName('extension_id')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('type') . ' = ' . $db->quote('component')) ); $componentId = (int) $db->loadResult(); if (!$componentId) { return; } $db->setQuery("SELECT id FROM #__menu WHERE menutype = '' AND level = 0 AND client_id = 0 LIMIT 1"); $rootId = (int) $db->loadResult() ?: 1; $db->setQuery('SELECT MAX(rgt) FROM #__menu WHERE client_id = 0'); $maxRgt = (int) $db->loadResult(); $item = (object) [ 'menutype' => 'mainmenu', 'title' => 'Support', 'alias' => 'support', 'note' => '', 'path' => 'support', 'link' => 'index.php?option=com_mokowaas&view=tickets', 'type' => 'component', 'published' => 1, 'parent_id' => $rootId, 'level' => 1, 'component_id' => $componentId, 'checked_out' => null, 'checked_out_time' => null, 'browserNav' => 0, 'access' => 2, 'img' => '', 'template_style_id' => 0, 'params' => '{}', 'lft' => $maxRgt + 1, 'rgt' => $maxRgt + 2, 'home' => 0, 'language' => '*', 'client_id' => 0, ]; $db->insertObject('#__menu', $item, 'id'); $supportId = (int) $item->id; // Create "Submit a Ticket" child menu item if ($supportId) { $db->setQuery('SELECT MAX(rgt) FROM #__menu WHERE client_id = 0'); $maxRgt2 = (int) $db->loadResult(); $child = (object) [ 'menutype' => 'mainmenu', 'title' => 'Submit a Ticket', 'alias' => 'submit-ticket', 'note' => '', 'path' => 'support/submit-ticket', 'link' => 'index.php?option=com_mokowaas&view=tickets&layout=submit', 'type' => 'component', 'published' => 1, 'parent_id' => $supportId, 'level' => 2, 'component_id' => $componentId, 'checked_out' => null, 'checked_out_time' => null, 'browserNav' => 0, 'access' => 2, 'img' => '', 'template_style_id' => 0, 'params' => '{}', 'lft' => $maxRgt2 + 1, 'rgt' => $maxRgt2 + 2, 'home' => 0, 'language' => '*', 'client_id' => 0, ]; $db->insertObject('#__menu', $child, 'id'); } } catch (\Throwable $e) { Log::add('Support menu setup error: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); } } /** * One-time migration of params from the monolithic core plugin to * the new feature plugins. Copies security, tenant, and dev params. * * @return void * * @since 02.32.00 */ private function migrateFeatureParams(): 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('mokowaas')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('folder') . ' = ' . $db->quote('system')); $db->setQuery($query); $coreParamsJson = (string) $db->loadResult(); if (empty($coreParamsJson) || $coreParamsJson === '{}') { return; } $core = json_decode($coreParamsJson, true); if (empty($core)) { return; } // Check migration marker if (!empty($core['_params_migrated_032'])) { return; } // Firewall params $firewallKeys = [ 'force_https', 'admin_session_timeout', 'trusted_ips', 'password_min_length', 'password_require_uppercase', 'password_require_number', 'password_require_special', 'upload_allowed_types', 'upload_max_size_mb', ]; // Tenant params $tenantKeys = [ 'restrict_installer', 'allow_extension_updates', 'hide_sysinfo', 'restrict_global_config', 'restrict_template_editing', 'disable_install_url', 'hidden_menu_items', ]; // DevTools params $devtoolsKeys = ['dev_mode', 'reset_hits', 'delete_versions']; $migrations = [ 'mokowaas_firewall' => $firewallKeys, 'mokowaas_tenant' => $tenantKeys, 'mokowaas_devtools' => $devtoolsKeys, ]; foreach ($migrations as $element => $keys) { $featureParams = []; foreach ($keys as $key) { if (isset($core[$key])) { $featureParams[$key] = $core[$key]; } } if (empty($featureParams)) { continue; } $db->setQuery( $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($featureParams))) ->where($db->quoteName('element') . ' = ' . $db->quote($element)) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) )->execute(); } // Set migration marker on core plugin $core['_params_migrated_032'] = 1; $db->setQuery( $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($core))) ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) )->execute(); Factory::getApplication()->enqueueMessage( 'MokoWaaS: migrated settings to feature plugins (Firewall, Tenant, DevTools).', 'message' ); } catch (\Throwable $e) { Log::add('Feature param migration error: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); } } }