[ 'icon' => 'icon-shield-alt', 'category' => 'core', 'label' => 'Core', 'description' => 'Heartbeat, health monitoring, site aliases, extension coordination, and download key preservation.', 'protected' => true, 'configure_only' => false, ], 'mokosuiteclient_firewall' => [ 'icon' => 'icon-lock', 'category' => 'security', 'label' => 'Firewall', 'description' => 'Web Application Firewall — SQLi, XSS, RFI, DFI shields, IP blocklist, admin secret URL, file protection.', 'protected' => false, 'configure_only' => false, ], 'mokosuiteclient_tenant' => [ 'icon' => 'icon-users', 'category' => 'security', 'label' => 'Tenant Restrictions', 'description' => 'Installer, sysinfo, config, and template access restrictions for non-master users.', 'protected' => false, 'configure_only' => false, ], 'mokosuiteclient_offline' => [ 'icon' => 'icon-globe', 'category' => 'security', 'label' => 'Offline Bypass', 'description' => 'Keep selected pages (TOS, Privacy Policy) accessible during offline mode.', 'protected' => false, 'configure_only' => true, ], 'mokosuiteclient_devtools' => [ 'icon' => 'icon-wrench', 'category' => 'tools', 'label' => 'Developer Tools', 'description' => 'Dev mode, hit counter reset, content version cleanup. Features are controlled inside the plugin settings.', 'protected' => false, 'configure_only' => true, ], 'mokosuiteclientdemo' => [ 'icon' => 'icon-undo', 'category' => 'content', 'label' => 'Demo Reset Task', 'description' => 'Scheduled demo site reset with content snapshots.', 'protected' => false, 'configure_only' => true, ], 'mokosuiteclientsync' => [ 'icon' => 'icon-sync', 'category' => 'content', 'label' => 'Content Sync Task', 'description' => 'Scheduled content synchronisation to remote MokoSuiteClient sites.', 'protected' => false, 'configure_only' => true, ], ]; /** * Category display labels and colours. */ private const CATEGORIES = [ 'core' => ['label' => 'Core', 'badge' => 'bg-dark'], 'security' => ['label' => 'Security', 'badge' => 'bg-danger'], 'tools' => ['label' => 'Tools', 'badge' => 'bg-info'], 'monitoring' => ['label' => 'Monitoring', 'badge' => 'bg-success'], 'content' => ['label' => 'Content', 'badge' => 'bg-primary'], 'api' => ['label' => 'API', 'badge' => 'bg-secondary'], ]; /** * Discover all installed MokoSuiteClient plugins. * * @return array Plugin rows enriched with dashboard metadata. */ public function getFeaturePlugins(): array { $db = $this->getDatabase(); $query = $db->getQuery(true) ->select([ $db->quoteName('extension_id'), $db->quoteName('name'), $db->quoteName('element'), $db->quoteName('folder'), $db->quoteName('type'), $db->quoteName('enabled'), $db->quoteName('protected'), $db->quoteName('params'), $db->quoteName('manifest_cache'), ]) ->from($db->quoteName('#__extensions')) ->where([ '(' . // System plugins: mokosuiteclient, mokosuiteclient_* '(' . $db->quoteName('type') . ' = ' . $db->quote('plugin') . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('system') . ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient') . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuiteclient\_%') . ')' . ' AND ' . $db->quoteName('element') . ' != ' . $db->quote('mokosuiteclient_monitor') . ')' // Webservices plugins . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin') . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('webservices') . ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient') . ')' // Task plugins . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin') . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('task') . ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuiteclient%') . ')' . ')', ]) ->order($db->quoteName('folder') . ' ASC, ' . $db->quoteName('element') . ' ASC'); $db->setQuery($query); $rows = $db->loadObjectList() ?: []; $plugins = []; foreach ($rows as $row) { $manifest = json_decode($row->manifest_cache ?? '{}'); $version = $manifest->version ?? ''; // Only system plugins and task plugins match PLUGIN_META by element $metaKey = ($row->folder === 'system' || $row->folder === 'task') ? $row->element : $row->folder . '_' . $row->element; $meta = self::PLUGIN_META[$metaKey] ?? null; // Auto-generate meta for task/webservices plugins not in the map if (!$meta) { $meta = $this->guessPluginMeta($row); } $categoryKey = $meta['category'] ?? 'tools'; $categoryInfo = self::CATEGORIES[$categoryKey] ?? self::CATEGORIES['tools']; $plugins[] = (object) [ 'extension_id' => (int) $row->extension_id, 'name' => $meta['label'] ?? $row->name, 'element' => $row->element, 'folder' => $row->folder, 'type' => $row->type, 'enabled' => (int) $row->enabled, 'protected' => (bool) ($meta['protected'] ?? false), 'configure_only' => (bool) ($meta['configure_only'] ?? false), 'version' => $version, 'icon' => $meta['icon'] ?? 'icon-puzzle-piece', 'category' => $categoryKey, 'categoryLabel' => $categoryInfo['label'], 'categoryBadge' => $categoryInfo['badge'], 'description' => $meta['description'] ?? '', ]; } return $plugins; } /** * Get basic site information for the info bar. * * @return object */ public function getSiteInfo(): object { $app = Factory::getApplication(); $config = $app->getConfig(); $db = $this->getDatabase(); // Get MokoSuiteClient package version $query = $db->getQuery(true) ->select($db->quoteName('manifest_cache')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokosuiteclient')) ->where($db->quoteName('type') . ' = ' . $db->quote('package')); $db->setQuery($query); $pkgCache = json_decode($db->loadResult() ?? '{}'); return (object) [ 'sitename' => $config->get('sitename', ''), 'joomla_version' => (new Version())->getShortVersion(), 'php_version' => PHP_VERSION, 'db_type' => $db->getServerType(), 'mokosuiteclient_version' => $pkgCache->version ?? '—', 'debug' => (bool) $config->get('debug'), 'offline' => (bool) $config->get('offline'), 'sef' => (bool) $config->get('sef'), 'caching' => (int) $config->get('caching'), ]; } /** * Get installed MokoSuiteClient component and modules with versions. * * @return array Array of extension objects with name, element, type, version. */ public function getMokoExtensions(): array { $db = $this->getDatabase(); $query = $db->getQuery(true) ->select([ $db->quoteName('element'), $db->quoteName('name'), $db->quoteName('type'), $db->quoteName('enabled'), $db->quoteName('manifest_cache'), ]) ->from($db->quoteName('#__extensions')) ->where('(' // The component . '(' . $db->quoteName('type') . ' = ' . $db->quote('component') . ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('com_mokosuiteclient') . ')' // Admin modules . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('module') . ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mod_mokosuiteclient%') . ')' . ')') ->order($db->quoteName('type') . ' ASC, ' . $db->quoteName('element') . ' ASC'); $db->setQuery($query); $rows = $db->loadObjectList() ?: []; $extensions = []; foreach ($rows as $row) { $manifest = json_decode($row->manifest_cache ?? '{}'); $extensions[] = (object) [ 'element' => $row->element, 'name' => $manifest->name ?? $row->name, 'type' => $row->type, 'version' => $manifest->version ?? '', 'enabled' => (int) $row->enabled, ]; } return $extensions; } /** * Toggle a plugin's enabled state. * * @param int $extensionId The extension ID. * @param int $enabled 1 = enable, 0 = disable. * * @return array Result with success and message keys. */ public function togglePlugin(int $extensionId, int $enabled): array { $db = $this->getDatabase(); // Verify the extension exists and is a MokoSuiteClient plugin $query = $db->getQuery(true) ->select([$db->quoteName('element'), $db->quoteName('protected')]) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('extension_id') . ' = ' . $extensionId) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient') . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuiteclient\\_%') . ')'); $db->setQuery($query); $ext = $db->loadObject(); if (!$ext) { return ['success' => false, 'message' => 'Extension not found.']; } // Don't allow disabling protected/core plugins if (!$enabled && ((int) $ext->protected || $ext->element === 'mokosuiteclient')) { return ['success' => false, 'message' => 'This plugin is protected and cannot be disabled.']; } $query = $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('enabled') . ' = ' . ($enabled ? 1 : 0)) ->where($db->quoteName('extension_id') . ' = ' . $extensionId); $db->setQuery($query); $db->execute(); return [ 'success' => true, 'message' => $ext->element . ($enabled ? ' enabled.' : ' disabled.'), 'enabled' => $enabled, ]; } /** * Clear all Joomla caches. * * @return array Result with success and message keys. */ public function clearCache(): array { try { // Use Joomla's native cache API — same as com_cache $cache = Factory::getContainer()->get(\Joomla\CMS\Cache\CacheControllerFactoryInterface::class); $cache->createCacheController('', ['defaultgroup' => ''])->cache->clean(''); // Also clean admin cache $conf = Factory::getApplication()->get('cache_handler', 'file'); $options = [ 'defaultgroup' => '', 'cachebase' => JPATH_ADMINISTRATOR . '/cache', 'storage' => $conf, ]; $cache->createCacheController('', $options)->cache->clean(''); // Clear opcache if available if (\function_exists('opcache_reset')) { \opcache_reset(); } return ['success' => true, 'message' => 'All cache cleared successfully.']; } catch (\Throwable $e) { return ['success' => false, 'message' => 'Cache clear failed: ' . $e->getMessage()]; } } /** * Clear the Joomla tmp directory. * * Removes all files and subdirectories from the configured tmp_path, * preserving the directory itself and any .htaccess / web.config files. * * @return array Result with success and message keys. */ public function clearTemp(): array { try { $tmpPath = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp'); if (!is_dir($tmpPath)) { return ['success' => false, 'message' => 'Temp directory does not exist: ' . $tmpPath]; } $count = 0; $protected = ['.htaccess', 'web.config', 'index.html', '.gitkeep']; $items = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($tmpPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($items as $item) { $basename = $item->getBasename(); // Skip protected files in the root tmp directory if ($item->getPath() === $tmpPath && \in_array($basename, $protected, true)) { continue; } if ($item->isDir()) { @rmdir($item->getPathname()); } else { @unlink($item->getPathname()); $count++; } } return ['success' => true, 'message' => sprintf('Temp directory cleaned (%d files removed).', $count)]; } catch (\Throwable $e) { return ['success' => false, 'message' => 'Temp clear failed: ' . $e->getMessage()]; } } /** * Auto-generate dashboard metadata for plugins not in the static map. */ private function guessPluginMeta(object $row): array { $meta = [ 'icon' => 'icon-puzzle-piece', 'category' => 'tools', 'label' => $row->name, 'description' => '', 'protected' => false, ]; if ($row->folder === 'webservices') { $meta['icon'] = 'icon-plug'; $meta['category'] = 'api'; $meta['label'] = 'Web Services — ' . ucfirst($row->element); } elseif ($row->folder === 'task') { $meta['icon'] = 'icon-clock'; $meta['category'] = 'content'; if (str_contains($row->element, 'sync')) { $meta['label'] = 'Content Sync Task'; $meta['description'] = 'Scheduled content synchronisation to remote MokoSuiteClient sites.'; } elseif (str_contains($row->element, 'demo')) { $meta['label'] = 'Demo Reset Task'; $meta['description'] = 'Scheduled demo site reset with content snapshots.'; } } return $meta; } /** * Get recent admin login attempts from action logs. */ public function getRecentLogins(int $limit = 10): array { try { $db = $this->getDatabase(); $query = $db->getQuery(true) ->select([ $db->quoteName('a.message'), $db->quoteName('a.log_date'), $db->quoteName('a.ip_address'), $db->quoteName('u.username'), ]) ->from($db->quoteName('#__action_logs', 'a')) ->leftJoin($db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.user_id')) ->where($db->quoteName('a.message_language_key') . ' = ' . $db->quote('PLG_ACTIONLOG_JOOMLA_USER_LOGGED_IN')) ->order($db->quoteName('a.log_date') . ' DESC') ->setLimit($limit); $db->setQuery($query); return $db->loadObjectList() ?: []; } catch (\Throwable $e) { return []; } } /** * Get pending extension updates. */ public function getPendingUpdates(): array { try { $db = $this->getDatabase(); $query = $db->getQuery(true) ->select([ $db->quoteName('u.name'), $db->quoteName('u.version'), $db->quoteName('u.type'), $db->quoteName('e.manifest_cache'), ]) ->from($db->quoteName('#__updates', 'u')) ->leftJoin($db->quoteName('#__extensions', 'e') . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id')) ->where($db->quoteName('u.extension_id') . ' != 0') ->order($db->quoteName('u.name') . ' ASC'); $db->setQuery($query); $rows = $db->loadObjectList() ?: []; foreach ($rows as $row) { $mc = json_decode($row->manifest_cache ?? '{}'); $row->current_version = $mc->version ?? ''; } return $rows; } catch (\Throwable $e) { return []; } } /** * Get checked-out items count and details. */ public function getCheckedOutItems(): array { try { $db = $this->getDatabase(); $query = $db->getQuery(true) ->select([ $db->quoteName('c.title'), $db->quoteName('c.checked_out'), $db->quoteName('c.checked_out_time'), $db->quoteName('u.username'), ]) ->from($db->quoteName('#__content', 'c')) ->leftJoin($db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('c.checked_out')) ->where($db->quoteName('c.checked_out') . ' IS NOT NULL') ->where($db->quoteName('c.checked_out') . ' != 0') ->order($db->quoteName('c.checked_out_time') . ' DESC') ->setLimit(10); $db->setQuery($query); return $db->loadObjectList() ?: []; } catch (\Throwable $e) { return []; } } /** * Get recent WAF blocks from the log table. */ public function getRecentWafBlocks(int $limit = 10): array { try { $db = $this->getDatabase(); $query = $db->getQuery(true) ->select('*') ->from($db->quoteName('#__mokosuiteclient_waf_log')) ->order($db->quoteName('created') . ' DESC') ->setLimit($limit); $db->setQuery($query); return $db->loadObjectList() ?: []; } catch (\Throwable $e) { return []; } } /** * WAF blocks per day for the last 14 days. */ public function getWafBlocksByDay(int $days = 14): array { try { $db = $this->getDatabase(); $db->setQuery( "SELECT DATE(" . $db->quoteName('created') . ") AS day, COUNT(*) AS total" . " FROM " . $db->quoteName('#__mokosuiteclient_waf_log') . " WHERE " . $db->quoteName('created') . " >= DATE_SUB(NOW(), INTERVAL " . (int) $days . " DAY)" . " GROUP BY day ORDER BY day" ); $rows = $db->loadObjectList() ?: []; // Fill in missing days with zero $result = []; $date = new \DateTime("-{$days} days"); $now = new \DateTime('now'); $map = []; foreach ($rows as $r) { $map[$r->day] = (int) $r->total; } while ($date <= $now) { $key = $date->format('Y-m-d'); $result[] = (object) ['day' => $date->format('M d'), 'total' => $map[$key] ?? 0]; $date->modify('+1 day'); } return $result; } catch (\Throwable $e) { return []; } } /** * Admin logins per day for the last 14 days. */ public function getLoginsByDay(int $days = 14): array { try { $db = $this->getDatabase(); $db->setQuery( "SELECT DATE(" . $db->quoteName('log_date') . ") AS day, COUNT(*) AS total" . " FROM " . $db->quoteName('#__action_logs') . " WHERE " . $db->quoteName('message_language_key') . " = 'PLG_ACTIONLOG_JOOMLA_USER_LOGGED_IN'" . " AND " . $db->quoteName('log_date') . " >= DATE_SUB(NOW(), INTERVAL " . (int) $days . " DAY)" . " GROUP BY day ORDER BY day" ); $rows = $db->loadObjectList() ?: []; $result = []; $date = new \DateTime("-{$days} days"); $now = new \DateTime('now'); $map = []; foreach ($rows as $r) { $map[$r->day] = (int) $r->total; } while ($date <= $now) { $key = $date->format('Y-m-d'); $result[] = (object) ['day' => $date->format('M d'), 'total' => $map[$key] ?? 0]; $date->modify('+1 day'); } return $result; } catch (\Throwable $e) { return []; } } }