saveDownloadKey(); try { $db = Factory::getDbo(); $db->setQuery("ALTER TABLE " . $db->quoteName('#__extensions') . " MODIFY " . $db->quoteName('element') . " VARCHAR(100) NOT NULL DEFAULT ''"); $db->execute(); } catch (\Throwable $e) { // Non-fatal — column may already have a default } } public function postflight($type, $parent) { // Migrate MokoWaaS database tables to MokoSuite naming $this->migrateWaasTables(); // Migrate params from old mokowaas extensions to mokosuite equivalents $this->migrateWaasExtensionParams(); // Remove legacy extensions and migrate settings before retiring $this->cleanupLegacyExtensions(); $this->migrateStandalonePlugins(); $this->removeRetiredExtensions(); $this->enablePlugin('system', 'mokosuite'); $this->enablePlugin('system', 'mokosuite_firewall'); $this->enablePlugin('system', 'mokosuite_tenant'); $this->enablePlugin('system', 'mokosuite_devtools'); $this->enablePlugin('system', 'mokosuite_offline'); $this->enablePlugin('webservices', 'mokosuite'); $this->enablePlugin('task', 'mokosuitedemo'); $this->enablePlugin('task', 'mokosuitesync'); $this->enablePlugin('task', 'mokosuite_tickets'); // Migrate params from core plugin to feature plugins (one-time) $this->migrateFeatureParams(); // Set up cpanel module on the admin dashboard $this->setupCpanelModule(); // Set up admin sidebar menu module $this->setupAdminMenuModule(); // Set up cache cleaner status bar module $this->setupCacheModule(); // Create Support portal menu item on frontend $this->setupSupportMenuItem(); // Set menu_icon params on submenu items (Joomla only renders img on level 1) $this->fixMenuIcons(); // Set up MokoSuite guided tours and unpublish Joomla defaults $this->setupGuidedTours(); // Mark MokoSuite extensions as protected (prevents disable/uninstall at framework level) $this->protectExtensions(); // Migrate all Moko update server URLs to new format $this->migrateUpdateServerUrls(); // Clean up stale/duplicate update sites $this->cleanupStaleUpdateSites(); // Restore download key saved in preflight $this->restoreDownloadKey(); // Fix orphaned update records (extension_id=0) $this->fixUpdateRecords(); // Trigger heartbeat registration $this->sendHeartbeat(); // Warn if no license key is configured $this->warnMissingLicenseKey(); } /** * Remove legacy/stale extension entries and filesystem remnants. * * The old standalone plugin was named "mokosuitebrand" (plg_system_mokosuitebrand). * After the rewrite into the pkg_mokosuite 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('mokosuitebrand'), $db->quote('plg_system_mokosuitebrand'), ]; // 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/mokosuitebrand', ]; foreach ($legacyDirs as $dir) { if (is_dir($dir)) { $this->rmdirRecursive($dir); } } if ($count > 0) { Factory::getApplication()->enqueueMessage( sprintf('Removed %d legacy MokoSuite extension(s).', $count), 'message' ); Log::add( sprintf('Cleaned up %d legacy MokoSuite extension entries', $count), Log::INFO, 'mokosuite' ); } } 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_mokosuite_monitor was merged into the core plugin in 02.32.00. * Health monitoring is now built into plg_system_mokosuite directly. * * @return void * * @since 02.32.00 */ private function migrateStandalonePlugins(): void { // Migrate standalone MokoJoomTOS plugin to MokoSuite Offline Bypass $migrations = [ ['old_element' => 'mokojoomtos', 'old_folder' => 'system', 'new_element' => 'mokosuite_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, 'mokosuite' ); } } catch (\Throwable $e) { Log::add('Standalone plugin migration error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } /** * 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' => 'mokosuite_monitor'], ['type' => 'plugin', 'folder' => 'system', 'element' => 'mokojoomtos'], ['type' => 'plugin', 'folder' => 'system', 'element' => 'mokoatsautomation'], ['type' => 'plugin', 'folder' => 'webservices', 'element' => 'mokodpcalendarapi'], ['type' => 'plugin', 'folder' => 'system', 'element' => 'mokogallerycalendar'], ]; 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 update site links and update sites $db->setQuery( $db->getQuery(true) ->select($db->quoteName('update_site_id')) ->from($db->quoteName('#__update_sites_extensions')) ->where($db->quoteName('extension_id') . ' = ' . $extId) ); $siteIds = $db->loadColumn(); $db->setQuery( $db->getQuery(true) ->delete($db->quoteName('#__update_sites_extensions')) ->where($db->quoteName('extension_id') . ' = ' . $extId) )->execute(); if (!empty($siteIds)) { $db->setQuery( $db->getQuery(true) ->delete($db->quoteName('#__updates')) ->where($db->quoteName('update_site_id') . ' IN (' . implode(',', $siteIds) . ')') )->execute(); $db->setQuery( $db->getQuery(true) ->delete($db->quoteName('#__update_sites')) ->where($db->quoteName('update_site_id') . ' IN (' . implode(',', $siteIds) . ')') )->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, 'mokosuite' ); } } catch (\Throwable $e) { Log::add('Retired extension cleanup error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } /** * 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 MokoSuite 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 MokoSuite elements: package, system plugin, component, // webservices plugins, task plugin $elements = [ $db->quote('pkg_mokosuite'), $db->quote('mokosuite'), $db->quote('mokosuite_firewall'), $db->quote('mokosuite_tenant'), $db->quote('mokosuite_devtools'), $db->quote('mokosuite_offline'), $db->quote('com_mokosuite'), $db->quote('mod_mokosuite_cpanel'), $db->quote('mokosuitedemo'), $db->quote('mokosuitesync'), $db->quote('mokosuite_tickets'), $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 MokoSuite extensions: ' . $e->getMessage(), Log::WARNING, 'jerror'); } } /** * Rewrite all Moko Consulting update server URLs from the old * raw/branch/main pattern to the new clean /updates.xml pattern. * * Old: https://git.mokoconsulting.tech/MokoConsulting/{repo}/raw/branch/main/updates.xml * New: https://git.mokoconsulting.tech/MokoConsulting/{repo}/updates.xml */ private function migrateUpdateServerUrls(): void { try { $db = Factory::getDbo(); $db->setQuery( "UPDATE " . $db->quoteName('#__update_sites') . " SET " . $db->quoteName('location') . " = REPLACE(" . $db->quoteName('location') . ", '/raw/branch/main/updates.xml', '/updates.xml')" . " WHERE " . $db->quoteName('location') . " LIKE " . $db->quote('%mokoconsulting.tech%/raw/branch/main/updates.xml') ); $db->execute(); $count = $db->getAffectedRows(); if ($count > 0) { Factory::getApplication()->enqueueMessage( sprintf('Migrated %d Moko update server URL(s) to new format.', $count), 'message' ); } } catch (\Throwable $e) { Log::add('Update server URL migration error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } /** * Remove stale and duplicate MokoSuite 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 fixUpdateRecords(): void { try { $db = Factory::getDbo(); // Link orphaned #__updates records to the installed extension $db->setQuery( "UPDATE " . $db->quoteName('#__updates') . " u" . " JOIN " . $db->quoteName('#__extensions') . " e" . " ON u.element = e.element AND u.type = e.type" . " SET u.extension_id = e.extension_id" . " WHERE u.extension_id = 0" . " AND u.element LIKE " . $db->quote('%mokosuite%') ); $db->execute(); } catch (\Throwable $e) { // Non-critical } } private function cleanupStaleUpdateSites(): void { try { $db = Factory::getDbo(); $dynamicUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/updates.xml'; // Find MokoSuite update sites (exclude MokoSuiteHQ and other Moko extensions) $query = $db->getQuery(true) ->select($db->quoteName(['update_site_id', 'location'])) ->from($db->quoteName('#__update_sites')) ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuite%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuite%') . ')') ->where($db->quoteName('name') . ' NOT LIKE ' . $db->quote('%MokoSuiteHQ%')) ->where($db->quoteName('location') . ' NOT LIKE ' . $db->quote('%MokoSuiteHQ%')); $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 MokoSuite update site(s).', $count), 'message' ); } } catch (\Throwable $e) { Log::add('Error cleaning up stale update sites: ' . $e->getMessage(), Log::WARNING, 'jerror'); } } /** * Backup all non-empty extra_query values from update sites. * * @return array Map of update_site_id => extra_query */ private function saveDownloadKey(): void { try { $db = Factory::getDbo(); // Check pkg_mokosuite first, then fall back to old pkg_mokowaas foreach (['pkg_mokosuite', 'pkg_mokowaas'] as $element) { $db->setQuery( $db->getQuery(true) ->select($db->quoteName('us.extra_query')) ->from($db->quoteName('#__update_sites', 'us')) ->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id') ->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id') ->where($db->quoteName('e.element') . ' = ' . $db->quote($element)) ->setLimit(1) ); $key = $db->loadResult(); if (!empty($key) && strpos($key, 'dlid=') !== false) { $this->savedDownloadKey = $key; break; } } } catch (\Throwable $e) {} } private function restoreDownloadKey(): void { if ($this->savedDownloadKey === null) { return; } try { $db = Factory::getDbo(); $db->setQuery( $db->getQuery(true) ->select($db->quoteName('us.update_site_id')) ->from($db->quoteName('#__update_sites', 'us')) ->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id') ->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id') ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuite')) ->setLimit(1) ); $siteId = (int) $db->loadResult(); if ($siteId > 0) { $db->setQuery( $db->getQuery(true) ->update($db->quoteName('#__update_sites')) ->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey)) ->where($db->quoteName('update_site_id') . ' = ' . $siteId) )->execute(); } } catch (\Throwable $e) {} } /** * Ensure the MokoSuite 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/MokoSuite/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('%MokoSuite%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuite%') . ')') ->where($db->quoteName('location') . ' != ' . $db->quote($staticUrl)) ); $db->execute(); // Enable all MokoSuite update sites $query = $db->getQuery(true) ->update($db->quoteName('#__update_sites')) ->set($db->quoteName('enabled') . ' = 1') ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuite%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuite%') . ')'); $db->setQuery($query); $db->execute(); } catch (\Throwable $e) { Log::add('Error enabling update server: ' . $e->getMessage(), Log::WARNING, 'jerror'); } } /** * Send heartbeat to the MokoSuite monitoring receiver. * * @return void * * @since 02.03.08 */ private function sendHeartbeat(): void { try { $db = Factory::getDbo(); // Get health token from core plugin $query = $db->getQuery(true) ->select($db->quoteName('params')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuite')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('folder') . ' = ' . $db->quote('system')); $coreParams = json_decode((string) $db->setQuery($query)->loadResult()); $healthToken = $coreParams->health_api_token ?? ''; if (empty($healthToken)) { 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('mokosuite_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 ?? '', '/'); // Fall back to manifest XML default if not yet saved in params if (empty($baseUrl)) { $manifestFile = JPATH_PLUGINS . '/system/mokosuite_monitor/mokosuite_monitor.xml'; if (is_file($manifestFile)) { $xml = simplexml_load_file($manifestFile); if ($xml) { foreach ($xml->xpath('//field[@name="base_url"]') as $field) { $baseUrl = rtrim((string) $field['default'], '/'); break; } } } } if (empty($baseUrl)) { return; } $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/'); $domain = parse_url($siteUrl, PHP_URL_HOST) ?: ''; $timestamp = time(); $payload = json_encode([ 'token' => $healthToken, 'domain' => $domain, 'site_name' => Factory::getConfig()->get('sitename', 'Joomla'), 'site_url' => $siteUrl, 'joomla_version' => (new \Joomla\CMS\Version())->getShortVersion(), 'php_version' => PHP_VERSION, 'timestamp' => $timestamp, ], JSON_UNESCAPED_SLASHES); $headers = ['Content-Type: application/json']; // RSA sign the request — fall back to manifest XML default $signingKeyB64 = $monitorParams->signing_key ?? ''; if (empty($signingKeyB64)) { $manifestFile = JPATH_PLUGINS . '/system/mokosuite_monitor/mokosuite_monitor.xml'; if (is_file($manifestFile)) { $xml = simplexml_load_file($manifestFile); if ($xml) { foreach ($xml->xpath('//field[@name="signing_key"]') as $field) { $signingKeyB64 = (string) $field['default']; break; } } } } if (!empty($signingKeyB64)) { $privateKeyPem = base64_decode($signingKeyB64); $privateKey = openssl_pkey_get_private($privateKeyPem); if ($privateKey !== false) { $message = $domain . '|' . $timestamp . '|' . $healthToken; $signature = ''; if (openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256)) { $headers[] = 'X-MokoSuite-Signature: ' . base64_encode($signature); $headers[] = 'X-MokoSuite-Timestamp: ' . $timestamp; } } } $endpoint = $baseUrl . '/api/index.php/v1/mokosuitehq/heartbeat'; $ch = curl_init($endpoint); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => $payload, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_FOLLOWLOCATION => true, 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('MokoSuiteHQ 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_mokosuite_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_mokosuite_cpanel')); $db->setQuery($query); if ((int) $db->loadResult() > 0) { return; } // Create the module instance on the cpanel position $module = (object) [ 'title' => 'MokoSuite', 'note' => '', 'content' => '', 'ordering' => 0, 'position' => 'top', 'checked_out' => null, 'checked_out_time' => null, 'publish_up' => null, 'publish_down' => null, 'published' => 1, 'module' => 'mod_mokosuite_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, 'mokosuite'); } } /** * Set up the MokoSuite admin sidebar menu module at position 0. */ private function setupAdminMenuModule(): void { try { $db = Factory::getDbo(); // Enable the module extension $db->setQuery( $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_mokosuite_menu')) )->execute(); // Check if module instance exists $db->setQuery( $db->getQuery(true) ->select('COUNT(*)') ->from($db->quoteName('#__modules')) ->where($db->quoteName('module') . ' = ' . $db->quote('mod_mokosuite_menu')) ); if ((int) $db->loadResult() > 0) { return; } $module = (object) [ 'title' => 'MokoSuite Menu', 'note' => '', 'content' => '', 'ordering' => 0, 'position' => 'menu', 'checked_out' => null, 'checked_out_time' => null, 'publish_up' => null, 'publish_down' => null, 'published' => 1, 'module' => 'mod_mokosuite_menu', 'access' => 3, 'showtitle' => 0, 'params' => '{}', 'client_id' => 1, 'language' => '*', ]; $db->insertObject('#__modules', $module, 'id'); if ((int) $module->id) { $db->insertObject('#__modules_menu', (object) ['moduleid' => (int) $module->id, 'menuid' => 0]); } } catch (\Throwable $e) { Log::add('Admin menu module setup error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } /** * Set up the cache cleaner module in the admin status bar position. */ private function setupCacheModule(): void { try { $db = Factory::getDbo(); // Enable the module extension $db->setQuery( $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_mokosuite_cache')) )->execute(); // Check if module instance exists $db->setQuery( $db->getQuery(true) ->select('COUNT(*)') ->from($db->quoteName('#__modules')) ->where($db->quoteName('module') . ' = ' . $db->quote('mod_mokosuite_cache')) ); if ((int) $db->loadResult() > 0) { return; } $module = (object) [ 'title' => 'MokoSuite Cache Cleaner', 'note' => '', 'content' => '', 'ordering' => 8, 'position' => 'status', 'checked_out' => null, 'checked_out_time' => null, 'publish_up' => null, 'publish_down' => null, 'published' => 1, 'module' => 'mod_mokosuite_cache', 'access' => 3, 'showtitle' => 0, 'params' => '{}', 'client_id' => 1, 'language' => '*', ]; $db->insertObject('#__modules', $module, 'id'); if ((int) $module->id) { $mm = (object) ['moduleid' => (int) $module->id, 'menuid' => 0]; $db->insertObject('#__modules_menu', $mm, 'moduleid'); } } catch (\Throwable $e) { Log::add('Cache module setup error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } /** * Joomla only renders the img column icon for level-1 menu items. * Submenu items (level 2) need menu_icon set in the params JSON. */ private function fixMenuIcons(): void { try { $db = Factory::getDbo(); $iconMap = [ 'class:cogs' => 'icon-cogs', 'class:puzzle-piece' => 'icon-puzzle-piece', 'class:headphones' => 'fa-solid fa-handshake-angle', 'class:file-code' => 'fa-solid fa-file-code', 'class:lock' => 'icon-lock', 'class:shield-alt' => 'icon-shield-alt', 'class:database' => 'icon-database', 'class:trash' => 'icon-trash', 'class:power-off' => 'icon-power-off', 'class:refresh' => 'icon-refresh', 'class:check-square' => 'icon-check-square', 'class:bolt' => 'icon-bolt', ]; // Find all MokoSuite component submenu items (including those linking to other components) $db->setQuery( $db->getQuery(true) ->select(['m.id', 'm.img', 'm.params']) ->from($db->quoteName('#__menu', 'm')) ->where('m.client_id = 1') ->where('m.level >= 2') ->where('m.parent_id IN (SELECT id FROM ' . $db->quoteName('#__menu') . ' WHERE client_id = 1 AND level = 1 AND link LIKE ' . $db->quote('%com_mokosuite%') . ')') ); foreach ($db->loadObjectList() as $item) { $icon = $iconMap[$item->img] ?? ''; if (!$icon) { continue; } $params = json_decode($item->params ?: '{}', true) ?: []; if (!empty($params['menu_icon'])) { continue; } $params['menu_icon'] = $icon; $db->setQuery( $db->getQuery(true) ->update($db->quoteName('#__menu')) ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params))) ->where($db->quoteName('id') . ' = ' . (int) $item->id) )->execute(); } } catch (\Throwable $e) { Log::add('Menu icon fix error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } /** * Unpublish default Joomla guided tours and create MokoSuite tours. * Re-enables the guided tours plugin if disabled. */ private function setupGuidedTours(): void { try { $db = Factory::getDbo(); // Re-enable guided tours plugin (may have been disabled) $db->setQuery( $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('enabled') . ' = 1') ->where($db->quoteName('element') . ' = ' . $db->quote('guidedtours')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) )->execute(); // Re-enable the guided tours module (shows our tours, not Joomla's) $db->setQuery( "UPDATE " . $db->quoteName('#__modules') . " SET published = 1, title = 'MokoSuite Tours'" . " WHERE module = 'mod_guidedtours'" ); $db->execute(); // Override the guided tours module language string $overridePath = JPATH_ADMINISTRATOR . '/language/overrides/en-GB.override.ini'; $overrides = file_exists($overridePath) ? parse_ini_file($overridePath) : []; if (empty($overrides['MOD_GUIDEDTOURS'])) { $overrides['MOD_GUIDEDTOURS'] = 'MokoSuite Tours'; $overrides['MOD_GUIDEDTOURS_TITLE'] = 'MokoSuite Tours'; $lines = []; foreach ($overrides as $k => $v) { $lines[] = $k . '="' . str_replace('"', '\"', $v) . '"'; } file_put_contents($overridePath, implode("\n", $lines) . "\n"); } // Unpublish all default Joomla tours $db->setQuery( "UPDATE " . $db->quoteName('#__guidedtours') . " SET published = 0" . " WHERE " . $db->quoteName('uid') . " LIKE 'joomla-%'" ); $db->execute(); // Define MokoSuite tours $tours = [ [ 'uid' => 'mokosuite-welcome', 'title' => 'Welcome to MokoSuite', 'desc' => 'Get started with the MokoSuite Admin Tools Suite. This tour shows you the key areas of your admin dashboard.', 'url' => 'administrator/index.php?option=com_mokosuite', 'steps' => [ ['title' => 'MokoSuite Dashboard', 'desc' => 'This is your MokoSuite control center. You can see site info, feature plugins, WAF activity, and quick actions all in one place.', 'target' => '#mokosuite-dashboard', 'type' => 0], ['title' => 'Site Information', 'desc' => 'The info bar shows your Joomla version, PHP version, database type, and debug/offline status at a glance.', 'target' => '.mokosuite-info-bar', 'type' => 0], ['title' => 'Quick Actions', 'desc' => 'Use these buttons to clear cache, check updates, manage extensions, and perform common admin tasks with one click.', 'target' => '#mokosuite-btn-cache', 'type' => 0], ['title' => 'Feature Plugins', 'desc' => 'MokoSuite features are split into toggleable plugins. Enable or disable security, tenant restrictions, developer tools, and more from here.', 'target' => '.mokosuite-plugin-grid', 'type' => 0], ['title' => 'MokoSuite Menu', 'desc' => 'The MokoSuite sidebar menu gives you quick access to all admin tools — Helpdesk, Extensions, WAF Log, Database Tools, and more.', 'target' => '.mokosuite-admin-menu, [class*="mokosuite"]', 'type' => 0], ], ], [ 'uid' => 'mokosuite-firewall', 'title' => 'MokoSuite Firewall Setup', 'desc' => 'Configure the Web Application Firewall to protect your site from common attacks.', 'url' => 'administrator/index.php?option=com_plugins&task=plugin.edit&filter[search]=mokosuite_firewall', 'steps' => [ ['title' => 'Firewall Plugin', 'desc' => 'The MokoSuite Firewall provides 10 security shields including SQL injection, XSS, and malicious user agent detection.', 'target' => '', 'type' => 0], ['title' => 'WAF Shields', 'desc' => 'Enable or disable individual WAF shields. Each shield protects against a specific attack vector. All shields are enabled by default.', 'target' => '', 'type' => 0], ['title' => 'Security Headers', 'desc' => 'Configure HTTP security headers like X-Frame-Options, Content-Security-Policy, and HSTS to harden your site against browser-based attacks.', 'target' => '', 'type' => 0], ['title' => 'IP Blocklist', 'desc' => 'Block specific IP addresses, CIDR ranges, or wildcard patterns. The auto-ban feature automatically blocks IPs that trigger too many WAF alerts.', 'target' => '', 'type' => 0], ], ], [ 'uid' => 'mokosuite-helpdesk', 'title' => 'MokoSuite Helpdesk', 'desc' => 'Learn how to manage support tickets, categories, and automation rules.', 'url' => 'administrator/index.php?option=com_mokosuite&view=tickets', 'steps' => [ ['title' => 'Ticket List', 'desc' => 'View all support tickets with status, priority, SLA tracking, and assignment. Filter by status or search to find specific tickets.', 'target' => '', 'type' => 0], ['title' => 'Create a Ticket', 'desc' => 'Click the New button to create a support ticket. Assign a category, priority, and optional SLA deadline.', 'target' => '', 'type' => 0], ['title' => 'Ticket Automation', 'desc' => 'Set up automation rules that trigger on ticket events (new ticket, status change) or Joomla events (user login, registration). Automate assignment, notifications, and status changes.', 'target' => '', 'type' => 0], ], ], [ 'uid' => 'mokosuite-extensions', 'title' => 'Moko Extensions Manager', 'desc' => 'Browse and install Moko Consulting extensions from the built-in catalog.', 'url' => 'administrator/index.php?option=com_mokosuite&view=extensions', 'steps' => [ ['title' => 'Extension Catalog', 'desc' => 'Browse all available Moko Consulting extensions. Each card shows the extension name, description, install status, and current version.', 'target' => '', 'type' => 0], ['title' => 'Install Extensions', 'desc' => 'Click Install to add an extension from the Moko Consulting repository. Updates are handled through Joomla\'s standard update system.', 'target' => '', 'type' => 0], ], ], ]; foreach ($tours as $tourDef) { // Check if tour already exists $db->setQuery( $db->getQuery(true) ->select('id') ->from($db->quoteName('#__guidedtours')) ->where($db->quoteName('uid') . ' = ' . $db->quote($tourDef['uid'])) ); if ($db->loadResult()) { continue; } $tour = (object) [ 'title' => $tourDef['title'], 'uid' => $tourDef['uid'], 'description' => $tourDef['desc'], 'extensions' => '', 'url' => $tourDef['url'], 'created' => date('Y-m-d H:i:s'), 'created_by' => 0, 'modified' => date('Y-m-d H:i:s'), 'modified_by' => 0, 'published' => 1, 'language' => '*', 'note' => 'MokoSuite', 'access' => 3, 'ordering' => 0, 'autostart' => 0, ]; $db->insertObject('#__guidedtours', $tour, 'id'); $tourId = (int) $tour->id; foreach ($tourDef['steps'] as $i => $stepDef) { $step = (object) [ 'tour_id' => $tourId, 'title' => $stepDef['title'], 'description' => $stepDef['desc'], 'target' => $stepDef['target'], 'type' => $stepDef['type'], 'interactive_type' => 1, 'url' => '', 'position' => 'bottom', 'ordering' => $i + 1, 'published' => 1, 'created' => date('Y-m-d H:i:s'), 'created_by' => 0, 'modified' => date('Y-m-d H:i:s'), 'modified_by' => 0, 'language' => '*', 'note' => '', 'params' => '{}', ]; $db->insertObject('#__guidedtour_steps', $step, 'id'); } } } catch (\Throwable $e) { Log::add('Guided tours setup error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } /** * 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_mokosuite&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_mokosuite')) ->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_mokosuite&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_mokosuite&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, 'mokosuite'); } } /** * 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('mokosuite')) ->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 = [ 'mokosuite_firewall' => $firewallKeys, 'mokosuite_tenant' => $tenantKeys, 'mokosuite_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('mokosuite')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) )->execute(); Factory::getApplication()->enqueueMessage( 'MokoSuite: migrated settings to feature plugins (Firewall, Tenant, DevTools).', 'message' ); } catch (\Throwable $e) { Log::add('Feature param migration error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } /** * Warn after install/update if no license key (dlid) is configured on the update site. */ private function warnMissingLicenseKey(): void { try { $db = Factory::getDbo(); $app = Factory::getApplication(); $query = $db->getQuery(true) ->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')]) ->from($db->quoteName('#__update_sites')) ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuite%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuite%') . ')') ->setLimit(1); $db->setQuery($query); $site = $db->loadObject(); if ($site) { $extraQuery = (string) ($site->extra_query ?? ''); if (!empty($extraQuery) && strpos($extraQuery, 'dlid=') !== false) { parse_str($extraQuery, $parsed); if (!empty($parsed['dlid'])) { return; } } $editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id; } else { $editUrl = 'index.php?option=com_installer&view=updatesites'; } $app->enqueueMessage( 'Moko Consulting License Key Required — ' . 'No download key is configured. Updates will not be available until a valid license key is entered. ' . 'Enter License Key', 'warning' ); } catch (\Throwable $e) { // Silent } } /** * Migrate MokoWaaS database tables to MokoSuite naming. * * For each table: create new mokosuite_* table → copy data from mokowaas_* → drop old table. * Safe to run multiple times — skips tables that don't exist or are already migrated. * * @return void * * @since 02.35.00 */ private function migrateWaasTables(): void { $tableMap = [ 'mokowaas_ticket_categories' => 'mokosuite_ticket_categories', 'mokowaas_tickets' => 'mokosuite_tickets', 'mokowaas_ticket_replies' => 'mokosuite_ticket_replies', 'mokowaas_ticket_canned' => 'mokosuite_ticket_canned', 'mokowaas_ticket_automation' => 'mokosuite_ticket_automation', 'mokowaas_consent_log' => 'mokosuite_consent_log', 'mokowaas_data_requests' => 'mokosuite_data_requests', 'mokowaas_retention_policies' => 'mokosuite_retention_policies', 'mokowaas_waf_log' => 'mokosuite_waf_log', ]; try { $db = Factory::getDbo(); $prefix = $db->getPrefix(); $migrated = 0; foreach ($tableMap as $oldSuffix => $newSuffix) { $oldTable = $prefix . $oldSuffix; $newTable = $prefix . $newSuffix; // Check if old table exists $db->setQuery("SHOW TABLES LIKE " . $db->quote($oldTable)); if (!$db->loadResult()) { continue; } // Create new table with same structure if it doesn't exist $db->setQuery("SHOW TABLES LIKE " . $db->quote($newTable)); if (!$db->loadResult()) { $db->setQuery("CREATE TABLE " . $db->quoteName('#__' . $newSuffix) . " LIKE " . $db->quoteName('#__' . $oldSuffix)); $db->execute(); } // Copy data from old to new (skip duplicates on primary key) $db->setQuery("INSERT IGNORE INTO " . $db->quoteName('#__' . $newSuffix) . " SELECT * FROM " . $db->quoteName('#__' . $oldSuffix)); $db->execute(); $copied = $db->getAffectedRows(); // Drop old table $db->setQuery("DROP TABLE IF EXISTS " . $db->quoteName('#__' . $oldSuffix)); $db->execute(); $migrated++; Log::add( sprintf('Migrated table %s → %s (%d rows)', $oldSuffix, $newSuffix, $copied), Log::INFO, 'mokosuite' ); } if ($migrated > 0) { Factory::getApplication()->enqueueMessage( sprintf('Migrated %d MokoWaaS database table(s) to MokoSuite naming.', $migrated), 'message' ); } } catch (\Throwable $e) { Log::add('Table migration error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } /** * Migrate params from old mokowaas extension entries to mokosuite equivalents. * * Copies params where the new extension has empty/default params, then deletes * the old extension entries and their filesystem remnants. * * @return void * * @since 02.35.00 */ private function migrateWaasExtensionParams(): void { // [old_element, old_folder, new_element, new_folder, type] $map = [ ['mokowaas', 'system', 'mokosuite', 'system', 'plugin'], ['mokowaas_firewall', 'system', 'mokosuite_firewall', 'system', 'plugin'], ['mokowaas_tenant', 'system', 'mokosuite_tenant', 'system', 'plugin'], ['mokowaas_devtools', 'system', 'mokosuite_devtools', 'system', 'plugin'], ['mokowaas_offline', 'system', 'mokosuite_offline', 'system', 'plugin'], ['mokowaas_monitor', 'system', 'mokosuite_monitor', 'system', 'plugin'], ['mokowaas', 'webservices', 'mokosuite', 'webservices', 'plugin'], ['mokowaassync', 'task', 'mokosuitesync', 'task', 'plugin'], ['mokowaasdemo', 'task', 'mokosuitedemo', 'task', 'plugin'], ['mokowaas_tickets', 'task', 'mokosuite_tickets', 'task', 'plugin'], ['com_mokowaas', '', 'com_mokosuite', '', 'component'], ['mod_mokowaas_cpanel', '', 'mod_mokosuite_cpanel', '', 'module'], ['mod_mokowaas_menu', '', 'mod_mokosuite_menu', '', 'module'], ['mod_mokowaas_cache', '', 'mod_mokosuite_cache', '', 'module'], ['mod_mokowaas_categories', '', 'mod_mokosuite_categories', '', 'module'], ['pkg_mokowaas', '', 'pkg_mokosuite', '', 'package'], ]; try { $db = Factory::getDbo(); $migrated = 0; foreach ($map as [$oldEl, $oldFolder, $newEl, $newFolder, $type]) { // Find old extension $query = $db->getQuery(true) ->select([$db->quoteName('extension_id'), $db->quoteName('params')]) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = ' . $db->quote($oldEl)) ->where($db->quoteName('type') . ' = ' . $db->quote($type)); if ($type === 'plugin') { $query->where($db->quoteName('folder') . ' = ' . $db->quote($oldFolder)); } $db->setQuery($query); $old = $db->loadObject(); if (!$old) { continue; } $oldParams = (string) ($old->params ?? '{}'); // Copy params to new extension only if new has empty params if ($oldParams !== '' && $oldParams !== '{}' && $oldParams !== '[]') { $newQuery = $db->getQuery(true) ->select($db->quoteName('params')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = ' . $db->quote($newEl)) ->where($db->quoteName('type') . ' = ' . $db->quote($type)); if ($type === 'plugin') { $newQuery->where($db->quoteName('folder') . ' = ' . $db->quote($newFolder)); } $db->setQuery($newQuery); $newParams = (string) $db->loadResult(); if (empty($newParams) || $newParams === '{}' || $newParams === '[]') { $updateQuery = $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('params') . ' = ' . $db->quote($oldParams)) ->where($db->quoteName('element') . ' = ' . $db->quote($newEl)) ->where($db->quoteName('type') . ' = ' . $db->quote($type)); if ($type === 'plugin') { $updateQuery->where($db->quoteName('folder') . ' = ' . $db->quote($newFolder)); } $db->setQuery($updateQuery)->execute(); Log::add( sprintf('Migrated params from %s to %s', $oldEl, $newEl), Log::INFO, 'mokosuite' ); } } // Unprotect old extension $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 update site links $db->setQuery( $db->getQuery(true) ->select($db->quoteName('update_site_id')) ->from($db->quoteName('#__update_sites_extensions')) ->where($db->quoteName('extension_id') . ' = ' . (int) $old->extension_id) ); $siteIds = $db->loadColumn(); $db->setQuery( $db->getQuery(true) ->delete($db->quoteName('#__update_sites_extensions')) ->where($db->quoteName('extension_id') . ' = ' . (int) $old->extension_id) )->execute(); if (!empty($siteIds)) { $db->setQuery( $db->getQuery(true) ->delete($db->quoteName('#__updates')) ->where($db->quoteName('update_site_id') . ' IN (' . implode(',', array_map('intval', $siteIds)) . ')') )->execute(); $db->setQuery( $db->getQuery(true) ->delete($db->quoteName('#__update_sites')) ->where($db->quoteName('update_site_id') . ' IN (' . implode(',', array_map('intval', $siteIds)) . ')') )->execute(); } // Delete old extension entry $db->setQuery( $db->getQuery(true) ->delete($db->quoteName('#__extensions')) ->where($db->quoteName('extension_id') . ' = ' . (int) $old->extension_id) )->execute(); // Remove old plugin/module filesystem remnants $dir = null; if ($type === 'plugin') { $dir = JPATH_PLUGINS . '/' . $oldFolder . '/' . $oldEl; } elseif ($type === 'module') { $dir = JPATH_ADMINISTRATOR . '/modules/' . $oldEl; } elseif ($type === 'component') { // Components have admin + site dirs foreach ([JPATH_ADMINISTRATOR . '/components/' . $oldEl, JPATH_SITE . '/components/' . $oldEl] as $cDir) { if (is_dir($cDir)) { $this->rmdirRecursive($cDir); } } } if ($dir && is_dir($dir)) { $this->rmdirRecursive($dir); } $migrated++; } if ($migrated > 0) { Factory::getApplication()->enqueueMessage( sprintf('Migrated params from %d MokoWaaS extension(s) and removed old entries.', $migrated), 'message' ); } } catch (\Throwable $e) { Log::add('Extension param migration error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } }