diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f6b1e88 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "src/packages/tpl_mokoonyx"] + path = src/packages/tpl_mokoonyx + url = https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git + branch = main diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 9732d0f..c8a9eb8 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ Package - MokoWaaS MokoConsulting White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments - 02.31.00 + 02.32.21 GNU General Public License v3 diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 6fb2b44..0b771b6 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -65,6 +65,7 @@ jobs: with: token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 1 + submodules: recursive - name: Setup moko-platform tools env: @@ -124,6 +125,7 @@ jobs: with: token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 0 + submodules: recursive - name: Configure git for bot pushes run: | diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 3c44a06..f33f15c 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: moko-platform.Automation -# VERSION: 09.23.00 +# VERSION: 02.32.21 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index ff818ba..81b4cce 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -51,6 +51,7 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.MOKOGITEA_TOKEN }} + submodules: recursive - name: Setup moko-platform tools env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e03e79..10eff57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,35 @@ INGROUP: MokoWaaS.Documentation REPO: https://github.com/mokoconsulting-tech/mokowaas PATH: ./CHANGELOG.md - VERSION: 02.31.00 + VERSION: 02.32.21 BRIEF: Version history using `Keep a Changelog` --> # Changelog +## [02.32.00] - 2026-06-02 +### Added +- Admin control panel dashboard in com_mokowaas with site info bar, feature plugin grid, and quick actions +- Feature plugin architecture — MokoWaaS features split into toggleable plugins managed from the dashboard +- plg_system_mokowaas_firewall — HTTPS enforcement, trusted IPs, session timeout, upload restrictions, password policy +- plg_system_mokowaas_tenant — Installer, sysinfo, config, template, and menu restrictions for non-master users +- plg_system_mokowaas_devtools — Dev mode, hit counter reset, content version cleanup +- plg_system_mokowaas_monitor — Grafana heartbeat integration and health monitoring +- MokoWaaSHelper utility class for shared master-user detection across feature plugins +- AJAX plugin toggle — enable/disable feature plugins directly from the dashboard +- Clear cache quick action on dashboard +- Static updates.xml for update server (licensing system deferred) +- Automatic param migration from core plugin to feature plugins on upgrade + +### Changed +- com_mokowaas upgraded from API-only to full admin component with dashboard views +- Package manifest updated with 4 new feature plugin entries (10 extensions total) +- Update server URL changed to static raw file endpoint +- Core plugin slimmed — security, tenant, devtools, and monitor features extracted to dedicated plugins + +### Removed +- License key validation (licensing system not ready — will return in future release) +- Dynamic MokoGitea update feed dependency (replaced with static updates.xml) + ## [02.31.00] - 2026-06-01 ### Added - License key support via Joomla's native Update Sites download key system (dlid) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 9a12bed..2c28193 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Documentation REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: ./CODE_OF_CONDUCT.md BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default --> diff --git a/GOVERNANCE.md b/GOVERNANCE.md index a2a7087..e050316 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -19,7 +19,7 @@ DEFGROUP: mokoconsulting-tech.MokoWaaSBrand INGROUP: MokoStandards.Governance REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: /GOVERNANCE.md BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand --> diff --git a/LICENSE.md b/LICENSE.md index d9396ce..42b9353 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -15,7 +15,7 @@ INGROUP: MokoWaaS.Documentation REPO: https://github.com/mokoconsulting-tech/mokowaas PATH: ./LICENSE.md - VERSION: 02.31.00 + VERSION: 02.32.21 BRIEF: Project license (GPL-3.0-or-later) --> GNU GENERAL PUBLIC LICENSE diff --git a/README.md b/README.md index a6aa2b1..f52baae 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: /README.md BRIEF: MokoWaaS platform plugin for Joomla --> diff --git a/SECURITY.md b/SECURITY.md index fbc3fea..9df1a2a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME] INGROUP: [PROJECT_NAME].Documentation REPO: [REPOSITORY_URL] PATH: /SECURITY.md -VERSION: 02.31.00 +VERSION: 02.32.21 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/docs/guides/build-guide.md b/docs/guides/build-guide.md index 47d2d6a..34cf61b 100644 --- a/docs/guides/build-guide.md +++ b/docs/guides/build-guide.md @@ -11,13 +11,13 @@ INGROUP: MokoWaaS.Build REPO: https://github.com/mokoconsulting-tech/mokowaas FILE: build-guide.md - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: /docs/guides/ BRIEF: Build and packaging guide for the MokoWaaS system plugin NOTE: Defines environment setup, repository layout, packaging rules, and release preparation --> -# MokoWaaS Build Guide (VERSION: 02.31.00) +# MokoWaaS Build Guide (VERSION: 02.32.21) ## 1. Purpose diff --git a/docs/guides/configuration-guide.md b/docs/guides/configuration-guide.md index 97ba41a..9b1e097 100644 --- a/docs/guides/configuration-guide.md +++ b/docs/guides/configuration-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: /docs/guides/configuration-guide.md BRIEF: Configuration guide for the MokoWaaS system plugin NOTE: Defines plugin parameters, expected behaviors, and recommended defaults --> -# MokoWaaS Configuration Guide (VERSION: 02.31.00) +# MokoWaaS Configuration Guide (VERSION: 02.32.21) ## 1. Objective diff --git a/docs/guides/installation-guide.md b/docs/guides/installation-guide.md index 6a79dd3..526b98a 100644 --- a/docs/guides/installation-guide.md +++ b/docs/guides/installation-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: /docs/guides/installation-guide.md BRIEF: Installation guide for the MokoWaaS system plugin NOTE: First document in the guide set --> -# MokoWaaS Installation Guide (VERSION: 02.31.00) +# MokoWaaS Installation Guide (VERSION: 02.32.21) ## Introduction diff --git a/docs/guides/operations-guide.md b/docs/guides/operations-guide.md index c4fe0bc..025a822 100644 --- a/docs/guides/operations-guide.md +++ b/docs/guides/operations-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: /docs/guides/operations-guide.md BRIEF: Operational guide for administering and managing the MokoWaaS system plugin NOTE: Defines lifecycle, responsibilities, and operational behaviors --> -# MokoWaaS Operations Guide (VERSION: 02.31.00) +# MokoWaaS Operations Guide (VERSION: 02.32.21) ## Introduction diff --git a/docs/guides/rollback-and-recovery-guide.md b/docs/guides/rollback-and-recovery-guide.md index 0e88266..55df494 100644 --- a/docs/guides/rollback-and-recovery-guide.md +++ b/docs/guides/rollback-and-recovery-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: /docs/guides/rollback-and-recovery-guide.md BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents NOTE: Completes the core guide set for WaaS plugin governance --> -# MokoWaaS Rollback and Recovery Guide (VERSION: 02.31.00) +# MokoWaaS Rollback and Recovery Guide (VERSION: 02.32.21) ## Introduction diff --git a/docs/guides/testing-guide.md b/docs/guides/testing-guide.md index 7ae7327..f3ca57f 100644 --- a/docs/guides/testing-guide.md +++ b/docs/guides/testing-guide.md @@ -7,13 +7,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: /docs/guides/testing-guide.md BRIEF: Testing guide for MokoWaaS v02.01.08 NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration --> -# MokoWaaS Testing Guide (VERSION: 02.31.00) +# MokoWaaS Testing Guide (VERSION: 02.32.21) ## 1. Prerequisites diff --git a/docs/guides/troubleshooting-guide.md b/docs/guides/troubleshooting-guide.md index 2f3fd3f..f75fd42 100644 --- a/docs/guides/troubleshooting-guide.md +++ b/docs/guides/troubleshooting-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: /docs/guides/troubleshooting-guide.md BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin NOTE: Designed for administrators and WaaS operations teams --> -# MokoWaaS Troubleshooting Guide (VERSION: 02.31.00) +# MokoWaaS Troubleshooting Guide (VERSION: 02.32.21) ## Introduction diff --git a/docs/guides/upgrade-and-versioning-guide.md b/docs/guides/upgrade-and-versioning-guide.md index 118061a..86ae2dd 100644 --- a/docs/guides/upgrade-and-versioning-guide.md +++ b/docs/guides/upgrade-and-versioning-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: /docs/guides/upgrade-and-versioning-guide.md BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin NOTE: Defines release flow, version rules, and upgrade validation --> -# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.31.00) +# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.32.21) ## Introduction diff --git a/docs/index.md b/docs/index.md index 40e0e7c..c930359 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Documentation REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.31.00 + VERSION: 02.32.21 PATH: /docs/index.md BRIEF: Master index of all documentation for the MokoWaaS plugin NOTE: Automatically maintained index for all guide canvases --> -# MokoWaaS Documentation Index (VERSION: 02.31.00) +# MokoWaaS Documentation Index (VERSION: 02.32.21) ## Introduction diff --git a/docs/plugin-basic.md b/docs/plugin-basic.md index 78d75c4..6c36ff8 100644 --- a/docs/plugin-basic.md +++ b/docs/plugin-basic.md @@ -11,12 +11,12 @@ INGROUP: MokoWaaS REPO: https://github.com/mokoconsulting-tech/mokowaas PATH: /docs/plugin-basic.md - VERSION: 02.31.00 + VERSION: 02.32.21 BRIEF: Baseline documentation for the MokoWaaS system plugin NOTE: Foundational reference for internal and external stakeholders --> -# MokoWaaS Plugin Overview (VERSION: 02.31.00) +# MokoWaaS Plugin Overview (VERSION: 02.32.21) ## Introduction diff --git a/docs/update-server.md b/docs/update-server.md index f03bf01..0059d11 100644 --- a/docs/update-server.md +++ b/docs/update-server.md @@ -10,7 +10,7 @@ DEFGROUP: MokoWaaS.Documentation INGROUP: MokoStandards.Templates REPO: https://github.com/mokoconsulting-tech/MokoWaaS PATH: /docs/update-server.md -VERSION: 02.31.00 +VERSION: 02.32.21 BRIEF: How this extension's Joomla update server file (update.xml) is managed --> diff --git a/src/packages/com_mokowaas/admin/language/en-GB/com_mokowaas.ini b/src/packages/com_mokowaas/admin/language/en-GB/com_mokowaas.ini new file mode 100644 index 0000000..2d7a1c7 --- /dev/null +++ b/src/packages/com_mokowaas/admin/language/en-GB/com_mokowaas.ini @@ -0,0 +1,21 @@ +; MokoWaaS Admin Dashboard - Language Strings +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +COM_MOKOWAAS_DASHBOARD_TITLE="MokoWaaS Control Panel" +COM_MOKOWAAS_SITE="Site" +COM_MOKOWAAS_DATABASE="Database" +COM_MOKOWAAS_DEBUG_ON="Debug ON" +COM_MOKOWAAS_OFFLINE="Offline" +COM_MOKOWAAS_CLEAR_CACHE="Clear Cache" +COM_MOKOWAAS_CHECK_UPDATES="Check Updates" +COM_MOKOWAAS_ENABLED="Enabled" +COM_MOKOWAAS_DISABLED="Disabled" +COM_MOKOWAAS_PROTECTED="Protected" +COM_MOKOWAAS_CONFIGURE="Configure" +COM_MOKOWAAS_TOGGLE_SUCCESS="Plugin state updated." +COM_MOKOWAAS_TOGGLE_FAIL="Failed to update plugin state." +COM_MOKOWAAS_CACHE_CLEARED="Cache cleared successfully." +COM_MOKOWAAS_EXTENSIONS_TITLE="Moko Extensions" +COM_MOKOWAAS_EXTENSIONS_INFO="Install Moko Consulting Joomla packages from the official release server. Updates are handled through Joomla's native System > Update mechanism — each package registers its own update server." +COM_MOKOWAAS_EXTENSIONS_LINK="Moko Extensions" diff --git a/src/packages/com_mokowaas/admin/language/en-GB/com_mokowaas.sys.ini b/src/packages/com_mokowaas/admin/language/en-GB/com_mokowaas.sys.ini new file mode 100644 index 0000000..ac058b5 --- /dev/null +++ b/src/packages/com_mokowaas/admin/language/en-GB/com_mokowaas.sys.ini @@ -0,0 +1,7 @@ +; MokoWaaS Admin Dashboard - System Language Strings +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +COM_MOKOWAAS="MokoWaaS" +COM_MOKOWAAS_DESCRIPTION="MokoWaaS admin dashboard and REST API. Control panel for managing site features, health monitoring, and remote management." +COM_MOKOWAAS_DASHBOARD_TITLE="MokoWaaS Control Panel" diff --git a/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php b/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php new file mode 100644 index 0000000..b6bdac5 --- /dev/null +++ b/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php @@ -0,0 +1,125 @@ +getInput(); + + $user = $app->getIdentity(); + if (!$user->authorise('core.manage', 'com_plugins')) + { + $app->setHeader('Content-Type', 'application/json'); + echo json_encode(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]); + $app->close(); + } + + $extensionId = $input->getInt('extension_id', 0); + $enabled = $input->getInt('enabled', 0); + + if (!$extensionId) + { + $app->setHeader('Content-Type', 'application/json'); + echo json_encode(['success' => false, 'message' => 'Missing extension_id']); + $app->close(); + } + + $model = $this->getModel('Dashboard'); + $result = $model->togglePlugin($extensionId, $enabled); + + $app->setHeader('Content-Type', 'application/json'); + echo json_encode($result); + $app->close(); + } + + /** + * Clear the Joomla cache. + */ + public function clearCache() + { + Session::checkToken() or die(Text::_('JINVALID_TOKEN')); + + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + if (!$user->authorise('core.admin')) + { + $app->setHeader('Content-Type', 'application/json'); + echo json_encode(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]); + $app->close(); + } + + $model = $this->getModel('Dashboard'); + $result = $model->clearCache(); + + $app->setHeader('Content-Type', 'application/json'); + echo json_encode($result); + $app->close(); + } + + /** + * Install a Moko extension from a download URL. + */ + public function installExtension() + { + Session::checkToken() or die(Text::_('JINVALID_TOKEN')); + + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + if (!$user->authorise('core.admin')) + { + $app->setHeader('Content-Type', 'application/json'); + echo json_encode(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]); + $app->close(); + } + + $downloadUrl = $app->getInput()->getString('download_url', ''); + + if (empty($downloadUrl)) + { + $app->setHeader('Content-Type', 'application/json'); + echo json_encode(['success' => false, 'message' => 'Missing download URL.']); + $app->close(); + } + + $model = $this->getModel('Extensions'); + $result = $model->installFromUrl($downloadUrl); + + $app->setHeader('Content-Type', 'application/json'); + echo json_encode($result); + $app->close(); + } +} diff --git a/src/packages/com_mokowaas/admin/src/Model/DashboardModel.php b/src/packages/com_mokowaas/admin/src/Model/DashboardModel.php new file mode 100644 index 0000000..8c9d383 --- /dev/null +++ b/src/packages/com_mokowaas/admin/src/Model/DashboardModel.php @@ -0,0 +1,425 @@ + [ + 'icon' => 'icon-shield-alt', + 'category' => 'core', + 'label' => 'Core — Branding & Identity', + 'description' => 'White-label branding, master user enforcement, emergency access, and plugin protection.', + 'protected' => true, + ], + 'mokowaas_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, + ], + 'mokowaas_tenant' => [ + 'icon' => 'icon-users', + 'category' => 'security', + 'label' => 'Tenant Restrictions', + 'description' => 'Installer, sysinfo, config, and template access restrictions for non-master users.', + 'protected' => false, + ], + 'mokowaas_devtools' => [ + 'icon' => 'icon-wrench', + 'category' => 'tools', + 'label' => 'Developer Tools', + 'description' => 'Dev mode, hit counter reset, content version cleanup.', + 'protected' => false, + ], + 'mokowaas_monitor' => [ + 'icon' => 'icon-heartbeat', + 'category' => 'monitoring', + 'label' => 'Health Monitor', + 'description' => 'Site health checks, Grafana heartbeat integration, and diagnostics.', + 'protected' => false, + ], + ]; + + /** + * 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 MokoWaaS 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: mokowaas, mokowaas_* + '(' . $db->quoteName('type') . ' = ' . $db->quote('plugin') + . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('system') + . ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') + . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas\_%') . '))' + // Webservices plugins + . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin') + . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('webservices') + . ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') . ')' + // Task plugins + . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin') + . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('task') + . ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas%') . ')' + . ')', + ]) + ->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 ?? ''; + + // Build a lookup key: system plugins use element, others use folder_element + $metaKey = $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' => (int) $row->protected || ($meta['protected'] ?? 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 MokoWaaS package version + $query = $db->getQuery(true) + ->select($db->quoteName('manifest_cache')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas')) + ->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(), + 'mokowaas_version' => $pkgCache->version ?? '—', + 'debug' => (bool) $config->get('debug'), + 'offline' => (bool) $config->get('offline'), + 'sef' => (bool) $config->get('sef'), + 'caching' => (int) $config->get('caching'), + ]; + } + + /** + * 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 MokoWaaS 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')); + $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 === 'mokowaas')) + { + 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 + { + $app = Factory::getApplication(); + $app->get('cache_handler', 'file'); + + // Clear site and admin caches + $cache = Factory::getContainer()->get(\Joomla\CMS\Cache\CacheControllerFactoryInterface::class); + Factory::getCache('', '')->gc(); + Factory::getCache('', '', 'administrator')->gc(); + + // Clear opcache if available + if (\function_exists('opcache_reset')) + { + \opcache_reset(); + } + + return ['success' => true, 'message' => 'Cache cleared successfully.']; + } + catch (\Throwable $e) + { + return ['success' => false, 'message' => 'Cache 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 MokoWaaS 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') . ' LIKE ' . $db->quote('%LOGIN%')) + ->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('#__mokowaas_waf_log')) + ->order($db->quoteName('created') . ' DESC') + ->setLimit($limit); + $db->setQuery($query); + + return $db->loadObjectList() ?: []; + } + catch (\Throwable $e) + { + return []; + } + } +} diff --git a/src/packages/com_mokowaas/admin/src/Model/ExtensionsModel.php b/src/packages/com_mokowaas/admin/src/Model/ExtensionsModel.php new file mode 100644 index 0000000..e8402f1 --- /dev/null +++ b/src/packages/com_mokowaas/admin/src/Model/ExtensionsModel.php @@ -0,0 +1,305 @@ + [ + 'label' => 'MokoWaaS', + 'description' => 'Admin dashboard, security firewall, tenant restrictions, health monitoring, and REST API.', + 'element' => 'pkg_mokowaas', + 'type' => 'package', + 'icon' => 'icon-shield-alt', + 'category' => 'Platform', + 'article' => 'https://mokoconsulting.tech/kb/mokowaas-platform', + 'protected' => true, + ], + 'MokoOnyx' => [ + 'label' => 'MokoOnyx', + 'description' => 'Modern Joomla site template with dark mode, custom layouts, and MokoWaaS integration.', + 'element' => 'mokoonyx', + 'type' => 'template', + 'icon' => 'icon-paint-brush', + 'category' => 'Templates', + 'article' => 'https://mokoconsulting.tech/kb/mokoonyx-template', + 'protected' => false, + ], + 'MokoJoomTOS' => [ + 'label' => 'MokoJoomTOS', + 'description' => 'Terms of Service and privacy policy component with consent tracking.', + 'element' => 'com_mokojoomtos', + 'type' => 'component', + 'icon' => 'icon-file-contract', + 'category' => 'Components', + 'article' => 'https://mokoconsulting.tech/kb/mokojoomtos', + 'protected' => false, + ], + 'MokoJoomHero' => [ + 'label' => 'MokoJoomHero', + 'description' => 'Random hero image module from a configurable folder.', + 'element' => 'mod_mokojoomhero', + 'type' => 'module', + 'icon' => 'icon-image', + 'category' => 'Modules', + 'article' => 'https://mokoconsulting.tech/kb/mokojoomhero', + 'protected' => false, + ], + 'MokoWaaSAnnounce' => [ + 'label' => 'MokoWaaS Announce', + 'description' => 'Centralized announcement system via admin module.', + 'element' => 'mod_mokowaas_announce', + 'type' => 'module', + 'icon' => 'icon-bullhorn', + 'category' => 'Modules', + 'article' => 'https://mokoconsulting.tech/kb/mokowaas-announce', + 'protected' => false, + ], + 'MokoDPCalendarAPI' => [ + 'label' => 'DPCalendar API', + 'description' => 'Web Services plugin exposing DPCalendar events and calendars via REST API.', + 'element' => 'mokodpcalendarapi', + 'type' => 'plugin', + 'icon' => 'icon-calendar', + 'category' => 'Plugins', + 'article' => 'https://mokoconsulting.tech/kb/mokodpcalendarapi', + 'protected' => false, + ], + 'MokoGalleryCalendar' => [ + 'label' => 'Gallery Calendar', + 'description' => 'JoomGallery and DPCalendar integration — link galleries to events.', + 'element' => 'mokogallerycalendar', + 'type' => 'plugin', + 'icon' => 'icon-images', + 'category' => 'Plugins', + 'article' => 'https://mokoconsulting.tech/kb/mokogallerycalendar', + 'protected' => false, + ], + ]; + + private const GITEA_URL = 'https://git.mokoconsulting.tech'; + private const GITEA_ORG = 'MokoConsulting'; + + /** + * Get the full catalog with install status and release info. + * + * @return array + */ + public function getCatalog(): array + { + $installed = $this->getInstalledVersions(); + $packages = []; + + foreach (self::CATALOG as $repo => $meta) + { + $release = $this->fetchLatestRelease($repo); + + $localVersion = $installed[$meta['element']] ?? null; + $remoteVersion = $release['version'] ?? ''; + $downloadUrl = $release['download_url'] ?? ''; + + $status = ($localVersion !== null) ? 'installed' : 'not_installed'; + + // Get extension_id for uninstall link + $extensionId = $this->getExtensionId($meta['element']); + + $packages[] = (object) [ + 'repo' => $repo, + 'label' => $meta['label'], + 'description' => $meta['description'], + 'element' => $meta['element'], + 'type' => $meta['type'], + 'icon' => $meta['icon'], + 'category' => $meta['category'], + 'local_version' => $localVersion ?? '', + 'remote_version' => $remoteVersion, + 'download_url' => $downloadUrl, + 'status' => $status, + 'article_url' => $meta['article'] ?? '', + 'protected' => $meta['protected'] ?? false, + 'extension_id' => $extensionId, + ]; + } + + return $packages; + } + + /** + * Install an extension from a remote ZIP URL. + * + * @param string $url The download URL. + * + * @return array Result with success, message, and extension info. + */ + public function installFromUrl(string $url): array + { + $tmpPath = Factory::getConfig()->get('tmp_path', JPATH_ROOT . '/tmp'); + $tmpFile = $tmpPath . '/mokowaas_install_' . md5($url) . '.zip'; + + try + { + // Download + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 120); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + $data = curl_exec($ch); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error || $code !== 200 || empty($data)) + { + return ['success' => false, 'message' => 'Download failed: ' . ($error ?: "HTTP {$code}")]; + } + + file_put_contents($tmpFile, $data); + + // Install via Joomla Installer + $installer = new \Joomla\CMS\Installer\Installer(); + $result = $installer->install($tmpFile); + + @unlink($tmpFile); + + if (!$result) + { + return ['success' => false, 'message' => 'Installation failed.']; + } + + return [ + 'success' => true, + 'message' => 'Installed successfully.', + ]; + } + catch (\Throwable $e) + { + @unlink($tmpFile); + + return ['success' => false, 'message' => 'Error: ' . $e->getMessage()]; + } + } + + /** + * Get installed versions of all Moko extensions. + * + * @return array element => version + */ + private function getInstalledVersions(): array + { + $db = $this->getDatabase(); + $elements = []; + + foreach (self::CATALOG as $meta) + { + $elements[] = $db->quote($meta['element']); + } + + $query = $db->getQuery(true) + ->select([$db->quoteName('element'), $db->quoteName('manifest_cache')]) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' IN (' . implode(',', $elements) . ')'); + $db->setQuery($query); + $rows = $db->loadObjectList() ?: []; + + $versions = []; + + foreach ($rows as $row) + { + $mc = json_decode($row->manifest_cache ?? '{}'); + $versions[$row->element] = $mc->version ?? '0.0.0'; + } + + return $versions; + } + + /** + * Fetch the latest release from Gitea for a repo. + * + * @param string $repo Repository name. + * + * @return array [version, download_url] or empty. + */ + private function fetchLatestRelease(string $repo): array + { + $url = self::GITEA_URL . '/api/v1/repos/' . self::GITEA_ORG . '/' . $repo . '/releases?limit=1'; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']); + $response = curl_exec($ch); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($code !== 200 || empty($response)) + { + return []; + } + + $releases = json_decode($response, true); + + if (empty($releases[0])) + { + return []; + } + + $release = $releases[0]; + $version = $release['tag_name'] ?? ''; + + // Find the first .zip asset + $downloadUrl = ''; + + foreach ($release['assets'] ?? [] as $asset) + { + if (str_ends_with(strtolower($asset['name'] ?? ''), '.zip')) + { + $downloadUrl = $asset['browser_download_url'] ?? ''; + break; + } + } + + return [ + 'version' => $version, + 'download_url' => $downloadUrl, + ]; + } + + /** + * Get the extension_id for an element (for uninstall links). + */ + private function getExtensionId(string $element): int + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote($element)) + ->setLimit(1); + $db->setQuery($query); + + return (int) $db->loadResult(); + } +} diff --git a/src/packages/com_mokowaas/admin/src/View/Dashboard/HtmlView.php b/src/packages/com_mokowaas/admin/src/View/Dashboard/HtmlView.php new file mode 100644 index 0000000..323febf --- /dev/null +++ b/src/packages/com_mokowaas/admin/src/View/Dashboard/HtmlView.php @@ -0,0 +1,58 @@ +getModel(); + + $this->plugins = $model->getFeaturePlugins(); + $this->siteInfo = $model->getSiteInfo(); + $this->recentLogins = $model->getRecentLogins(5); + $this->pendingUpdates = $model->getPendingUpdates(); + $this->checkedOutItems = $model->getCheckedOutItems(); + $this->wafBlocks = $model->getRecentWafBlocks(5); + + $this->addToolbar(); + + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + $wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); + $wa->registerAndUseScript('com_mokowaas.dashboard', 'com_mokowaas/dashboard.js', [], ['defer' => true]); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_MOKOWAAS_DASHBOARD_TITLE'), 'cogs'); + + $user = Factory::getApplication()->getIdentity(); + + if ($user->authorise('core.admin', 'com_mokowaas')) + { + ToolbarHelper::preferences('com_mokowaas'); + } + } +} diff --git a/src/packages/com_mokowaas/admin/src/View/Extensions/HtmlView.php b/src/packages/com_mokowaas/admin/src/View/Extensions/HtmlView.php new file mode 100644 index 0000000..f086a84 --- /dev/null +++ b/src/packages/com_mokowaas/admin/src/View/Extensions/HtmlView.php @@ -0,0 +1,41 @@ +getModel(); + + $this->packages = $model->getCatalog(); + + $this->addToolbar(); + + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + $wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_MOKOWAAS_EXTENSIONS_TITLE'), 'puzzle-piece'); + ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas'); + } +} diff --git a/src/packages/com_mokowaas/admin/tmpl/dashboard/default.php b/src/packages/com_mokowaas/admin/tmpl/dashboard/default.php new file mode 100644 index 0000000..c892288 --- /dev/null +++ b/src/packages/com_mokowaas/admin/tmpl/dashboard/default.php @@ -0,0 +1,269 @@ +siteInfo; +$plugins = $this->plugins; +$recentLogins = $this->recentLogins; +$pendingUpdates = $this->pendingUpdates; +$checkedOut = $this->checkedOutItems; +$wafBlocks = $this->wafBlocks; +$token = Session::getFormToken(); + +// Group plugins by category +$grouped = []; +foreach ($plugins as $plugin) +{ + $grouped[$plugin->category][] = $plugin; +} + +$categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api']; +?> + +
+ +
+
+
+ + escape($siteInfo->sitename); ?> +
+
+ MokoWaaS + escape($siteInfo->mokowaas_version); ?> +
+
+ Joomla + escape($siteInfo->joomla_version); ?> +
+
+ PHP + escape($siteInfo->php_version); ?> +
+
+ + escape($siteInfo->db_type); ?> +
+ debug): ?> + + + offline): ?> + + +
+
+ + +
+
+ +
+ + +
+ + +
+ +
+ + + +

+ escape($first->categoryLabel); ?> +

+
+ +
+
+
+
+
+ +
escape($plugin->name); ?>
+
+ version): ?> + escape($plugin->version); ?> + +
+

escape($plugin->description); ?>

+
+ protected): ?> + + +
+ enabled ? 'checked' : ''; ?>> + +
+ + type === 'plugin'): ?> + + + + +
+
+
+
+ +
+ +
+ + +
+ + +
+
+ Pending Updates + +
+ +
+ + + + + + + + + + + +
ExtensionCurrentAvailable
escape($upd->name); ?>escape($upd->current_version); ?>escape($upd->version); ?>
+
+ +
+ All extensions up to date +
+ +
+ + +
+
+ Checked Out Items + +
+ +
+ + + + + + + + + + + +
ArticleUserSince
escape(mb_substr($item->title, 0, 30)); ?>escape($item->username ?? ''); ?>checked_out_time, 'M d H:i'); ?>
+
+ + +
+ No checked out items +
+ +
+ + +
+
+ Recent WAF Blocks + +
+ +
+ + + + + + + + + + + +
IPRuleTime
escape($block->ip); ?>escape($block->rule); ?>created, 'M d H:i'); ?>
+
+ +
+ No recent blocks +
+ +
+ + +
+
+ Recent Logins +
+ +
+ + + + + + + + + + + +
UserIPTime
escape($login->username ?? ''); ?>escape($login->ip_address ?? ''); ?>log_date, 'M d H:i'); ?>
+
+ +
No login activity recorded
+ +
+ +
+
+
diff --git a/src/packages/com_mokowaas/admin/tmpl/extensions/default.php b/src/packages/com_mokowaas/admin/tmpl/extensions/default.php new file mode 100644 index 0000000..cdaa685 --- /dev/null +++ b/src/packages/com_mokowaas/admin/tmpl/extensions/default.php @@ -0,0 +1,154 @@ +packages; +$token = Session::getFormToken(); + +// Group by category +$grouped = []; +foreach ($packages as $pkg) +{ + $grouped[$pkg->category][] = $pkg; +} + +$statusBadge = [ + 'installed' => ['bg-success', 'Installed'], + 'not_installed' => ['bg-secondary', 'Not Installed'], +]; +?> + +
+
+ + +
+ + $pkgs): ?> +

+
+ + status] ?? $statusBadge['not_installed']; + ?> +
+
+
+
+
+ +
+
label); ?>
+ type); ?> +
+
+ +
+ +

description); ?>

+ +
+
+ local_version): ?> + vlocal_version); ?> + remote_version): ?> + Latest: remote_version); ?> + +
+
+ article_url): ?> + + + + + download_url && $pkg->status === 'not_installed'): ?> + + status === 'installed'): ?> + + Installed + + protected && $pkg->extension_id): ?> + + + + + + No release + +
+
+
+
+
+ +
+ +
+ + diff --git a/src/packages/com_mokowaas/api/src/Controller/DashboardController.php b/src/packages/com_mokowaas/api/src/Controller/DashboardController.php new file mode 100644 index 0000000..f086e57 --- /dev/null +++ b/src/packages/com_mokowaas/api/src/Controller/DashboardController.php @@ -0,0 +1,145 @@ +getIdentity(); + + if (!$user->authorise('core.manage', 'com_plugins')) + { + $this->sendJson(403, ['error' => 'Not authorized']); + + return; + } + + $config = Factory::getConfig(); + $db = Factory::getDbo(); + + // Package version + $query = $db->getQuery(true) + ->select($db->quoteName('manifest_cache')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas')) + ->where($db->quoteName('type') . ' = ' . $db->quote('package')); + $db->setQuery($query); + $pkgCache = json_decode($db->loadResult() ?? '{}'); + + // Feature plugins + $query = $db->getQuery(true) + ->select([ + $db->quoteName('extension_id'), + $db->quoteName('name'), + $db->quoteName('element'), + $db->quoteName('folder'), + $db->quoteName('enabled'), + $db->quoteName('manifest_cache'), + ]) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') + . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas\\_%') . ')') + ->order($db->quoteName('element') . ' ASC'); + $db->setQuery($query); + $pluginRows = $db->loadObjectList() ?: []; + + $plugins = []; + + foreach ($pluginRows as $row) + { + $manifest = json_decode($row->manifest_cache ?? '{}'); + $plugins[] = [ + 'extension_id' => (int) $row->extension_id, + 'name' => $row->name, + 'element' => $row->element, + 'enabled' => (bool) $row->enabled, + 'version' => $manifest->version ?? '', + ]; + } + + // Quick health checks + $dbOk = true; + + try + { + $db->setQuery('SELECT 1'); + $db->loadResult(); + } + catch (\Throwable $e) + { + $dbOk = false; + } + + $freeDiskMb = null; + $free = @disk_free_space(JPATH_ROOT); + + if ($free !== false) + { + $freeDiskMb = round($free / 1048576); + } + + $this->sendJson(200, [ + 'status' => 'ok', + 'timestamp' => gmdate('Y-m-d\TH:i:s\Z'), + 'site' => [ + 'name' => $config->get('sitename', ''), + 'url' => rtrim(Uri::root(), '/'), + 'mokowaas_version' => $pkgCache->version ?? '', + 'joomla_version' => (new Version())->getShortVersion(), + 'php_version' => PHP_VERSION, + 'db_type' => $db->getServerType(), + 'debug' => (bool) $config->get('debug'), + 'offline' => (bool) $config->get('offline'), + 'caching' => (int) $config->get('caching'), + ], + 'health' => [ + 'database' => $dbOk ? 'ok' : 'error', + 'free_disk_mb' => $freeDiskMb, + ], + 'plugins' => $plugins, + ]); + } + + private function sendJson(int $code, array $payload): void + { + $app = Factory::getApplication(); + $app->setHeader('Content-Type', 'application/json', true); + $app->setHeader('Status', (string) $code, true); + echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + $app->close(); + } +} diff --git a/src/packages/com_mokowaas/api/src/Controller/PluginsController.php b/src/packages/com_mokowaas/api/src/Controller/PluginsController.php new file mode 100644 index 0000000..a0b84be --- /dev/null +++ b/src/packages/com_mokowaas/api/src/Controller/PluginsController.php @@ -0,0 +1,180 @@ +getIdentity(); + + if (!$user->authorise('core.manage', 'com_plugins')) + { + $this->sendJson(403, ['error' => 'Not authorized']); + + return; + } + + $db = Factory::getDbo(); + $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('manifest_cache'), + ]) + ->from($db->quoteName('#__extensions')) + ->where([ + '(' . + '(' . $db->quoteName('type') . ' = ' . $db->quote('plugin') + . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('system') + . ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') + . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas\\_%') . '))' + . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin') + . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('webservices') + . ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') . ')' + . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin') + . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('task') + . ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas%') . ')' + . ')', + ]) + ->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 ?? '{}'); + + $plugins[] = [ + 'extension_id' => (int) $row->extension_id, + 'name' => $row->name, + 'element' => $row->element, + 'folder' => $row->folder, + 'type' => $row->type, + 'enabled' => (bool) $row->enabled, + 'protected' => (bool) $row->protected, + 'version' => $manifest->version ?? '', + ]; + } + + $this->sendJson(200, [ + 'status' => 'ok', + 'count' => \count($plugins), + 'plugins' => $plugins, + ]); + } + + /** + * Toggle a MokoWaaS feature plugin on or off. + * + * Expects JSON body: {"extension_id": 123, "enabled": true} + * + * @return void + */ + public function execute(): void + { + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + if (!$user->authorise('core.manage', 'com_plugins')) + { + $this->sendJson(403, ['error' => 'Not authorized']); + + return; + } + + $input = json_decode(file_get_contents('php://input'), true); + + $extensionId = (int) ($input['extension_id'] ?? 0); + $enabled = (bool) ($input['enabled'] ?? false); + + if (!$extensionId) + { + $this->sendJson(400, ['error' => 'Missing extension_id']); + + return; + } + + $db = Factory::getDbo(); + + // Verify the extension exists and is a MokoWaaS 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')); + $db->setQuery($query); + $ext = $db->loadObject(); + + if (!$ext) + { + $this->sendJson(404, ['error' => 'Extension not found']); + + return; + } + + // Don't allow disabling protected/core plugins + if (!$enabled && ((int) $ext->protected || $ext->element === 'mokowaas')) + { + $this->sendJson(409, ['error' => 'This plugin is protected and cannot be disabled']); + + return; + } + + $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(); + + $this->sendJson(200, [ + 'status' => 'ok', + 'extension_id' => $extensionId, + 'element' => $ext->element, + 'enabled' => $enabled, + ]); + } + + private function sendJson(int $code, array $payload): void + { + $app = Factory::getApplication(); + $app->setHeader('Content-Type', 'application/json', true); + $app->setHeader('Status', (string) $code, true); + echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + $app->close(); + } +} diff --git a/src/packages/com_mokowaas/media/css/dashboard.css b/src/packages/com_mokowaas/media/css/dashboard.css new file mode 100644 index 0000000..91bf844 --- /dev/null +++ b/src/packages/com_mokowaas/media/css/dashboard.css @@ -0,0 +1,94 @@ +/** + * MokoWaaS Dashboard Styles + * @package com_mokowaas + */ + +/* Info bar */ +.mokowaas-info-bar .card-body { + padding: 1rem 1.5rem; +} + +.mokowaas-info-item { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.mokowaas-info-label { + font-size: 0.8125rem; + color: #6c757d; + text-transform: uppercase; + letter-spacing: 0.025em; +} + +.mokowaas-info-value { + font-size: 0.875rem; +} + +/* Plugin cards */ +.mokowaas-plugin-card { + transition: box-shadow 0.15s ease, opacity 0.15s ease; + border-left: 3px solid #0d6efd; +} + +.mokowaas-plugin-card:hover { + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); +} + +.mokowaas-plugin-disabled { + opacity: 0.6; + border-left-color: #adb5bd; +} + +.mokowaas-plugin-disabled:hover { + opacity: 0.8; +} + +.mokowaas-plugin-icon { + font-size: 1.5rem; + color: #1a2744; + width: 2rem; + text-align: center; +} + +/* Category headings */ +.mokowaas-category-heading { + font-size: 1rem; + font-weight: 600; + padding-top: 0.5rem; +} + +/* Toggle switch */ +.mokowaas-toggle { + cursor: pointer; +} + +.mokowaas-toggle:disabled { + cursor: not-allowed; +} + +/* Quick actions */ +.mokowaas-quick-actions .btn { + transition: all 0.15s ease; +} + +.mokowaas-quick-actions .btn:disabled { + pointer-events: none; +} + +/* Loading spinner overlay on toggle */ +.mokowaas-plugin-card.is-loading { + position: relative; + pointer-events: none; +} + +.mokowaas-plugin-card.is-loading::after { + content: ''; + position: absolute; + inset: 0; + background: rgba(255, 255, 255, 0.7); + display: flex; + align-items: center; + justify-content: center; + border-radius: inherit; +} diff --git a/src/packages/com_mokowaas/media/js/dashboard.js b/src/packages/com_mokowaas/media/js/dashboard.js new file mode 100644 index 0000000..df8433e --- /dev/null +++ b/src/packages/com_mokowaas/media/js/dashboard.js @@ -0,0 +1,112 @@ +/** + * MokoWaaS Dashboard Scripts + * @package com_mokowaas + */ + +document.addEventListener('DOMContentLoaded', function () { + 'use strict'; + + // Plugin toggle switches + document.querySelectorAll('.mokowaas-toggle').forEach(function (toggle) { + toggle.addEventListener('change', function () { + var checkbox = this; + var card = checkbox.closest('.mokowaas-plugin-card'); + var extensionId = checkbox.dataset.extensionId; + var url = checkbox.dataset.url; + var token = checkbox.dataset.token; + var enabled = checkbox.checked ? 1 : 0; + var label = card.querySelector('.form-check-label'); + + card.classList.add('is-loading'); + checkbox.disabled = true; + + var formData = new FormData(); + formData.append('extension_id', extensionId); + formData.append('enabled', enabled); + formData.append(token, '1'); + + fetch(url, { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(function (response) { return response.json(); }) + .then(function (data) { + if (data.success) { + card.classList.toggle('mokowaas-plugin-disabled', !enabled); + if (label) { + label.textContent = enabled + ? Joomla.Text._('COM_MOKOWAAS_ENABLED') || 'Enabled' + : Joomla.Text._('COM_MOKOWAAS_DISABLED') || 'Disabled'; + } + } else { + // Revert on failure + checkbox.checked = !checkbox.checked; + Joomla.renderMessages({error: [data.message || 'Toggle failed.']}); + } + }) + .catch(function () { + checkbox.checked = !checkbox.checked; + Joomla.renderMessages({error: ['Network error. Please try again.']}); + }) + .finally(function () { + card.classList.remove('is-loading'); + checkbox.disabled = false; + }); + }); + }); + + // Clear cache button + var cacheBtn = document.getElementById('mokowaas-btn-cache'); + if (cacheBtn) { + cacheBtn.addEventListener('click', function () { + var btn = this; + var url = btn.dataset.url; + var token = btn.dataset.token; + + btn.disabled = true; + var btnIcon = btn.querySelector('span'); + var btnOriginalClass = btnIcon ? btnIcon.className : ''; + if (btnIcon) { + btnIcon.className = 'icon-spinner icon-spin'; + } + btn.childNodes.forEach(function (n) { + if (n.nodeType === Node.TEXT_NODE) n.textContent = ' Clearing...'; + }); + + var formData = new FormData(); + formData.append(token, '1'); + + fetch(url, { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(function (response) { return response.json(); }) + .then(function (data) { + if (data.success) { + Joomla.renderMessages({message: [data.message || 'Cache cleared.']}); + } else { + Joomla.renderMessages({error: [data.message || 'Cache clear failed.']}); + } + }) + .catch(function () { + Joomla.renderMessages({error: ['Network error. Please try again.']}); + }) + .finally(function () { + btn.disabled = false; + var icon = btn.querySelector('span'); + if (icon) { + icon.className = btnOriginalClass; + } + btn.childNodes.forEach(function (n) { + if (n.nodeType === Node.TEXT_NODE) n.textContent = ' Clear Cache'; + }); + }); + }); + } +}); diff --git a/src/packages/com_mokowaas/mokowaas.xml b/src/packages/com_mokowaas/mokowaas.xml index 5a40368..d3cca22 100644 --- a/src/packages/com_mokowaas/mokowaas.xml +++ b/src/packages/com_mokowaas/mokowaas.xml @@ -1,24 +1,48 @@ + - MokoWaaS API + MokoWaaS Moko Consulting - 2026-05-23 + 2026-06-02 Copyright (C) 2026 Moko Consulting. All rights reserved. GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.31.00 - 02.31.00 - Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups. - Moko\Component\MokoWaaS\Api + 02.32.21 + MokoWaaS admin dashboard and REST API. Provides a control panel for managing MokoWaaS feature plugins, site health monitoring, and remote management endpoints. + + Moko\Component\MokoWaaS + + MokoWaaS + language services + src + tmpl + src + + + css + js + diff --git a/src/packages/mod_mokowaas_cpanel/language/en-GB/mod_mokowaas_cpanel.ini b/src/packages/mod_mokowaas_cpanel/language/en-GB/mod_mokowaas_cpanel.ini new file mode 100644 index 0000000..f40143a --- /dev/null +++ b/src/packages/mod_mokowaas_cpanel/language/en-GB/mod_mokowaas_cpanel.ini @@ -0,0 +1,29 @@ +; MokoWaaS CPanel Module +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +MOD_MOKOWAAS_CPANEL="MokoWaaS" +MOD_MOKOWAAS_CPANEL_DESC="Displays MokoWaaS feature plugin status and site health on the admin dashboard." + +MOD_MOKOWAAS_CPANEL_FIELDSET_DISPLAY="Display Options" +MOD_MOKOWAAS_CPANEL_FIELDSET_DISPLAY_DESC="Choose which sections to show in the module." + +MOD_MOKOWAAS_CPANEL_COLLAPSED_LABEL="Collapsed by Default" +MOD_MOKOWAAS_CPANEL_COLLAPSED_DESC="Start the module body collapsed. Click the header to expand." +MOD_MOKOWAAS_CPANEL_SHOW_HEALTH_LABEL="Health Status" +MOD_MOKOWAAS_CPANEL_SHOW_STATS_LABEL="Stats Cards" +MOD_MOKOWAAS_CPANEL_SHOW_STATS_DESC="Article count, user count, and pending updates." +MOD_MOKOWAAS_CPANEL_SHOW_DISK_LABEL="Disk Usage" +MOD_MOKOWAAS_CPANEL_SHOW_IP_LABEL="Current IP" +MOD_MOKOWAAS_CPANEL_SHOW_PLUGINS_LABEL="Feature Plugins" +MOD_MOKOWAAS_CPANEL_SHOW_ACTIONS_LABEL="Quick Actions" +MOD_MOKOWAAS_CPANEL_SHOW_ACTIONS_DESC="Clear cache, check updates buttons." +MOD_MOKOWAAS_CPANEL_SHOW_VERSIONS_LABEL="Joomla/PHP Versions" +MOD_MOKOWAAS_CPANEL_SHOW_VERSIONS_DESC="Show Joomla and PHP version numbers." + +MOD_MOKOWAAS_CPANEL_OPEN_DASHBOARD="Control Panel" +MOD_MOKOWAAS_CPANEL_DEBUG="Debug ON" +MOD_MOKOWAAS_CPANEL_OFFLINE="Offline" +MOD_MOKOWAAS_CPANEL_HEALTH_OK="All Systems OK" +MOD_MOKOWAAS_CPANEL_HEALTH_ERROR="Database Error" +MOD_MOKOWAAS_CPANEL_PLUGINS_SUMMARY="%d of %d features enabled" diff --git a/src/packages/mod_mokowaas_cpanel/language/en-GB/mod_mokowaas_cpanel.sys.ini b/src/packages/mod_mokowaas_cpanel/language/en-GB/mod_mokowaas_cpanel.sys.ini new file mode 100644 index 0000000..60ded55 --- /dev/null +++ b/src/packages/mod_mokowaas_cpanel/language/en-GB/mod_mokowaas_cpanel.sys.ini @@ -0,0 +1,3 @@ +; MokoWaaS CPanel Module - System strings +MOD_MOKOWAAS_CPANEL="MokoWaaS" +MOD_MOKOWAAS_CPANEL_DESC="Displays MokoWaaS feature plugin status and site health on the admin dashboard." diff --git a/src/packages/mod_mokowaas_cpanel/mod_mokowaas_cpanel.xml b/src/packages/mod_mokowaas_cpanel/mod_mokowaas_cpanel.xml new file mode 100644 index 0000000..8e6ec27 --- /dev/null +++ b/src/packages/mod_mokowaas_cpanel/mod_mokowaas_cpanel.xml @@ -0,0 +1,93 @@ + + + mod_mokowaas_cpanel + Moko Consulting + 2026-06-02 + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + hello@mokoconsulting.tech + https://mokoconsulting.tech + 02.32.21 + MOD_MOKOWAAS_CPANEL_DESC + Moko\Module\MokoWaaSCpanel + + + services + src + tmpl + + + + en-GB/mod_mokowaas_cpanel.ini + en-GB/mod_mokowaas_cpanel.sys.ini + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/src/packages/mod_mokowaas_cpanel/services/provider.php b/src/packages/mod_mokowaas_cpanel/services/provider.php new file mode 100644 index 0000000..c0d11c0 --- /dev/null +++ b/src/packages/mod_mokowaas_cpanel/services/provider.php @@ -0,0 +1,25 @@ +registerServiceProvider(new ModuleDispatcherFactory('\\Moko\\Module\\MokoWaaSCpanel')); + $container->registerServiceProvider(new HelperFactory('\\Moko\\Module\\MokoWaaSCpanel\\Administrator\\Helper')); + $container->registerServiceProvider(new Module()); + } +}; diff --git a/src/packages/mod_mokowaas_cpanel/src/Dispatcher/Dispatcher.php b/src/packages/mod_mokowaas_cpanel/src/Dispatcher/Dispatcher.php new file mode 100644 index 0000000..d3b1c19 --- /dev/null +++ b/src/packages/mod_mokowaas_cpanel/src/Dispatcher/Dispatcher.php @@ -0,0 +1,39 @@ +get(DatabaseInterface::class); + $helper = $this->getHelperFactory()->getHelper('CpanelHelper'); + + $data['siteInfo'] = $helper->getSiteInfo($db); + $data['plugins'] = $helper->getFeaturePlugins($db); + $data['healthOk'] = $helper->isDatabaseOk($db); + $data['counts'] = $helper->getCounts($db); + $data['disk'] = $helper->getDiskInfo(); + $data['currentIp'] = $helper->getCurrentIp(); + + return $data; + } +} diff --git a/src/packages/mod_mokowaas_cpanel/src/Helper/CpanelHelper.php b/src/packages/mod_mokowaas_cpanel/src/Helper/CpanelHelper.php new file mode 100644 index 0000000..7160329 --- /dev/null +++ b/src/packages/mod_mokowaas_cpanel/src/Helper/CpanelHelper.php @@ -0,0 +1,139 @@ +getQuery(true) + ->select($db->quoteName('manifest_cache')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas')) + ->where($db->quoteName('type') . ' = ' . $db->quote('package')); + $db->setQuery($query); + $pkgCache = json_decode($db->loadResult() ?? '{}'); + + return (object) [ + 'mokowaas_version' => $pkgCache->version ?? '', + 'joomla_version' => (new Version())->getShortVersion(), + 'php_version' => PHP_VERSION, + 'debug' => (bool) $config->get('debug'), + 'offline' => (bool) $config->get('offline'), + ]; + } + + /** + * Get MokoWaaS system feature plugins with their enabled state. + */ + public function getFeaturePlugins(DatabaseInterface $db): array + { + $query = $db->getQuery(true) + ->select([ + $db->quoteName('extension_id'), + $db->quoteName('name'), + $db->quoteName('element'), + $db->quoteName('enabled'), + ]) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') + . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas\\_%') . ')') + ->order($db->quoteName('element') . ' ASC'); + $db->setQuery($query); + + return $db->loadObjectList() ?: []; + } + + /** + * Quick database connectivity check. + */ + public function isDatabaseOk(DatabaseInterface $db): bool + { + try + { + $db->setQuery('SELECT 1'); + $db->loadResult(); + + return true; + } + catch (\Throwable $e) + { + return false; + } + } + + /** + * Get content and system counts. + */ + public function getCounts(DatabaseInterface $db): object + { + $counts = (object) [ + 'articles' => 0, + 'users' => 0, + 'extensions' => 0, + 'updates' => 0, + ]; + + try + { + $db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__content'))); + $counts->articles = (int) $db->loadResult(); + + $db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__users'))); + $counts->users = (int) $db->loadResult(); + + $db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__extensions'))->where($db->quoteName('enabled') . ' = 1')); + $counts->extensions = (int) $db->loadResult(); + + $db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__updates'))->where($db->quoteName('extension_id') . ' != 0')); + $counts->updates = (int) $db->loadResult(); + } + catch (\Throwable $e) + { + // Silent + } + + return $counts; + } + + /** + * Get disk usage info. + */ + public function getDiskInfo(): object + { + $free = @disk_free_space(JPATH_ROOT); + $total = @disk_total_space(JPATH_ROOT); + + return (object) [ + 'free_mb' => $free !== false ? round($free / 1048576) : null, + 'total_mb' => $total !== false ? round($total / 1048576) : null, + ]; + } + + /** + * Get the current visitor's IP address. + */ + public function getCurrentIp(): string + { + return $_SERVER['REMOTE_ADDR'] ?? ''; + } +} diff --git a/src/packages/mod_mokowaas_cpanel/tmpl/default.php b/src/packages/mod_mokowaas_cpanel/tmpl/default.php new file mode 100644 index 0000000..b3ed62f --- /dev/null +++ b/src/packages/mod_mokowaas_cpanel/tmpl/default.php @@ -0,0 +1,199 @@ + 0, 'users' => 0, 'extensions' => 0, 'updates' => 0]; +$disk = $disk ?? (object) ['free_mb' => null, 'total_mb' => null]; +$currentIp = $currentIp ?? ''; +$collapsed = $params->get('collapsed', 1); +$showHealth = $params->get('show_health', 1); +$showStats = $params->get('show_stats', 1); +$showDisk = $params->get('show_disk', 1); +$showIp = $params->get('show_ip', 1); +$showPlugins = $params->get('show_plugins', 1); +$showActions = $params->get('show_actions', 1); +$showVersions = $params->get('show_versions', 1); +$token = Session::getFormToken(); + +$enabledCount = 0; +$totalCount = count($plugins); + +foreach ($plugins as $p) +{ + if ($p->enabled) + { + $enabledCount++; + } +} + +$labels = [ + 'mokowaas' => 'Core', + 'mokowaas_firewall' => 'Firewall', + 'mokowaas_tenant' => 'Tenant', + 'mokowaas_devtools' => 'DevTools', + 'mokowaas_monitor' => 'Monitor', +]; + +$diskPct = ($disk->total_mb && $disk->total_mb > 0) + ? round((($disk->total_mb - ($disk->free_mb ?? 0)) / $disk->total_mb) * 100) + : null; +$diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !== null && $diskPct > 75) ? 'bg-warning' : 'bg-success'); +?> + +
+ + + + +
+ + + +
+
+
+ + + Healthy + + + DB Error + +
+
+
+
+ articles; ?> + Articles +
+
+
+
+ users; ?> + Users +
+
+
+
+ updates > 0): ?> + updates; ?> + Updates + + + Up to date + +
+
+
+ + +
+ + + + % + + free_mb ?? 0) / 1024, 1); ?>G free + + + + + + + Jjoomla_version ?? ''); ?> / PHP php_version ?? ''); ?> + + + + + element] ?? $p->element; + $badge = $p->enabled ? 'bg-success' : 'bg-secondary'; + $icon = $p->enabled ? 'icon-check' : 'icon-times'; + $configUrl = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . (int) $p->extension_id); + ?> + + + + + + + + + + Check Updates + + updates > 0): ?> + + updates; ?> updateupdates > 1 ? 's' : ''; ?> + + + +
+ + +
+
+ + diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 1b54b90..badd115 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -22,7 +22,7 @@ * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS * REPO: https://github.com/mokoconsulting-tech/mokowaas - * VERSION: 02.31.00 + * VERSION: 02.32.21 * PATH: /src/Extension/MokoWaaS.php * NOTE: Handles Joomla system events for rebranding functionality */ @@ -1463,6 +1463,96 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface return true; } + /** + * Cascade enable/disable state across all MokoWaaS extensions. + * + * When the core system plugin (plg_system_mokowaas) is disabled, + * all feature plugins and the cpanel module are also disabled. + * When re-enabled, they are re-enabled too. + * + * @param string $context The extension context + * @param array $pks Extension IDs being changed + * @param int $value New state (1=enabled, 0=disabled) + * + * @return void + * + * @since 02.32.00 + */ + public function onExtensionChangeState($context, $pks, $value) + { + if (empty($pks)) + { + return; + } + + try + { + $db = Factory::getDbo(); + + // Check if the core MokoWaaS plugin is among the changed extensions + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->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); + $coreId = (int) $db->loadResult(); + + if (!$coreId || !\in_array($coreId, array_map('intval', $pks), true)) + { + return; + } + + // Cascade to all MokoWaaS feature plugins + module + $mokoElements = [ + $db->quote('mokowaas_firewall'), + $db->quote('mokowaas_tenant'), + $db->quote('mokowaas_devtools'), + $db->quote('mokowaas_monitor'), + $db->quote('mod_mokowaas_cpanel'), + ]; + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = ' . (int) $value) + ->where($db->quoteName('element') . ' IN (' . implode(',', $mokoElements) . ')'); + $db->setQuery($query); + $db->execute(); + $affected = $db->getAffectedRows(); + + // Also update module published state + if ($value == 0) + { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__modules')) + ->set($db->quoteName('published') . ' = 0') + ->where($db->quoteName('module') . ' = ' . $db->quote('mod_mokowaas_cpanel')) + )->execute(); + } + else + { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__modules')) + ->set($db->quoteName('published') . ' = 1') + ->where($db->quoteName('module') . ' = ' . $db->quote('mod_mokowaas_cpanel')) + )->execute(); + } + + $state = $value ? 'enabled' : 'disabled'; + $this->app->enqueueMessage( + "MokoWaaS: {$state} {$affected} associated extensions.", + 'message' + ); + } + catch (\Throwable $e) + { + Log::add('MokoWaaS cascade state error: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); + } + } + /** * Filter admin menu items for non-master users. * @@ -4350,6 +4440,16 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface return; } + // Only warn once per session + $session = Factory::getSession(); + + if ($session->get('mokowaas.license_warned', false)) + { + return; + } + + $session->set('mokowaas.license_warned', true); + try { $db = Factory::getDbo(); @@ -4366,10 +4466,10 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface if (empty($extraQuery) || strpos($extraQuery, 'dlid=') === false) { $this->app->enqueueMessage( - 'MokoWaaS License Key Required — ' + 'Moko Consulting License Key Required — ' . 'No download key is configured. Updates will not be available until a valid license key is entered. ' . 'Go to System → Update Sites ' - . 'and enter your license key (MOKO-XXXX-XXXX-XXXX-XXXX) in the Download Key field for the MokoWaaS update site.', + . 'and enter your license key in the Download Key field for the MokoWaaS update site.', 'warning' ); @@ -4396,7 +4496,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface if ($session->get('mokowaas.license_invalid', false)) { $this->app->enqueueMessage( - 'MokoWaaS License Key Invalid — ' + 'Moko Consulting License Key Invalid — ' . 'Your license key could not be validated. Please verify your key in ' . 'System → Update Sites.', 'error' @@ -4430,7 +4530,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface if (!$isValid) { $this->app->enqueueMessage( - 'MokoWaaS License Key Invalid — ' + 'Moko Consulting License Key Invalid — ' . 'Your license key could not be validated. Updates will not be available. ' . 'Please verify your key in ' . 'System → Update Sites.', diff --git a/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php b/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php index 23666eb..2aa3e65 100644 --- a/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php +++ b/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php @@ -7,7 +7,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.31.00 + * VERSION: 02.32.21 * PATH: /src/Field/AllowedIpsField.php * BRIEF: Custom form field that displays the current IP whitelist */ diff --git a/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php b/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php index 8a39d41..7ed749d 100644 --- a/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php +++ b/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php @@ -8,7 +8,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.31.00 + * VERSION: 02.32.21 * PATH: /src/Field/CopyableTokenField.php * BRIEF: Read-only token field with a copy-to-clipboard button */ diff --git a/src/packages/plg_system_mokowaas/Field/CurrentIpField.php b/src/packages/plg_system_mokowaas/Field/CurrentIpField.php index 91ceec1..ea475f6 100644 --- a/src/packages/plg_system_mokowaas/Field/CurrentIpField.php +++ b/src/packages/plg_system_mokowaas/Field/CurrentIpField.php @@ -7,7 +7,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.31.00 + * VERSION: 02.32.21 * PATH: /src/Field/CurrentIpField.php * BRIEF: Read-only field that displays the current user's IP address */ diff --git a/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php b/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php index 49a8298..9b280b3 100644 --- a/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php +++ b/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php @@ -8,7 +8,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.31.00 + * VERSION: 02.32.21 * PATH: /src/Field/DemoTaskInfoField.php * BRIEF: Read-only field showing scheduled task info with link to manage it */ diff --git a/src/packages/plg_system_mokowaas/Field/NextResetField.php b/src/packages/plg_system_mokowaas/Field/NextResetField.php index 446ed78..f8dc2b2 100644 --- a/src/packages/plg_system_mokowaas/Field/NextResetField.php +++ b/src/packages/plg_system_mokowaas/Field/NextResetField.php @@ -8,7 +8,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.31.00 + * VERSION: 02.32.21 * PATH: /src/Field/NextResetField.php * BRIEF: Read-only field showing next reset time from Joomla scheduled task */ diff --git a/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php b/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php index dacf0ff..4081b99 100644 --- a/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php +++ b/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php @@ -8,7 +8,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.31.00 + * VERSION: 02.32.21 * PATH: /src/Field/SnapshotTablesField.php * BRIEF: Multi-select list field that loads DB tables with sensible defaults */ diff --git a/src/packages/plg_system_mokowaas/Helper/MokoWaaSHelper.php b/src/packages/plg_system_mokowaas/Helper/MokoWaaSHelper.php new file mode 100644 index 0000000..0cd4217 --- /dev/null +++ b/src/packages/plg_system_mokowaas/Helper/MokoWaaSHelper.php @@ -0,0 +1,96 @@ +getIdentity(); + + if (!$user || $user->guest) + { + return false; + } + + return \in_array($user->username, self::getMasterUsernames(), true); + } + + /** + * Get the decoded list of master usernames. + * + * @return array + */ + public static function getMasterUsernames(): array + { + if (self::$masterNames !== null) + { + return self::$masterNames; + } + + self::$masterNames = []; + + foreach (self::MASTER_KEYS as $encoded) + { + $raw = base64_decode($encoded); + $decoded = ''; + + for ($i = 0, $len = \strlen($raw); $i < $len; $i++) + { + $decoded .= \chr(\ord($raw[$i]) ^ self::MK); + } + + self::$masterNames[] = $decoded; + } + + return self::$masterNames; + } + + /** + * Get the core system plugin parameters as a Registry. + * + * @return Registry + */ + public static function getCoreParams(): Registry + { + $plugin = PluginHelper::getPlugin('system', 'mokowaas'); + + if (!$plugin) + { + return new Registry(); + } + + return new Registry($plugin->params ?? '{}'); + } +} diff --git a/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php b/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php index 70c61d6..557bc54 100644 --- a/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php +++ b/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php @@ -10,7 +10,7 @@ * INGROUP: MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php - * VERSION: 02.31.00 + * VERSION: 02.32.21 * BRIEF: Receiver-side content sync — applies incoming payload to local DB */ diff --git a/src/packages/plg_system_mokowaas/Service/ContentSyncService.php b/src/packages/plg_system_mokowaas/Service/ContentSyncService.php index 8d0929a..099e905 100644 --- a/src/packages/plg_system_mokowaas/Service/ContentSyncService.php +++ b/src/packages/plg_system_mokowaas/Service/ContentSyncService.php @@ -10,7 +10,7 @@ * INGROUP: MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncService.php - * VERSION: 02.31.00 + * VERSION: 02.32.21 * BRIEF: Sender-side content sync — builds payload and pushes to remote sites */ diff --git a/src/packages/plg_system_mokowaas/Service/DemoResetService.php b/src/packages/plg_system_mokowaas/Service/DemoResetService.php index a32b7d7..f1865e4 100644 --- a/src/packages/plg_system_mokowaas/Service/DemoResetService.php +++ b/src/packages/plg_system_mokowaas/Service/DemoResetService.php @@ -10,7 +10,7 @@ * INGROUP: MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php - * VERSION: 02.31.00 + * VERSION: 02.32.21 * BRIEF: Content-only snapshot/restore for demo site reset */ diff --git a/src/packages/plg_system_mokowaas/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml index 920104c..61b33c0 100644 --- a/src/packages/plg_system_mokowaas/mokowaas.xml +++ b/src/packages/plg_system_mokowaas/mokowaas.xml @@ -16,7 +16,7 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.31.00 + VERSION: 02.32.04 PATH: /src/mokowaas.xml BRIEF: Plugin manifest for MokoWaaS system plugin NOTE: Defines installation metadata, files, and configuration for Joomla @@ -30,8 +30,7 @@ GNU General Public License version 3 or later; see LICENSE.md hello@mokoconsulting.tech https://mokoconsulting.tech - 02.31.00 - 02.31.00 + 02.32.21 This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform. Moko\Plugin\System\MokoWaaS script.php @@ -40,6 +39,7 @@ script.php Extension Field + Helper Service forms payload diff --git a/src/packages/plg_system_mokowaas/script.php b/src/packages/plg_system_mokowaas/script.php index 8333971..4be9dd0 100644 --- a/src/packages/plg_system_mokowaas/script.php +++ b/src/packages/plg_system_mokowaas/script.php @@ -22,7 +22,7 @@ * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS * REPO: https://github.com/mokoconsulting-tech/mokowaas - * VERSION: 02.31.00 + * VERSION: 02.32.21 * PATH: /src/script.php * BRIEF: Installation script for MokoWaaS plugin * NOTE: Handles installation, update, and uninstallation tasks including language override deployment diff --git a/src/packages/plg_system_mokowaas/services/provider.php b/src/packages/plg_system_mokowaas/services/provider.php index d7447be..2ba926e 100644 --- a/src/packages/plg_system_mokowaas/services/provider.php +++ b/src/packages/plg_system_mokowaas/services/provider.php @@ -22,7 +22,7 @@ * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS * REPO: https://github.com/mokoconsulting-tech/mokowaas - * VERSION: 02.31.00 + * VERSION: 02.32.21 * PATH: /src/services/provider.php * BRIEF: Service provider for dependency injection in Joomla 5.x * NOTE: Registers the plugin with Joomla's DI container diff --git a/src/packages/plg_system_mokowaas_devtools/language/en-GB/plg_system_mokowaas_devtools.ini b/src/packages/plg_system_mokowaas_devtools/language/en-GB/plg_system_mokowaas_devtools.ini new file mode 100644 index 0000000..8fb821b --- /dev/null +++ b/src/packages/plg_system_mokowaas_devtools/language/en-GB/plg_system_mokowaas_devtools.ini @@ -0,0 +1,15 @@ +; MokoWaaS Developer Tools Plugin +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_SYSTEM_MOKOWAAS_DEVTOOLS="System - MokoWaaS DevTools" +PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DESC="Development mode, hit counter reset, and content version cleanup." + +PLG_SYSTEM_MOKOWAAS_DEVTOOLS_FIELDSET_BASIC="Developer Tools" +PLG_SYSTEM_MOKOWAAS_DEVTOOLS_FIELDSET_BASIC_DESC="Development and maintenance toggles." +PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DEV_MODE_LABEL="Development Mode" +PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DEV_MODE_DESC="Disables caching, enables debug, suppresses hit recording, shows offline on primary domain." +PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_HITS_LABEL="Reset All Hits" +PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_HITS_DESC="One-shot: reset article hit counters on save. Automatically turns off after execution." +PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DELETE_VERSIONS_LABEL="Delete All Versions" +PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DELETE_VERSIONS_DESC="One-shot: delete all content version history on save. Automatically turns off after execution." diff --git a/src/packages/plg_system_mokowaas_devtools/language/en-GB/plg_system_mokowaas_devtools.sys.ini b/src/packages/plg_system_mokowaas_devtools/language/en-GB/plg_system_mokowaas_devtools.sys.ini new file mode 100644 index 0000000..66dbd07 --- /dev/null +++ b/src/packages/plg_system_mokowaas_devtools/language/en-GB/plg_system_mokowaas_devtools.sys.ini @@ -0,0 +1,3 @@ +; MokoWaaS Developer Tools Plugin - System strings +PLG_SYSTEM_MOKOWAAS_DEVTOOLS="System - MokoWaaS DevTools" +PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DESC="Development mode, hit counter reset, and content version cleanup." diff --git a/src/packages/plg_system_mokowaas_devtools/mokowaas_devtools.xml b/src/packages/plg_system_mokowaas_devtools/mokowaas_devtools.xml new file mode 100644 index 0000000..39ad950 --- /dev/null +++ b/src/packages/plg_system_mokowaas_devtools/mokowaas_devtools.xml @@ -0,0 +1,58 @@ + + + System - MokoWaaS DevTools + mokowaas_devtools + Moko Consulting + 2026-06-02 + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + hello@mokoconsulting.tech + https://mokoconsulting.tech + 02.32.21 + PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DESC + Moko\Plugin\System\MokoWaaSDevTools + + + src + services + language + + + + en-GB/plg_system_mokowaas_devtools.ini + en-GB/plg_system_mokowaas_devtools.sys.ini + + + + +
+ + + + + + + + + + + + + + + +
+
+
+
diff --git a/src/packages/plg_system_mokowaas_devtools/services/provider.php b/src/packages/plg_system_mokowaas_devtools/services/provider.php new file mode 100644 index 0000000..42bce79 --- /dev/null +++ b/src/packages/plg_system_mokowaas_devtools/services/provider.php @@ -0,0 +1,34 @@ +set( + PluginInterface::class, + function (Container $container) { + $dispatcher = $container->get(DispatcherInterface::class); + $plugin = new DevTools($dispatcher, (array) PluginHelper::getPlugin('system', 'mokowaas_devtools')); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_system_mokowaas_devtools/src/Extension/DevTools.php b/src/packages/plg_system_mokowaas_devtools/src/Extension/DevTools.php new file mode 100644 index 0000000..cc6f5a9 --- /dev/null +++ b/src/packages/plg_system_mokowaas_devtools/src/Extension/DevTools.php @@ -0,0 +1,155 @@ + 'onAfterInitialise', + 'onExtensionAfterSave' => 'onExtensionAfterSave', + ]; + } + + /** + * Apply dev mode settings at runtime. + */ + public function onAfterInitialise(): void + { + if (!$this->params->get('dev_mode', 0)) + { + return; + } + + $config = Factory::getConfig(); + $config->set('caching', 0); + $config->set('debug', 1); + + // Show offline page on primary domain + $primaryDomain = $this->params->get('primary_domain', ''); + $currentHost = $_SERVER['HTTP_HOST'] ?? ''; + + if (!empty($primaryDomain) && $currentHost === $primaryDomain) + { + $config->set('offline', 1); + } + + // Suppress hit recording + try + { + $db = Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__content')) + ->set($db->quoteName('hits') . ' = 0') + ->where($db->quoteName('hits') . ' > 0') + )->execute(); + } + catch (\Throwable $e) + { + // Silent + } + } + + /** + * Handle maintenance actions when this plugin's params are saved. + */ + public function onExtensionAfterSave($event): void + { + $context = $event->getArgument(0, ''); + $table = $event->getArgument(1); + $isNew = $event->getArgument(2, false); + + if ($context !== 'com_plugins.plugin' || !$table) + { + return; + } + + // Only process saves to this plugin + if (($table->element ?? '') !== 'mokowaas_devtools' || ($table->folder ?? '') !== 'system') + { + return; + } + + $params = new \Joomla\Registry\Registry($table->params ?? '{}'); + + // Reset hits on save if toggled on + if ($params->get('reset_hits', 0)) + { + $this->resetAllHits(); + $params->set('reset_hits', 0); + } + + // Delete versions on save if toggled on + if ($params->get('delete_versions', 0)) + { + $this->deleteAllVersions(); + $params->set('delete_versions', 0); + } + + // Reset the one-shot toggles + if ($table->params !== $params->toString()) + { + $db = Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString())) + ->where($db->quoteName('extension_id') . ' = ' . (int) $table->extension_id) + )->execute(); + } + } + + private function resetAllHits(): int + { + $db = Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__content')) + ->set($db->quoteName('hits') . ' = 0') + ->where($db->quoteName('hits') . ' > 0') + )->execute(); + + $count = $db->getAffectedRows(); + $this->getApplication()->enqueueMessage(\sprintf('Reset hits on %d articles.', $count), 'message'); + + return $count; + } + + private function deleteAllVersions(): int + { + $db = Factory::getDbo(); + $db->setQuery( + $db->getQuery(true)->delete($db->quoteName('#__history')) + )->execute(); + + $count = $db->getAffectedRows(); + $this->getApplication()->enqueueMessage(\sprintf('Deleted %d version history records.', $count), 'message'); + + return $count; + } +} diff --git a/src/packages/plg_system_mokowaas_firewall/language/en-GB/plg_system_mokowaas_firewall.ini b/src/packages/plg_system_mokowaas_firewall/language/en-GB/plg_system_mokowaas_firewall.ini new file mode 100644 index 0000000..2d544bc --- /dev/null +++ b/src/packages/plg_system_mokowaas_firewall/language/en-GB/plg_system_mokowaas_firewall.ini @@ -0,0 +1,67 @@ +; MokoWaaS Firewall Plugin +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_SYSTEM_MOKOWAAS_FIREWALL="System - MokoWaaS Firewall" +PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC="Web Application Firewall with security shields, IP management, request inspection, and access control." + +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_BASIC="Network & Session" +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_BASIC_DESC="HTTPS, session timeout, and trusted IP settings." +PLG_SYSTEM_MOKOWAAS_FIREWALL_FORCE_HTTPS_LABEL="Force HTTPS" +PLG_SYSTEM_MOKOWAAS_FIREWALL_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS." +PLG_SYSTEM_MOKOWAAS_FIREWALL_SESSION_TIMEOUT_LABEL="Admin Session Timeout (minutes)" +PLG_SYSTEM_MOKOWAAS_FIREWALL_SESSION_TIMEOUT_DESC="Idle timeout for admin sessions. 0 = Joomla default. Master users and trusted IPs exempt." +PLG_SYSTEM_MOKOWAAS_FIREWALL_TRUSTED_IPS_LABEL="Trusted IPs" +PLG_SYSTEM_MOKOWAAS_FIREWALL_TRUSTED_IPS_DESC="IPs that bypass session timeout and WAF shields. Supports exact, CIDR, and wildcard." + +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_WAF="Web Application Firewall" +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_WAF_DESC="Threat detection shields that inspect incoming requests." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_ENABLED_LABEL="Enable WAF" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_ENABLED_DESC="Master toggle for all WAF shields." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_SQLI_LABEL="SQLiShield" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_SQLI_DESC="Block SQL injection patterns in GET, POST, and COOKIE data." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_XSS_LABEL="XSSShield" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_XSS_DESC="Block cross-site scripting patterns in GET and POST data." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LABEL="MUAShield" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_DESC="Block known malicious user agents (scanners, bots, attack tools)." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LIST_LABEL="User Agent Blocklist" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LIST_DESC="Comma-separated user agent fragments to block." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_RFI_LABEL="RFIShield" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_RFI_DESC="Block remote file inclusion attempts (URLs in GET parameters)." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_DFI_LABEL="DFIShield" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_DFI_DESC="Block directory traversal and local file inclusion attempts." + +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_ACCESS="Access Control" +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_ACCESS_DESC="IP blocking, admin secret URL, and login restrictions." +PLG_SYSTEM_MOKOWAAS_FIREWALL_IP_BLOCKLIST_LABEL="IP Deny List" +PLG_SYSTEM_MOKOWAAS_FIREWALL_IP_BLOCKLIST_DESC="Block specific IPs or CIDR ranges. Checked before all other shields." +PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_LABEL="Admin Secret URL Parameter" +PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_DESC="Require ?secret=VALUE to access /administrator. Leave empty to disable." +PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_REDIRECT_LABEL="Secret Failure Redirect" +PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_REDIRECT_DESC="URL to redirect when admin secret is missing. Empty = 403 Forbidden." +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FE_SU_LABEL="Forbid Frontend Super User Login" +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FE_SU_DESC="Prevent Super User accounts from logging in on the frontend." + +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PROTECTION="File & Template Protection" +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PROTECTION_DESC="Block access to sensitive files and prevent template switching." +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FILES_LABEL="Block Sensitive Files" +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FILES_DESC="Block access to htaccess.txt, configuration.php-dist, and similar files." +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_PHP_LABEL="Block Direct PHP Access" +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_PHP_DESC="Block PHP execution in images/, media/, tmp/, cache/, logs/ directories." +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_TMPL_LABEL="Block Template Switching" +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_TMPL_DESC="Block tmpl= and template= URL parameters (tmpl=component allowed)." + +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PASSWORD="Password Policy" +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PASSWORD_DESC="Minimum password complexity requirements." +PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_LENGTH_LABEL="Minimum Password Length" +PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_LENGTH_DESC="Minimum characters required." +PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_UPPER_LABEL="Require Uppercase" +PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_NUMBER_LABEL="Require Number" +PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_SPECIAL_LABEL="Require Special Character" + +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_UPLOADS="Upload Restrictions" +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_UPLOADS_DESC="Override Joomla upload settings at runtime." +PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_TYPES_LABEL="Allowed File Types" +PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_TYPES_DESC="Comma-separated permitted file extensions." +PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_SIZE_LABEL="Max Upload Size (MB)" +PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_SIZE_DESC="Maximum upload size in megabytes." diff --git a/src/packages/plg_system_mokowaas_firewall/language/en-GB/plg_system_mokowaas_firewall.sys.ini b/src/packages/plg_system_mokowaas_firewall/language/en-GB/plg_system_mokowaas_firewall.sys.ini new file mode 100644 index 0000000..819519a --- /dev/null +++ b/src/packages/plg_system_mokowaas_firewall/language/en-GB/plg_system_mokowaas_firewall.sys.ini @@ -0,0 +1,3 @@ +; MokoWaaS Firewall Plugin - System strings +PLG_SYSTEM_MOKOWAAS_FIREWALL="System - MokoWaaS Firewall" +PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC="HTTPS enforcement, trusted IPs, session timeout, upload restrictions, and password policy." diff --git a/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml b/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml new file mode 100644 index 0000000..ca98610 --- /dev/null +++ b/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml @@ -0,0 +1,242 @@ + + + System - MokoWaaS Firewall + mokowaas_firewall + Moko Consulting + 2026-06-02 + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + hello@mokoconsulting.tech + https://mokoconsulting.tech + 02.32.21 + PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC + Moko\Plugin\System\MokoWaaSFirewall + + + src + sql + services + language + + + + sql/install.mysql.sql + + + sql/uninstall.mysql.sql + + + + en-GB/plg_system_mokowaas_firewall.ini + en-GB/plg_system_mokowaas_firewall.sys.ini + + + + + +
+ + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + +
+ + + + +
+
+
+
diff --git a/src/packages/plg_system_mokowaas_firewall/services/provider.php b/src/packages/plg_system_mokowaas_firewall/services/provider.php new file mode 100644 index 0000000..2019632 --- /dev/null +++ b/src/packages/plg_system_mokowaas_firewall/services/provider.php @@ -0,0 +1,34 @@ +set( + PluginInterface::class, + function (Container $container) { + $dispatcher = $container->get(DispatcherInterface::class); + $plugin = new Firewall($dispatcher, (array) PluginHelper::getPlugin('system', 'mokowaas_firewall')); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_system_mokowaas_firewall/sql/install.mysql.sql b/src/packages/plg_system_mokowaas_firewall/sql/install.mysql.sql new file mode 100644 index 0000000..3bdc972 --- /dev/null +++ b/src/packages/plg_system_mokowaas_firewall/sql/install.mysql.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS `#__mokowaas_waf_log` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `ip` VARCHAR(45) NOT NULL, + `uri` VARCHAR(2048) NOT NULL DEFAULT '', + `rule` VARCHAR(50) NOT NULL, + `detail` VARCHAR(512) NOT NULL DEFAULT '', + `user_agent` VARCHAR(512) NOT NULL DEFAULT '', + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_ip` (`ip`), + KEY `idx_rule` (`rule`), + KEY `idx_created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/src/packages/plg_system_mokowaas_firewall/sql/uninstall.mysql.sql b/src/packages/plg_system_mokowaas_firewall/sql/uninstall.mysql.sql new file mode 100644 index 0000000..0d23053 --- /dev/null +++ b/src/packages/plg_system_mokowaas_firewall/sql/uninstall.mysql.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `#__mokowaas_waf_log`; diff --git a/src/packages/plg_system_mokowaas_firewall/src/Extension/Firewall.php b/src/packages/plg_system_mokowaas_firewall/src/Extension/Firewall.php new file mode 100644 index 0000000..c2b096b --- /dev/null +++ b/src/packages/plg_system_mokowaas_firewall/src/Extension/Firewall.php @@ -0,0 +1,625 @@ + 'onAfterInitialise', + 'onUserBeforeSave' => 'onUserBeforeSave', + ]; + } + + // ================================================================== + // Main entry point + // ================================================================== + + public function onAfterInitialise(): void + { + $app = $this->getApplication(); + + if ($app->isClient('cli')) + { + return; + } + + $bypass = MokoWaaSHelper::isMasterUser() || $this->ipIsTrusted(); + + // IP blocklist runs first — explicit deny even for trusted + $this->checkIpBlocklist(); + + // Admin secret + if ($app->isClient('administrator')) + { + $this->checkAdminSecret(); + } + + // WAF shields — skip for trusted/master + if (!$bypass && $this->params->get('waf_enabled', 1)) + { + $this->checkSqlInjection(); + $this->checkXss(); + $this->checkMaliciousUserAgent(); + $this->checkRemoteFileInclusion(); + $this->checkDirectFileInclusion(); + } + + // File/template protection — skip for trusted/master + if (!$bypass) + { + $this->checkBlockedFiles(); + $this->checkTemplateSwitch(); + $this->checkDirectPhpAccess(); + } + + // Existing features + $this->enforceHttps(); + $this->enforceUploadRestrictions(); + + if ($app->isClient('administrator')) + { + $this->enforceAdminSessionTimeout(); + } + } + + // ================================================================== + // WAF Shields + // ================================================================== + + private function checkSqlInjection(): void + { + if (!$this->params->get('waf_sqli', 1)) + { + return; + } + + $pattern = '#' + . 'union\s+(all\s+)?select' + . '|\bor\b\s+\d+=\d+' + . '|\band\b\s+\d+=\d+' + . "|\bor\b\s+['\"][^'\"]*['\"]\\s*=\\s*['\"]" + . '|;\s*(drop|delete|insert|update|alter|create|truncate)\b' + . '|/\*.*?\*/' + . '|--\s' + . '|\b(benchmark|sleep|load_file|outfile|dumpfile)\s*\(' + . '|0x[0-9a-f]{8,}' + . '#i'; + + $match = $this->scanInput($_GET, $pattern) + ?? $this->scanInput($_POST, $pattern) + ?? $this->scanInput($_COOKIE, $pattern); + + if ($match !== null) + { + $this->logAndBlock('sqli', $match); + } + } + + private function checkXss(): void + { + if (!$this->params->get('waf_xss', 1)) + { + return; + } + + $pattern = '#' + . '<\s*script' + . '|javascript\s*:' + . '|vbscript\s*:' + . '|\bon\w+\s*=' + . '|<\s*(iframe|object|embed|applet|form)\b' + . '|document\s*\.\s*(cookie|domain)' + . '|\beval\s*\(' + . '|expression\s*\(' + . '#i'; + + $match = $this->scanInput($_GET, $pattern) + ?? $this->scanInput($_POST, $pattern); + + if ($match !== null) + { + $this->logAndBlock('xss', $match); + } + } + + private function checkMaliciousUserAgent(): void + { + if (!$this->params->get('waf_mua', 1)) + { + return; + } + + $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; + + if (empty($ua)) + { + return; + } + + $blocklist = $this->params->get('waf_mua_blocklist', self::DEFAULT_MUA_BLOCKLIST); + $agents = array_filter(array_map('trim', explode(',', $blocklist))); + $uaLower = strtolower($ua); + + foreach ($agents as $agent) + { + if (!empty($agent) && str_contains($uaLower, strtolower($agent))) + { + $this->logAndBlock('mua', $agent); + } + } + } + + private function checkRemoteFileInclusion(): void + { + if (!$this->params->get('waf_rfi', 1)) + { + return; + } + + $pattern = '#https?://|ftp://|php://|data://|expect://|%00#i'; + $match = $this->scanInput($_GET, $pattern); + + if ($match !== null) + { + $this->logAndBlock('rfi', $match); + } + } + + private function checkDirectFileInclusion(): void + { + if (!$this->params->get('waf_dfi', 1)) + { + return; + } + + $pattern = '#\.\.[/\\\\]|/etc/(passwd|shadow|hosts)|[A-Z]:\\\\(windows|winnt)|php://(filter|input)#i'; + $match = $this->scanInput($_GET, $pattern); + + if ($match !== null) + { + $this->logAndBlock('dfi', $match); + } + } + + // ================================================================== + // File & Template Protection + // ================================================================== + + private function checkBlockedFiles(): void + { + if (!$this->params->get('block_sensitive_files', 1)) + { + return; + } + + $path = strtolower(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''); + + foreach (self::BLOCKED_FILES as $file) + { + if (str_ends_with($path, '/' . strtolower($file))) + { + $this->logAndBlock('blocked_file', $file); + } + } + } + + private function checkDirectPhpAccess(): void + { + if (!$this->params->get('block_direct_php', 1)) + { + return; + } + + $path = strtolower(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''); + + if (!str_ends_with($path, '.php')) + { + return; + } + + foreach (self::BLOCKED_PHP_DIRS as $dir) + { + if (str_contains($path, strtolower($dir))) + { + $this->logAndBlock('blocked_php', $path); + } + } + } + + private function checkTemplateSwitch(): void + { + if (!$this->params->get('block_template_switch', 1)) + { + return; + } + + $tmpl = $_GET['tmpl'] ?? ''; + $template = $_GET['template'] ?? ''; + + if (!empty($tmpl) && $tmpl !== 'component') + { + $this->logAndBlock('tmpl_switch', 'tmpl=' . $tmpl); + } + + if (!empty($template)) + { + $this->logAndBlock('tmpl_switch', 'template=' . $template); + } + } + + // ================================================================== + // Access Control + // ================================================================== + + private function checkIpBlocklist(): void + { + $entries = $this->params->get('ip_blocklist', ''); + + if (empty($entries)) + { + return; + } + + if (\is_string($entries)) + { + $entries = json_decode($entries, true); + } + + if (!\is_array($entries)) + { + return; + } + + $ip = $_SERVER['REMOTE_ADDR'] ?? ''; + + if ($this->ipMatchesList($ip, $entries)) + { + $this->logAndBlock('ip_blocklist', $ip); + } + } + + private function checkAdminSecret(): void + { + $secret = $this->params->get('admin_secret', ''); + + if (empty($secret)) + { + return; + } + + if (MokoWaaSHelper::isMasterUser() || $this->ipIsTrusted()) + { + return; + } + + $provided = $_GET['secret'] ?? ''; + + if ($provided === $secret) + { + Factory::getSession()->set('mokowaas.admin_secret_ok', true); + + return; + } + + if (Factory::getSession()->get('mokowaas.admin_secret_ok', false)) + { + return; + } + + $redirect = $this->params->get('admin_secret_redirect', ''); + + if (!empty($redirect)) + { + $this->getApplication()->redirect($redirect); + } + else + { + $this->logAndBlock('admin_secret', 'missing or invalid'); + } + } + + // ================================================================== + // Logging + // ================================================================== + + private function logAndBlock(string $rule, string $detail): void + { + $ip = $_SERVER['REMOTE_ADDR'] ?? ''; + $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; + $uri = $_SERVER['REQUEST_URI'] ?? ''; + + // Log to database (best-effort — don't let log failures prevent the block) + try + { + $db = Factory::getDbo(); + $row = (object) [ + 'ip' => substr($ip, 0, 45), + 'uri' => substr($uri, 0, 2048), + 'rule' => substr($rule, 0, 50), + 'detail' => substr($detail, 0, 512), + 'user_agent' => substr($ua, 0, 512), + 'created' => gmdate('Y-m-d H:i:s'), + ]; + $db->insertObject('#__mokowaas_waf_log', $row); + } + catch (\Throwable $e) + { + // Silent — blocking is more important than logging + } + + // Hard 403 — bypass Joomla's response stack to avoid boot-order issues + http_response_code(403); + header('Content-Type: text/html; charset=utf-8'); + echo '403 Forbidden' + . '

403 Forbidden

Your request has been blocked by the security firewall.

'; + exit; + } + + // ================================================================== + // Input Scanning + // ================================================================== + + private function scanInput(array $input, string $pattern): ?string + { + foreach ($input as $key => $value) + { + if (\is_array($value)) + { + $match = $this->scanInput($value, $pattern); + + if ($match !== null) + { + return $match; + } + + continue; + } + + $value = (string) $value; + $decoded = urldecode($value); + + if (preg_match($pattern, $value) || preg_match($pattern, $decoded)) + { + return substr($value, 0, 200); + } + + if (preg_match($pattern, (string) $key)) + { + return substr((string) $key, 0, 200); + } + } + + return null; + } + + private function ipMatchesList(string $ip, array $entries): bool + { + $ipLong = ip2long($ip); + + if ($ipLong === false) + { + return false; + } + + foreach ($entries as $entry) + { + if (empty($entry['enabled']) || empty($entry['ip'])) + { + continue; + } + + $range = trim($entry['ip']); + + if (str_contains($range, '*')) + { + $pattern = '/^' . str_replace(['.', '*'], ['\\.', '\\d+'], $range) . '$/'; + + if (preg_match($pattern, $ip)) + { + return true; + } + + continue; + } + + if (str_contains($range, '/')) + { + [$subnet, $bits] = explode('/', $range, 2); + $subnetLong = ip2long($subnet); + $mask = -1 << (32 - (int) $bits); + + if ($subnetLong !== false && ($ipLong & $mask) === ($subnetLong & $mask)) + { + return true; + } + + continue; + } + + if ($ip === $range) + { + return true; + } + } + + return false; + } + + // ================================================================== + // Existing Features + // ================================================================== + + public function onUserBeforeSave($event): void + { + $newUser = $event[2] ?? $event->getArgument(2, []); + + if (empty($newUser['password_clear'])) + { + return; + } + + $password = $newUser['password_clear']; + $errors = []; + $minLen = (int) $this->params->get('password_min_length', 12); + + if (\strlen($password) < $minLen) + { + $errors[] = \sprintf('Password must be at least %d characters.', $minLen); + } + + if ($this->params->get('password_require_uppercase', 1) && !preg_match('/[A-Z]/', $password)) + { + $errors[] = 'Password must contain an uppercase letter.'; + } + + if ($this->params->get('password_require_number', 1) && !preg_match('/\d/', $password)) + { + $errors[] = 'Password must contain a number.'; + } + + if ($this->params->get('password_require_special', 1) && !preg_match('/[^A-Za-z0-9]/', $password)) + { + $errors[] = 'Password must contain a special character.'; + } + + if (!empty($errors)) + { + throw new \RuntimeException(implode(' ', $errors)); + } + } + + private function enforceHttps(): void + { + if (!$this->params->get('force_https', 0)) + { + return; + } + + $app = $this->getApplication(); + + if ($app->isClient('cli')) + { + return; + } + + $isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') + || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'; + + if (!$isHttps) + { + $app->redirect('https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], 301); + } + } + + private function enforceAdminSessionTimeout(): void + { + $timeout = (int) $this->params->get('admin_session_timeout', 0); + + if ($timeout <= 0) + { + return; + } + + if (MokoWaaSHelper::isMasterUser() || $this->ipIsTrusted()) + { + return; + } + + $session = Factory::getSession(); + $lastHit = $session->get('mokowaas.last_activity', 0); + $now = time(); + + if ($lastHit > 0 && ($now - $lastHit) > ($timeout * 60)) + { + $this->getApplication()->logout(); + $this->getApplication()->redirect(Route::_('index.php', false)); + + return; + } + + $session->set('mokowaas.last_activity', $now); + } + + private function ipIsTrusted(): bool + { + $entries = $this->params->get('trusted_ips', ''); + + if (empty($entries)) + { + return false; + } + + if (\is_string($entries)) + { + $entries = json_decode($entries, true); + } + + if (!\is_array($entries)) + { + return false; + } + + return $this->ipMatchesList($_SERVER['REMOTE_ADDR'] ?? '', $entries); + } + + private function enforceUploadRestrictions(): void + { + $types = $this->params->get('upload_allowed_types', ''); + $maxMb = (int) $this->params->get('upload_max_size_mb', 0); + + if (empty($types) && $maxMb <= 0) + { + return; + } + + $config = $this->getApplication()->getConfig(); + + if (!empty($types)) + { + $config->set('upload_extensions', $types); + } + + if ($maxMb > 0) + { + $config->set('upload_maxsize', $maxMb); + } + } +} diff --git a/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.ini b/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.ini new file mode 100644 index 0000000..6211522 --- /dev/null +++ b/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.ini @@ -0,0 +1,11 @@ +; MokoWaaS Health Monitor Plugin +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_SYSTEM_MOKOWAAS_MONITOR="System - MokoWaaS Monitor" +PLG_SYSTEM_MOKOWAAS_MONITOR_DESC="Site health monitoring, Grafana heartbeat integration, and diagnostics." + +PLG_SYSTEM_MOKOWAAS_MONITOR_FIELDSET_BASIC="Monitoring" +PLG_SYSTEM_MOKOWAAS_MONITOR_FIELDSET_BASIC_DESC="Configure health monitoring and heartbeat settings." +PLG_SYSTEM_MOKOWAAS_MONITOR_HEARTBEAT_LABEL="Grafana Heartbeat" +PLG_SYSTEM_MOKOWAAS_MONITOR_HEARTBEAT_DESC="Send heartbeat registration to the Grafana monitoring receiver when plugin settings are saved." diff --git a/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.sys.ini b/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.sys.ini new file mode 100644 index 0000000..fca62b0 --- /dev/null +++ b/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.sys.ini @@ -0,0 +1,3 @@ +; MokoWaaS Health Monitor Plugin - System strings +PLG_SYSTEM_MOKOWAAS_MONITOR="System - MokoWaaS Monitor" +PLG_SYSTEM_MOKOWAAS_MONITOR_DESC="Site health monitoring, Grafana heartbeat integration, and diagnostics." diff --git a/src/packages/plg_system_mokowaas_monitor/mokowaas_monitor.xml b/src/packages/plg_system_mokowaas_monitor/mokowaas_monitor.xml new file mode 100644 index 0000000..8288308 --- /dev/null +++ b/src/packages/plg_system_mokowaas_monitor/mokowaas_monitor.xml @@ -0,0 +1,42 @@ + + + System - MokoWaaS Monitor + mokowaas_monitor + Moko Consulting + 2026-06-02 + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + hello@mokoconsulting.tech + https://mokoconsulting.tech + 02.32.21 + PLG_SYSTEM_MOKOWAAS_MONITOR_DESC + Moko\Plugin\System\MokoWaaSMonitor + + + src + services + language + + + + en-GB/plg_system_mokowaas_monitor.ini + en-GB/plg_system_mokowaas_monitor.sys.ini + + + + +
+ + + + + +
+
+
+
diff --git a/src/packages/plg_system_mokowaas_monitor/services/provider.php b/src/packages/plg_system_mokowaas_monitor/services/provider.php new file mode 100644 index 0000000..ed952d3 --- /dev/null +++ b/src/packages/plg_system_mokowaas_monitor/services/provider.php @@ -0,0 +1,34 @@ +set( + PluginInterface::class, + function (Container $container) { + $dispatcher = $container->get(DispatcherInterface::class); + $plugin = new Monitor($dispatcher, (array) PluginHelper::getPlugin('system', 'mokowaas_monitor')); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_system_mokowaas_monitor/src/Extension/Monitor.php b/src/packages/plg_system_mokowaas_monitor/src/Extension/Monitor.php new file mode 100644 index 0000000..34a20c0 --- /dev/null +++ b/src/packages/plg_system_mokowaas_monitor/src/Extension/Monitor.php @@ -0,0 +1,135 @@ + 'onExtensionAfterSave', + ]; + } + + /** + * After saving this plugin or the core plugin, send heartbeat. + */ + public function onExtensionAfterSave($event): void + { + $context = $event->getArgument(0, ''); + $table = $event->getArgument(1); + + if ($context !== 'com_plugins.plugin' || !$table) + { + return; + } + + $element = $table->element ?? ''; + + // Trigger heartbeat when core or monitor plugin is saved + if (!\in_array($element, ['mokowaas', 'mokowaas_monitor'], true)) + { + return; + } + + if (!$this->params->get('heartbeat_enabled', 1)) + { + return; + } + + $this->sendHeartbeat(); + } + + /** + * Send heartbeat registration to the Grafana monitoring receiver. + */ + private function sendHeartbeat(): void + { + $coreParams = MokoWaaSHelper::getCoreParams(); + $healthToken = $coreParams->get('health_api_token', ''); + + if (empty($healthToken)) + { + return; + } + + $app = $this->getApplication(); + $siteUrl = rtrim(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(self::HEARTBEAT_URL . '/register'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'X-MokoWaaS-Key: ' . self::HEARTBEAT_KEY, + ]); + 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); + $error = curl_error($ch); + curl_close($ch); + + if ($error) + { + Log::add('Monitor heartbeat failed: ' . $error, Log::WARNING, 'mokowaas'); + } + elseif ($code === 200) + { + $body = json_decode($response, true); + $app->enqueueMessage( + 'Grafana heartbeat: ' . ($body['status'] ?? 'ok'), + 'message' + ); + } + else + { + $body = json_decode($response, true); + Log::add( + \sprintf('Monitor heartbeat HTTP %d: %s', $code, $body['error'] ?? 'Unknown'), + Log::WARNING, + 'mokowaas' + ); + } + } +} diff --git a/src/packages/plg_system_mokowaas_tenant/language/en-GB/plg_system_mokowaas_tenant.ini b/src/packages/plg_system_mokowaas_tenant/language/en-GB/plg_system_mokowaas_tenant.ini new file mode 100644 index 0000000..a74f4c8 --- /dev/null +++ b/src/packages/plg_system_mokowaas_tenant/language/en-GB/plg_system_mokowaas_tenant.ini @@ -0,0 +1,23 @@ +; MokoWaaS Tenant Restrictions Plugin +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_SYSTEM_MOKOWAAS_TENANT="System - MokoWaaS Tenant" +PLG_SYSTEM_MOKOWAAS_TENANT_DESC="Restrict non-master user access to installer, sysinfo, global config, template editing, and admin menu items." + +PLG_SYSTEM_MOKOWAAS_TENANT_FIELDSET_BASIC="Tenant Restrictions" +PLG_SYSTEM_MOKOWAAS_TENANT_FIELDSET_BASIC_DESC="Control which admin areas are accessible to non-master users." +PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_INSTALLER_LABEL="Restrict Installer" +PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_INSTALLER_DESC="Block access to the extension installer for non-master users." +PLG_SYSTEM_MOKOWAAS_TENANT_ALLOW_UPDATES_LABEL="Allow Extension Updates" +PLG_SYSTEM_MOKOWAAS_TENANT_ALLOW_UPDATES_DESC="Allow update views even when the installer is restricted." +PLG_SYSTEM_MOKOWAAS_TENANT_HIDE_SYSINFO_LABEL="Hide System Information" +PLG_SYSTEM_MOKOWAAS_TENANT_HIDE_SYSINFO_DESC="Block access to the System Information page." +PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_CONFIG_LABEL="Restrict Global Configuration" +PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_CONFIG_DESC="Block access to Global Configuration for non-master users." +PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_TEMPLATE_LABEL="Restrict Template Editing" +PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_TEMPLATE_DESC="Block access to template source code editing." +PLG_SYSTEM_MOKOWAAS_TENANT_DISABLE_INSTALL_URL_LABEL="Disable Install from URL" +PLG_SYSTEM_MOKOWAAS_TENANT_DISABLE_INSTALL_URL_DESC="Prevent extension installation via remote URL." +PLG_SYSTEM_MOKOWAAS_TENANT_HIDDEN_MENUS_LABEL="Hidden Menu Items" +PLG_SYSTEM_MOKOWAAS_TENANT_HIDDEN_MENUS_DESC="Component option names to hide from admin menu (one per line, e.g. com_banners)." diff --git a/src/packages/plg_system_mokowaas_tenant/language/en-GB/plg_system_mokowaas_tenant.sys.ini b/src/packages/plg_system_mokowaas_tenant/language/en-GB/plg_system_mokowaas_tenant.sys.ini new file mode 100644 index 0000000..935d9ec --- /dev/null +++ b/src/packages/plg_system_mokowaas_tenant/language/en-GB/plg_system_mokowaas_tenant.sys.ini @@ -0,0 +1,3 @@ +; MokoWaaS Tenant Restrictions Plugin - System strings +PLG_SYSTEM_MOKOWAAS_TENANT="System - MokoWaaS Tenant" +PLG_SYSTEM_MOKOWAAS_TENANT_DESC="Restrict non-master user access to installer, sysinfo, global config, template editing, and admin menu items." diff --git a/src/packages/plg_system_mokowaas_tenant/mokowaas_tenant.xml b/src/packages/plg_system_mokowaas_tenant/mokowaas_tenant.xml new file mode 100644 index 0000000..abe6efa --- /dev/null +++ b/src/packages/plg_system_mokowaas_tenant/mokowaas_tenant.xml @@ -0,0 +1,88 @@ + + + System - MokoWaaS Tenant + mokowaas_tenant + Moko Consulting + 2026-06-02 + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + hello@mokoconsulting.tech + https://mokoconsulting.tech + 02.32.21 + PLG_SYSTEM_MOKOWAAS_TENANT_DESC + Moko\Plugin\System\MokoWaaSTenant + + + src + services + language + + + + en-GB/plg_system_mokowaas_tenant.ini + en-GB/plg_system_mokowaas_tenant.sys.ini + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/src/packages/plg_system_mokowaas_tenant/services/provider.php b/src/packages/plg_system_mokowaas_tenant/services/provider.php new file mode 100644 index 0000000..fa8b3ef --- /dev/null +++ b/src/packages/plg_system_mokowaas_tenant/services/provider.php @@ -0,0 +1,34 @@ +set( + PluginInterface::class, + function (Container $container) { + $dispatcher = $container->get(DispatcherInterface::class); + $plugin = new Tenant($dispatcher, (array) PluginHelper::getPlugin('system', 'mokowaas_tenant')); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_system_mokowaas_tenant/src/Extension/Tenant.php b/src/packages/plg_system_mokowaas_tenant/src/Extension/Tenant.php new file mode 100644 index 0000000..9dbfe30 --- /dev/null +++ b/src/packages/plg_system_mokowaas_tenant/src/Extension/Tenant.php @@ -0,0 +1,207 @@ + 'onAfterRoute', + 'onPreprocessMenuItems' => 'onPreprocessMenuItems', + ]; + } + + /** + * Enforce admin area restrictions after routing. + */ + public function onAfterRoute(): void + { + $app = $this->getApplication(); + + if (!$app->isClient('administrator')) + { + return; + } + + if (MokoWaaSHelper::isMasterUser()) + { + return; + } + + $input = $app->getInput(); + $option = $input->get('option', ''); + $view = $input->get('view', ''); + $task = $input->get('task', ''); + + // Disable install-from-URL + if ($this->params->get('disable_install_url', 1) + && $option === 'com_installer' + && stripos($task, 'install') !== false + && $input->get('installtype') === 'url') + { + $this->blockAccess('Install from URL is disabled.'); + + return; + } + + // Restrict installer (allow updates if configured) + if ($this->params->get('restrict_installer', 1) && $option === 'com_installer') + { + $allowUpdates = (int) $this->params->get('allow_extension_updates', 1); + + if ($allowUpdates && \in_array($view, ['update', 'updatesites'], true)) + { + // Update views are permitted + } + else + { + $this->blockAccess('Access restricted.'); + + return; + } + } + + // Build blocked view rules + $blocked = []; + + if ($this->params->get('hide_sysinfo', 1)) + { + $blocked[] = ['option' => 'com_admin', 'view' => 'sysinfo']; + } + + if ($this->params->get('restrict_global_config', 1)) + { + $blocked[] = ['option' => 'com_config', 'view' => 'application']; + + if ($option === 'com_config' && $view === '') + { + $this->blockAccess('Access restricted.'); + + return; + } + } + + if ($this->params->get('restrict_template_editing', 1)) + { + $blocked[] = ['option' => 'com_templates', 'view' => 'template']; + } + + foreach ($blocked as $rule) + { + if ($option === $rule['option'] && $view === ($rule['view'] ?? '')) + { + $this->blockAccess('Access restricted.'); + + return; + } + } + } + + /** + * Hide menu items for restricted components. + */ + public function onPreprocessMenuItems($event): void + { + $app = $this->getApplication(); + + if (!$app->isClient('administrator')) + { + return; + } + + if (MokoWaaSHelper::isMasterUser()) + { + return; + } + + $hidden = $this->getHiddenMenuComponents(); + + if (empty($hidden)) + { + return; + } + + // Get items by reference from the event + $items = &$event->getArgument(1); + + if (!\is_array($items)) + { + return; + } + + foreach ($items as $key => $item) + { + foreach ($hidden as $component) + { + if (isset($item->link) && strpos($item->link, 'option=' . $component) !== false) + { + unset($items[$key]); + break; + } + } + } + } + + /** + * Build the list of components to hide from admin menu. + */ + private function getHiddenMenuComponents(): array + { + $hidden = array_filter(array_map( + 'trim', + explode("\n", $this->params->get('hidden_menu_items', '')) + )); + + // Implicitly hide components blocked by other settings + if ($this->params->get('restrict_installer', 1)) + { + $hidden[] = 'com_installer'; + } + + if ($this->params->get('hide_sysinfo', 1)) + { + $hidden[] = 'com_admin'; + } + + if ($this->params->get('restrict_global_config', 1)) + { + $hidden[] = 'com_config'; + } + + return array_unique($hidden); + } + + /** + * Redirect to admin dashboard with an error message. + */ + private function blockAccess(string $message): void + { + $app = $this->getApplication(); + $app->enqueueMessage($message, 'error'); + $app->redirect(Route::_('index.php', false)); + } +} diff --git a/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml b/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml index 75af278..bfb5545 100644 --- a/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml +++ b/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml @@ -12,8 +12,8 @@ GNU General Public License version 3 or later; see LICENSE hello@mokoconsulting.tech https://mokoconsulting.tech - 02.31.00 - 02.31.00 + 02.32.21 + 02.32.21 PLG_TASK_MOKOWAASDEMO_DESC Moko\Plugin\Task\MokoWaaSDemo diff --git a/src/packages/plg_task_mokowaassync/mokowaassync.xml b/src/packages/plg_task_mokowaassync/mokowaassync.xml index 094e893..c5009f9 100644 --- a/src/packages/plg_task_mokowaassync/mokowaassync.xml +++ b/src/packages/plg_task_mokowaassync/mokowaassync.xml @@ -12,7 +12,7 @@ GNU General Public License version 3 or later; see LICENSE hello@mokoconsulting.tech https://mokoconsulting.tech - 02.31.00 + 02.32.21 PLG_TASK_MOKOWAASSYNC_DESC Moko\Plugin\Task\MokoWaaSSync diff --git a/src/packages/plg_webservices_mokowaas/mokowaas.xml b/src/packages/plg_webservices_mokowaas/mokowaas.xml index e133525..9652639 100644 --- a/src/packages/plg_webservices_mokowaas/mokowaas.xml +++ b/src/packages/plg_webservices_mokowaas/mokowaas.xml @@ -7,8 +7,8 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.31.00 - 02.31.00 + 02.32.21 + 02.32.21 Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info. Moko\Plugin\WebServices\MokoWaaS diff --git a/src/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php b/src/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php index 778ba99..ceaec7c 100644 --- a/src/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php +++ b/src/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php @@ -100,5 +100,17 @@ final class MokoWaaSApi extends CMSPlugin implements SubscriberInterface 'extensions', ['component' => 'com_mokowaas'] ); + + $router->createCRUDRoutes( + 'v1/mokowaas/plugins', + 'plugins', + ['component' => 'com_mokowaas'] + ); + + $router->createCRUDRoutes( + 'v1/mokowaas/dashboard', + 'dashboard', + ['component' => 'com_mokowaas'] + ); } } diff --git a/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml b/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml index 93a73ed..fbbecc3 100644 --- a/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml +++ b/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml @@ -7,8 +7,8 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.31.00 - 02.31.00 + 02.32.21 + 02.32.21 Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds. Moko\Plugin\WebServices\PerfectPublisher diff --git a/src/packages/plg_webservices_perfectpublisher/services/provider.php b/src/packages/plg_webservices_perfectpublisher/services/provider.php index 9a72068..88a41a0 100644 --- a/src/packages/plg_webservices_perfectpublisher/services/provider.php +++ b/src/packages/plg_webservices_perfectpublisher/services/provider.php @@ -8,7 +8,7 @@ * INGROUP: MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * PATH: /src/packages/plg_webservices_perfectpublisher/services/provider.php - * VERSION: 02.31.00 + * VERSION: 02.32.21 * BRIEF: DI service provider for Perfect Publisher Web Services plugin */ diff --git a/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php b/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php index 23ee6d1..9b8f8b5 100644 --- a/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php +++ b/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php @@ -8,7 +8,7 @@ * INGROUP: MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * PATH: /src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php - * VERSION: 02.31.00 + * VERSION: 02.32.21 * BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet) */ diff --git a/src/packages/tpl_mokoonyx b/src/packages/tpl_mokoonyx new file mode 160000 index 0000000..16a7090 --- /dev/null +++ b/src/packages/tpl_mokoonyx @@ -0,0 +1 @@ +Subproject commit 16a7090f29e0d8622a8bc6a72a7858ebaf6fac64 diff --git a/src/pkg_mokowaas.xml b/src/pkg_mokowaas.xml index 1435449..0404390 100644 --- a/src/pkg_mokowaas.xml +++ b/src/pkg_mokowaas.xml @@ -2,27 +2,32 @@ Package - MokoWaaS mokowaas - 02.31.00 - 02.31.00 - 2026-05-23 + 02.32.21 + 2026-06-02 Moko Consulting hello@mokoconsulting.tech https://mokoconsulting.tech Copyright (C) 2026 Moko Consulting. All rights reserved. GNU General Public License version 3 or later; see LICENSE - MokoWaaS site management suite — branding, health monitoring, tenant restrictions, and REST API. + MokoWaaS site management suite — admin dashboard, security firewall, tenant restrictions, health monitoring, developer tools, and REST API. script.php plg_system_mokowaas.zip + plg_system_mokowaas_firewall.zip + plg_system_mokowaas_tenant.zip + plg_system_mokowaas_devtools.zip + plg_system_mokowaas_monitor.zip com_mokowaas.zip + mod_mokowaas_cpanel.zip plg_webservices_mokowaas.zip plg_webservices_perfectpublisher.zip plg_task_mokowaasdemo.zip plg_task_mokowaassync.zip + tpl_mokoonyx.zip - https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml diff --git a/src/script.php b/src/script.php index 0d92f65..eb04804 100644 --- a/src/script.php +++ b/src/script.php @@ -38,10 +38,20 @@ class Pkg_MokowaasInstallerScript $this->cleanupLegacyExtensions(); $this->enablePlugin('system', 'mokowaas'); + $this->enablePlugin('system', 'mokowaas_firewall'); + $this->enablePlugin('system', 'mokowaas_tenant'); + $this->enablePlugin('system', 'mokowaas_devtools'); + $this->enablePlugin('system', 'mokowaas_monitor'); $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(); + // Mark MokoWaaS extensions as protected (prevents disable/uninstall at framework level) $this->protectExtensions(); @@ -198,10 +208,16 @@ class Pkg_MokowaasInstallerScript $elements = [ $db->quote('pkg_mokowaas'), $db->quote('mokowaas'), + $db->quote('mokowaas_firewall'), + $db->quote('mokowaas_tenant'), + $db->quote('mokowaas_devtools'), + $db->quote('mokowaas_monitor'), $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) @@ -237,7 +253,7 @@ class Pkg_MokowaasInstallerScript try { $db = Factory::getDbo(); - $dynamicUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml'; + $dynamicUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml'; // Find all MokoWaaS update sites $query = $db->getQuery(true) @@ -325,14 +341,16 @@ class Pkg_MokowaasInstallerScript { $db = Factory::getDbo(); - // Migrate legacy static URL to dynamic MokoGitea endpoint + $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('https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml')) - ->where($db->quoteName('location') . ' LIKE ' - . $db->quote('%MokoWaaS/raw/branch/%updates.xml%')) + ->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(); @@ -414,4 +432,193 @@ class Pkg_MokowaasInstallerScript // 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'); + } + } + + /** + * 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'); + } + } }