chore(release): stable release #134

Merged
jmiller merged 79 commits from dev into main 2026-06-02 18:53:24 +00:00
89 changed files with 4843 additions and 74 deletions
+4
View File
@@ -0,0 +1,4 @@
[submodule "src/packages/tpl_mokoonyx"]
path = src/packages/tpl_mokoonyx
url = https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
branch = main
+1 -1
View File
@@ -9,7 +9,7 @@
<display-name>Package - MokoWaaS</display-name>
<org>MokoConsulting</org>
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
<version>02.31.00</version>
<version>02.32.21</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
+2
View File
@@ -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: |
+1 -1
View File
@@ -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"
+1
View File
@@ -51,6 +51,7 @@ jobs:
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
submodules: recursive
- name: Setup moko-platform tools
env:
+25 -1
View File
@@ -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)
+1 -1
View File
@@ -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
-->
+1 -1
View File
@@ -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
-->
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
-->
+1 -1
View File
@@ -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
-->
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
-->
@@ -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"
@@ -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"
@@ -0,0 +1,125 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
class DisplayController extends BaseController
{
protected $default_view = 'dashboard';
public function display($cachable = false, $urlparams = [])
{
return parent::display($cachable, $urlparams);
}
/**
* Toggle a MokoWaaS feature plugin on or off.
*
* Expects POST with extension_id and enabled (0 or 1).
* Returns JSON response for AJAX calls.
*/
public function togglePlugin()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
$app = Factory::getApplication();
$input = $app->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();
}
}
@@ -0,0 +1,425 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Version;
class DashboardModel extends BaseDatabaseModel
{
/**
* Feature plugin metadata keyed by element name.
* Provides icon, category, and description for dashboard display.
*/
private const PLUGIN_META = [
'mokowaas' => [
'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 [];
}
}
}
@@ -0,0 +1,305 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
/**
* Extension manager model — fetches Moko Consulting Joomla packages
* from the Gitea API and checks local install status.
*
* @since 02.32.00
*/
class ExtensionsModel extends BaseDatabaseModel
{
/**
* Curated catalog of Moko Consulting Joomla packages.
* Each entry maps a Gitea repo name to local extension metadata.
*/
private const CATALOG = [
'MokoWaaS' => [
'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();
}
}
@@ -0,0 +1,58 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\View\Dashboard;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $plugins = [];
protected $siteInfo;
protected $recentLogins = [];
protected $pendingUpdates = [];
protected $checkedOutItems = [];
protected $wafBlocks = [];
public function display($tpl = null)
{
$model = $this->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');
}
}
}
@@ -0,0 +1,41 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\View\Extensions;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $packages = [];
public function display($tpl = null)
{
$model = $this->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');
}
}
@@ -0,0 +1,269 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
/** @var \Moko\Component\MokoWaaS\Administrator\View\Dashboard\HtmlView $this */
$siteInfo = $this->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'];
?>
<div id="mokowaas-dashboard">
<!-- Site Info Bar -->
<div class="mokowaas-info-bar card mb-4">
<div class="card-body d-flex flex-wrap align-items-center gap-4">
<div class="mokowaas-info-item">
<span class="mokowaas-info-label"><?php echo Text::_('COM_MOKOWAAS_SITE'); ?></span>
<span class="mokowaas-info-value fw-bold"><?php echo $this->escape($siteInfo->sitename); ?></span>
</div>
<div class="mokowaas-info-item">
<span class="mokowaas-info-label">MokoWaaS</span>
<span class="mokowaas-info-value"><span class="badge bg-primary"><?php echo $this->escape($siteInfo->mokowaas_version); ?></span></span>
</div>
<div class="mokowaas-info-item">
<span class="mokowaas-info-label">Joomla</span>
<span class="mokowaas-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->joomla_version); ?></span></span>
</div>
<div class="mokowaas-info-item">
<span class="mokowaas-info-label">PHP</span>
<span class="mokowaas-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->php_version); ?></span></span>
</div>
<div class="mokowaas-info-item">
<span class="mokowaas-info-label"><?php echo Text::_('COM_MOKOWAAS_DATABASE'); ?></span>
<span class="mokowaas-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->db_type); ?></span></span>
</div>
<?php if ($siteInfo->debug): ?>
<span class="badge bg-warning text-dark"><?php echo Text::_('COM_MOKOWAAS_DEBUG_ON'); ?></span>
<?php endif; ?>
<?php if ($siteInfo->offline): ?>
<span class="badge bg-danger"><?php echo Text::_('COM_MOKOWAAS_OFFLINE'); ?></span>
<?php endif; ?>
</div>
</div>
<!-- Quick Actions (large buttons) -->
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<button type="button" class="btn btn-outline-primary w-100 py-3" id="mokowaas-btn-cache"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.clearCache&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-trash d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
<?php echo Text::_('COM_MOKOWAAS_CLEAR_CACHE'); ?>
</button>
</div>
<div class="col-12 col-md-4">
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="btn btn-outline-primary w-100 py-3">
<span class="icon-refresh d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
<?php echo Text::_('COM_MOKOWAAS_CHECK_UPDATES'); ?>
</a>
</div>
<div class="col-12 col-md-4">
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=extensions'); ?>" class="btn btn-outline-primary w-100 py-3">
<span class="icon-puzzle-piece d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
<?php echo Text::_('COM_MOKOWAAS_EXTENSIONS_LINK'); ?>
</a>
</div>
</div>
<!-- Three-column layout: plugins left, tables right -->
<div class="row">
<!-- Left: Feature Plugin Grid (8 cols) -->
<div class="col-12 col-xl-8">
<?php foreach ($categoryOrder as $catKey): ?>
<?php if (empty($grouped[$catKey])) continue; ?>
<?php
$catPlugins = $grouped[$catKey];
$first = $catPlugins[0];
?>
<h3 class="mokowaas-category-heading mb-3">
<span class="badge <?php echo $this->escape($first->categoryBadge); ?>"><?php echo $this->escape($first->categoryLabel); ?></span>
</h3>
<div class="mokowaas-plugin-grid row g-3 mb-4">
<?php foreach ($catPlugins as $plugin): ?>
<div class="col-12 col-md-6">
<div class="card mokowaas-plugin-card h-100 <?php echo $plugin->enabled ? '' : 'mokowaas-plugin-disabled'; ?>"
data-extension-id="<?php echo $plugin->extension_id; ?>">
<div class="card-body d-flex flex-column">
<div class="d-flex align-items-start justify-content-between mb-2">
<div class="d-flex align-items-center gap-2">
<span class="<?php echo $this->escape($plugin->icon); ?> mokowaas-plugin-icon" aria-hidden="true"></span>
<h5 class="card-title mb-0"><?php echo $this->escape($plugin->name); ?></h5>
</div>
<?php if ($plugin->version): ?>
<span class="badge bg-light text-dark"><?php echo $this->escape($plugin->version); ?></span>
<?php endif; ?>
</div>
<p class="card-text text-muted small flex-grow-1"><?php echo $this->escape($plugin->description); ?></p>
<div class="d-flex align-items-center justify-content-between mt-auto pt-2 border-top">
<?php if ($plugin->protected): ?>
<span class="badge bg-dark"><?php echo Text::_('COM_MOKOWAAS_PROTECTED'); ?></span>
<?php else: ?>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input mokowaas-toggle" role="switch"
id="toggle-<?php echo $plugin->extension_id; ?>"
data-extension-id="<?php echo $plugin->extension_id; ?>"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.togglePlugin&format=json'); ?>"
data-token="<?php echo $token; ?>"
<?php echo $plugin->enabled ? 'checked' : ''; ?>>
<label class="form-check-label small" for="toggle-<?php echo $plugin->extension_id; ?>">
<?php echo $plugin->enabled ? Text::_('COM_MOKOWAAS_ENABLED') : Text::_('COM_MOKOWAAS_DISABLED'); ?>
</label>
</div>
<?php endif; ?>
<?php if ($plugin->type === 'plugin'): ?>
<a href="<?php echo Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $plugin->extension_id); ?>" class="btn btn-sm btn-outline-secondary">
<span class="icon-cog" aria-hidden="true"></span> <?php echo Text::_('COM_MOKOWAAS_CONFIGURE'); ?>
</a>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<!-- Right: Information Tables (4 cols) -->
<div class="col-12 col-xl-4">
<!-- Pending Updates -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="icon-refresh" aria-hidden="true"></span> Pending Updates</strong>
<span class="badge bg-<?php echo count($pendingUpdates) > 0 ? 'warning text-dark' : 'success'; ?>"><?php echo count($pendingUpdates); ?></span>
</div>
<?php if (!empty($pendingUpdates)): ?>
<div class="table-responsive">
<table class="table table-sm table-striped mb-0">
<thead><tr><th>Extension</th><th>Current</th><th>Available</th></tr></thead>
<tbody>
<?php foreach ($pendingUpdates as $upd): ?>
<tr>
<td class="small"><?php echo $this->escape($upd->name); ?></td>
<td class="small text-muted"><?php echo $this->escape($upd->current_version); ?></td>
<td class="small text-success"><?php echo $this->escape($upd->version); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card-body text-center text-muted small py-3">
<span class="icon-check-circle text-success"></span> All extensions up to date
</div>
<?php endif; ?>
</div>
<!-- Checked Out Items -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="icon-lock" aria-hidden="true"></span> Checked Out Items</strong>
<span class="badge bg-<?php echo count($checkedOut) > 0 ? 'info' : 'success'; ?>"><?php echo count($checkedOut); ?></span>
</div>
<?php if (!empty($checkedOut)): ?>
<div class="table-responsive">
<table class="table table-sm table-striped mb-0">
<thead><tr><th>Article</th><th>User</th><th>Since</th></tr></thead>
<tbody>
<?php foreach ($checkedOut as $item): ?>
<tr>
<td class="small"><?php echo $this->escape(mb_substr($item->title, 0, 30)); ?></td>
<td class="small"><?php echo $this->escape($item->username ?? ''); ?></td>
<td class="small text-muted"><?php echo HTMLHelper::_('date', $item->checked_out_time, 'M d H:i'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="card-footer text-center py-1">
<a href="<?php echo Route::_('index.php?option=com_checkin'); ?>" class="small">Global Check-in</a>
</div>
<?php else: ?>
<div class="card-body text-center text-muted small py-3">
<span class="icon-check-circle text-success"></span> No checked out items
</div>
<?php endif; ?>
</div>
<!-- WAF Blocks -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="icon-shield-alt" aria-hidden="true"></span> Recent WAF Blocks</strong>
<span class="badge bg-<?php echo count($wafBlocks) > 0 ? 'danger' : 'success'; ?>"><?php echo count($wafBlocks); ?></span>
</div>
<?php if (!empty($wafBlocks)): ?>
<div class="table-responsive">
<table class="table table-sm table-striped mb-0">
<thead><tr><th>IP</th><th>Rule</th><th>Time</th></tr></thead>
<tbody>
<?php foreach ($wafBlocks as $block): ?>
<tr>
<td class="small"><code><?php echo $this->escape($block->ip); ?></code></td>
<td class="small"><span class="badge bg-danger"><?php echo $this->escape($block->rule); ?></span></td>
<td class="small text-muted"><?php echo HTMLHelper::_('date', $block->created, 'M d H:i'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card-body text-center text-muted small py-3">
<span class="icon-check-circle text-success"></span> No recent blocks
</div>
<?php endif; ?>
</div>
<!-- Recent Logins -->
<div class="card mb-3">
<div class="card-header">
<strong><span class="icon-user" aria-hidden="true"></span> Recent Logins</strong>
</div>
<?php if (!empty($recentLogins)): ?>
<div class="table-responsive">
<table class="table table-sm table-striped mb-0">
<thead><tr><th>User</th><th>IP</th><th>Time</th></tr></thead>
<tbody>
<?php foreach ($recentLogins as $login): ?>
<tr>
<td class="small"><?php echo $this->escape($login->username ?? ''); ?></td>
<td class="small"><code><?php echo $this->escape($login->ip_address ?? ''); ?></code></td>
<td class="small text-muted"><?php echo HTMLHelper::_('date', $login->log_date, 'M d H:i'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card-body text-center text-muted small py-3">No login activity recorded</div>
<?php endif; ?>
</div>
</div><!-- /.col-xl-4 -->
</div><!-- /.row -->
</div>
@@ -0,0 +1,154 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
/** @var \Moko\Component\MokoWaaS\Administrator\View\Extensions\HtmlView $this */
$packages = $this->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'],
];
?>
<div id="mokowaas-extensions">
<div class="alert alert-info">
<span class="icon-info-circle" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOWAAS_EXTENSIONS_INFO'); ?>
</div>
<?php foreach ($grouped as $category => $pkgs): ?>
<h3 class="mb-3"><?php echo htmlspecialchars($category); ?></h3>
<div class="row g-3 mb-4">
<?php foreach ($pkgs as $pkg): ?>
<?php
$badge = $statusBadge[$pkg->status] ?? $statusBadge['not_installed'];
?>
<div class="col-12 col-md-6 col-xl-4">
<div class="card h-100">
<div class="card-body d-flex flex-column">
<div class="d-flex align-items-start justify-content-between mb-2">
<div class="d-flex align-items-center gap-2">
<span class="<?php echo htmlspecialchars($pkg->icon); ?>" aria-hidden="true" style="font-size:1.5rem;color:#1a2744"></span>
<div>
<h5 class="card-title mb-0"><?php echo htmlspecialchars($pkg->label); ?></h5>
<small class="text-muted"><?php echo htmlspecialchars($pkg->type); ?></small>
</div>
</div>
<span class="badge <?php echo $badge[0]; ?>"><?php echo $badge[1]; ?></span>
</div>
<p class="card-text text-muted flex-grow-1"><?php echo htmlspecialchars($pkg->description); ?></p>
<div class="d-flex align-items-center justify-content-between mt-auto pt-2 border-top">
<div class="small text-muted">
<?php if ($pkg->local_version): ?>
v<?php echo htmlspecialchars($pkg->local_version); ?>
<?php elseif ($pkg->remote_version): ?>
Latest: <?php echo htmlspecialchars($pkg->remote_version); ?>
<?php endif; ?>
</div>
<div class="d-flex gap-1">
<?php if ($pkg->article_url): ?>
<a href="<?php echo htmlspecialchars($pkg->article_url); ?>" target="_blank" class="btn btn-sm btn-outline-secondary" title="Documentation">
<span class="icon-book" aria-hidden="true"></span>
</a>
<?php endif; ?>
<?php if ($pkg->download_url && $pkg->status === 'not_installed'): ?>
<button type="button" class="btn btn-sm btn-primary mokowaas-install-btn"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.installExtension&format=json'); ?>"
data-download="<?php echo htmlspecialchars($pkg->download_url); ?>"
data-token="<?php echo $token; ?>"
data-label="<?php echo htmlspecialchars($pkg->label); ?>">
<span class="icon-download" aria-hidden="true"></span>
Install
</button>
<?php elseif ($pkg->status === 'installed'): ?>
<span class="btn btn-sm btn-outline-success disabled">
<span class="icon-check" aria-hidden="true"></span> Installed
</span>
<?php if (!$pkg->protected && $pkg->extension_id): ?>
<a href="<?php echo Route::_('index.php?option=com_installer&task=manage.remove&cid[]=' . $pkg->extension_id . '&' . $token . '=1'); ?>"
class="btn btn-sm btn-outline-danger"
onclick="return confirm('Uninstall <?php echo htmlspecialchars($pkg->label); ?>?')"
title="Uninstall">
<span class="icon-times" aria-hidden="true"></span>
</a>
<?php endif; ?>
<?php else: ?>
<span class="btn btn-sm btn-outline-secondary disabled">No release</span>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokowaas-install-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var el = this;
var url = el.dataset.url;
var downloadUrl = el.dataset.download;
var token = el.dataset.token;
var label = el.dataset.label;
if (!confirm('Install ' + label + '?')) return;
el.disabled = true;
var origHtml = el.textContent;
el.textContent = ' Installing...';
var fd = new FormData();
fd.append('download_url', downloadUrl);
fd.append(token, '1');
fetch(url, {
method: 'POST',
body: fd,
headers: {'X-Requested-With': 'XMLHttpRequest'}
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) {
Joomla.renderMessages({message: [label + ': ' + d.message]});
location.reload();
} else {
Joomla.renderMessages({error: [label + ': ' + (d.message || 'Failed')]});
el.disabled = false;
el.textContent = origHtml;
}
})
.catch(function() {
Joomla.renderMessages({error: ['Network error']});
el.disabled = false;
el.textContent = origHtml;
});
});
});
});
</script>
@@ -0,0 +1,145 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Version;
use Joomla\Registry\Registry;
/**
* Dashboard summary API controller.
*
* GET /api/index.php/v1/mokowaas/dashboard
*
* Returns a combined payload of site info and feature plugin states,
* suitable for remote dashboards and monitoring.
*
* @since 02.32.00
*/
class DashboardController extends BaseController
{
/**
* Return dashboard summary data.
*
* @return void
*/
public function displayList(): void
{
$app = Factory::getApplication();
$user = $app->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();
}
}
@@ -0,0 +1,180 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Feature plugins API controller.
*
* GET /api/index.php/v1/mokowaas/plugins — list MokoWaaS plugins + status
* POST /api/index.php/v1/mokowaas/plugins/toggle — enable/disable a feature plugin
*
* @since 02.32.00
*/
class PluginsController extends BaseController
{
/**
* List all MokoWaaS feature plugins with their enabled state.
*
* @return void
*/
public function displayList(): void
{
$app = Factory::getApplication();
$user = $app->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();
}
}
@@ -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;
}
@@ -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';
});
});
});
}
});
+30 -6
View File
@@ -1,24 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: Joomla.Component
INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.32.04
PATH: /mokowaas.xml
BRIEF: Component manifest for MokoWaaS admin dashboard and REST API
-->
<extension type="component" method="upgrade">
<name>MokoWaaS API</name>
<name>MokoWaaS</name>
<author>Moko Consulting</author>
<creationDate>2026-05-23</creationDate>
<creationDate>2026-06-02</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.31.00</version>
<version>02.31.00</version>
<description>Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups.</description>
<namespace path="api/src">Moko\Component\MokoWaaS\Api</namespace>
<version>02.32.21</version>
<description>MokoWaaS admin dashboard and REST API. Provides a control panel for managing MokoWaaS feature plugins, site health monitoring, and remote management endpoints.</description>
<namespace path="src">Moko\Component\MokoWaaS</namespace>
<administration>
<menu img="class:cogs">MokoWaaS</menu>
<files folder="admin">
<folder>language</folder>
<folder>services</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
</administration>
<api>
<files folder="api">
<folder>src</folder>
</files>
</api>
<media destination="com_mokowaas" folder="media">
<folder>css</folder>
<folder>js</folder>
</media>
</extension>
@@ -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"
@@ -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."
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="module" client="administrator" method="upgrade">
<name>mod_mokowaas_cpanel</name>
<author>Moko Consulting</author>
<creationDate>2026-06-02</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.32.21</version>
<description>MOD_MOKOWAAS_CPANEL_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCpanel</namespace>
<files>
<folder module="mod_mokowaas_cpanel">services</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/mod_mokowaas_cpanel.ini</language>
<language tag="en-GB">en-GB/mod_mokowaas_cpanel.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic"
label="MOD_MOKOWAAS_CPANEL_FIELDSET_DISPLAY"
description="MOD_MOKOWAAS_CPANEL_FIELDSET_DISPLAY_DESC">
<field name="collapsed" type="radio" default="1"
label="MOD_MOKOWAAS_CPANEL_COLLAPSED_LABEL"
description="MOD_MOKOWAAS_CPANEL_COLLAPSED_DESC"
layout="joomla.form.field.radio.switcher">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="show_health" type="radio" default="1"
label="MOD_MOKOWAAS_CPANEL_SHOW_HEALTH_LABEL"
layout="joomla.form.field.radio.switcher">
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field name="show_stats" type="radio" default="1"
label="MOD_MOKOWAAS_CPANEL_SHOW_STATS_LABEL"
description="MOD_MOKOWAAS_CPANEL_SHOW_STATS_DESC"
layout="joomla.form.field.radio.switcher">
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field name="show_disk" type="radio" default="1"
label="MOD_MOKOWAAS_CPANEL_SHOW_DISK_LABEL"
layout="joomla.form.field.radio.switcher">
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field name="show_ip" type="radio" default="1"
label="MOD_MOKOWAAS_CPANEL_SHOW_IP_LABEL"
layout="joomla.form.field.radio.switcher">
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field name="show_plugins" type="radio" default="1"
label="MOD_MOKOWAAS_CPANEL_SHOW_PLUGINS_LABEL"
layout="joomla.form.field.radio.switcher">
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field name="show_actions" type="radio" default="1"
label="MOD_MOKOWAAS_CPANEL_SHOW_ACTIONS_LABEL"
description="MOD_MOKOWAAS_CPANEL_SHOW_ACTIONS_DESC"
layout="joomla.form.field.radio.switcher">
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field name="show_versions" type="radio" default="1"
label="MOD_MOKOWAAS_CPANEL_SHOW_VERSIONS_LABEL"
description="MOD_MOKOWAAS_CPANEL_SHOW_VERSIONS_DESC"
layout="joomla.form.field.radio.switcher">
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
</fieldset>
</fields>
</config>
</extension>
@@ -0,0 +1,25 @@
<?php
/**
* @package MokoWaaS
* @subpackage mod_mokowaas_cpanel
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\Service\Provider\HelperFactory;
use Joomla\CMS\Extension\Service\Provider\Module;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->registerServiceProvider(new ModuleDispatcherFactory('\\Moko\\Module\\MokoWaaSCpanel'));
$container->registerServiceProvider(new HelperFactory('\\Moko\\Module\\MokoWaaSCpanel\\Administrator\\Helper'));
$container->registerServiceProvider(new Module());
}
};
@@ -0,0 +1,39 @@
<?php
/**
* @package MokoWaaS
* @subpackage mod_mokowaas_cpanel
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Module\MokoWaaSCpanel\Administrator\Dispatcher;
defined('_JEXEC') or die;
use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\HelperFactoryAwareInterface;
use Joomla\CMS\Helper\HelperFactoryAwareTrait;
use Joomla\Database\DatabaseInterface;
class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareInterface
{
use HelperFactoryAwareTrait;
protected function getLayoutData()
{
$data = parent::getLayoutData();
$db = Factory::getContainer()->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;
}
}
@@ -0,0 +1,139 @@
<?php
/**
* @package MokoWaaS
* @subpackage mod_mokowaas_cpanel
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Module\MokoWaaSCpanel\Administrator\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Version;
use Joomla\Database\DatabaseInterface;
class CpanelHelper
{
/**
* Get basic site info for the cpanel card header.
*/
public function getSiteInfo(DatabaseInterface $db): object
{
$config = Factory::getConfig();
$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) [
'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'] ?? '';
}
}
@@ -0,0 +1,199 @@
<?php
/**
* @package MokoWaaS
* @subpackage mod_mokowaas_cpanel
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$siteInfo = $siteInfo ?? (object) [];
$plugins = $plugins ?? [];
$healthOk = $healthOk ?? true;
$counts = $counts ?? (object) ['articles' => 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');
?>
<div class="mod-mokowaas-cpanel card p-3 mb-4">
<!-- Header row (always visible, acts as collapse toggle) -->
<div class="d-flex align-items-center justify-content-between">
<a class="d-flex align-items-center gap-2 text-decoration-none text-reset" data-bs-toggle="collapse" href="#mokowaas-cpanel-body" role="button" aria-expanded="<?php echo $collapsed ? 'false' : 'true'; ?>" aria-controls="mokowaas-cpanel-body">
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.25rem;color:#1a2744"></span>
<strong>MokoWaaS</strong>
<span class="badge bg-primary"><?php echo htmlspecialchars($siteInfo->mokowaas_version ?? ''); ?></span>
<?php if (!empty($siteInfo->debug)): ?>
<span class="badge bg-warning text-dark">Debug</span>
<?php endif; ?>
<?php if (!empty($siteInfo->offline)): ?>
<span class="badge bg-danger">Offline</span>
<?php endif; ?>
<span class="icon-chevron-down small text-muted" aria-hidden="true"></span>
</a>
<a href="<?php echo Route::_('index.php?option=com_mokowaas'); ?>" class="btn btn-sm btn-primary">
<span class="icon-cogs" aria-hidden="true"></span>
<?php echo Text::_('MOD_MOKOWAAS_CPANEL_OPEN_DASHBOARD'); ?>
</a>
</div>
<!-- Collapsible body -->
<div class="collapse<?php echo $collapsed ? '' : ' show'; ?> mt-3" id="mokowaas-cpanel-body">
<?php if ($showHealth && $showStats): ?>
<!-- Health + stats row -->
<div class="row g-2 mb-3">
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<?php if ($healthOk): ?>
<span class="icon-check-circle text-success d-block" style="font-size:1.5rem"></span>
<small class="text-success fw-bold">Healthy</small>
<?php else: ?>
<span class="icon-exclamation-circle text-danger d-block" style="font-size:1.5rem"></span>
<small class="text-danger fw-bold">DB Error</small>
<?php endif; ?>
</div>
</div>
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<span class="fw-bold d-block" style="font-size:1.25rem"><?php echo $counts->articles; ?></span>
<small class="text-muted">Articles</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<span class="fw-bold d-block" style="font-size:1.25rem"><?php echo $counts->users; ?></span>
<small class="text-muted">Users</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<?php if ($counts->updates > 0): ?>
<span class="fw-bold d-block text-warning" style="font-size:1.25rem"><?php echo $counts->updates; ?></span>
<small class="text-warning">Updates</small>
<?php else: ?>
<span class="icon-check d-block text-success" style="font-size:1.25rem"></span>
<small class="text-muted">Up to date</small>
<?php endif; ?>
</div>
</div>
</div>
<!-- Info + plugins + actions (consolidated) -->
<div class="d-flex flex-wrap align-items-center gap-2">
<?php if ($showDisk && $diskPct !== null): ?>
<span class="text-muted d-inline-flex align-items-center gap-1">
<span class="icon-hdd" aria-hidden="true"></span>
<?php echo $diskPct; ?>%
<span class="progress d-inline-flex" style="width:40px;height:5px"><span class="progress-bar <?php echo $diskColor; ?>" style="width:<?php echo $diskPct; ?>%"></span></span>
<?php echo number_format(($disk->free_mb ?? 0) / 1024, 1); ?>G free
</span>
<?php endif; ?>
<?php if ($showIp && $currentIp): ?>
<span class="text-muted"><span class="icon-globe" aria-hidden="true"></span> <code><?php echo htmlspecialchars($currentIp); ?></code></span>
<?php endif; ?>
<?php if ($showVersions): ?>
<span class="text-muted">J<?php echo htmlspecialchars($siteInfo->joomla_version ?? ''); ?> / PHP <?php echo htmlspecialchars($siteInfo->php_version ?? ''); ?></span>
<?php endif; ?>
<?php if ($showPlugins && !empty($plugins)): ?>
<span class="border-start ps-2 ms-1"></span>
<?php foreach ($plugins as $p): ?>
<?php
$label = $labels[$p->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);
?>
<a href="<?php echo $configUrl; ?>" class="badge <?php echo $badge; ?> text-decoration-none" title="<?php echo htmlspecialchars($p->name); ?>">
<span class="<?php echo $icon; ?>" aria-hidden="true"></span> <?php echo htmlspecialchars($label); ?>
</a>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($showActions): ?>
<span class="border-start ps-2 ms-1"></span>
<button type="button" class="btn btn-sm btn-outline-secondary" id="mokowaas-cpanel-cache"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.clearCache&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-trash" aria-hidden="true"></span> Clear Cache
</button>
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="btn btn-sm btn-outline-secondary">
<span class="icon-refresh" aria-hidden="true"></span> Check Updates
</a>
<?php if ($counts->updates > 0): ?>
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-warning text-dark text-decoration-none">
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->updates; ?> update<?php echo $counts->updates > 1 ? 's' : ''; ?>
</a>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endif; ?>
</div><!-- /.collapse -->
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('mokowaas-cpanel-cache');
if (!btn) return;
btn.addEventListener('click', function() {
var el = this;
var url = el.dataset.url;
var token = el.dataset.token;
el.disabled = true;
var icon = el.querySelector('span');
var origClass = icon ? icon.className : '';
if (icon) icon.className = 'icon-spinner icon-spin';
var fd = new FormData();
fd.append(token, '1');
fetch(url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) Joomla.renderMessages({message:['Cache cleared.']});
else Joomla.renderMessages({error:[d.message||'Failed']});
})
.catch(function(){Joomla.renderMessages({error:['Network error']})})
.finally(function(){
el.disabled = false;
if (icon) icon.className = origClass;
});
});
});
</script>
@@ -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(
'<strong>MokoWaaS License Key Required</strong> — '
'<strong>Moko Consulting License Key Required</strong> — '
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
. 'Go to <a href="index.php?option=com_installer&view=updatesites">System → Update Sites</a> '
. '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(
'<strong>MokoWaaS License Key Invalid</strong> — '
'<strong>Moko Consulting License Key Invalid</strong> — '
. 'Your license key could not be validated. Please verify your key in '
. '<a href="index.php?option=com_installer&view=updatesites">System → Update Sites</a>.',
'error'
@@ -4430,7 +4530,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
if (!$isValid)
{
$this->app->enqueueMessage(
'<strong>MokoWaaS License Key Invalid</strong> — '
'<strong>Moko Consulting License Key Invalid</strong> — '
. 'Your license key could not be validated. Updates will not be available. '
. 'Please verify your key in '
. '<a href="index.php?option=com_installer&view=updatesites">System → Update Sites</a>.',
@@ -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
*/
@@ -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
*/
@@ -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
*/
@@ -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
*/
@@ -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
*/
@@ -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
*/
@@ -0,0 +1,96 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Plugin\System\MokoWaaS\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Registry\Registry;
/**
* Shared utility class for MokoWaaS feature plugins.
*
* Provides master-user detection and core plugin parameter access
* so that feature plugins do not need to duplicate obfuscated constants.
*
* @since 02.32.00
*/
final class MokoWaaSHelper
{
private const MASTER_KEYS = ['NzUxNTk1NCkvNi4zND0='];
private const MK = 0x5A;
/** @var array|null Decoded master usernames cache. */
private static ?array $masterNames = null;
/**
* Check whether the current user is a master user.
*
* @return bool
*/
public static function isMasterUser(): bool
{
$user = Factory::getApplication()->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 ?? '{}');
}
}
@@ -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
*/
@@ -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
*/
@@ -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
*/
@@ -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 @@
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.31.00</version>
<version>02.31.00</version>
<version>02.32.21</version>
<description>This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.</description>
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
<scriptfile>script.php</scriptfile>
@@ -40,6 +39,7 @@
<filename plugin="mokowaas">script.php</filename>
<folder>Extension</folder>
<folder>Field</folder>
<folder>Helper</folder>
<folder>Service</folder>
<folder>forms</folder>
<folder>payload</folder>
+1 -1
View File
@@ -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
@@ -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
@@ -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."
@@ -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."
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoWaaS DevTools</name>
<element>mokowaas_devtools</element>
<author>Moko Consulting</author>
<creationDate>2026-06-02</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.32.21</version>
<description>PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSDevTools</namespace>
<files>
<folder>src</folder>
<folder>services</folder>
<folder>language</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/plg_system_mokowaas_devtools.ini</language>
<language tag="en-GB">en-GB/plg_system_mokowaas_devtools.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic"
label="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_FIELDSET_BASIC"
description="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_FIELDSET_BASIC_DESC">
<field name="dev_mode" type="radio" default="0"
label="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DEV_MODE_LABEL"
description="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DEV_MODE_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="reset_hits" type="radio" default="0"
label="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_HITS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_HITS_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="delete_versions" type="radio" default="0"
label="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DELETE_VERSIONS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DELETE_VERSIONS_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
</fields>
</config>
</extension>
@@ -0,0 +1,34 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas_devtools
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Moko\Plugin\System\MokoWaaSDevTools\Extension\DevTools;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->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;
}
);
}
};
@@ -0,0 +1,155 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas_devtools
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Plugin\System\MokoWaaSDevTools\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
/**
* MokoWaaS Developer Tools Plugin
*
* Provides development mode (disables caching, enables debug), hit counter
* reset, and content version cleanup.
*
* @since 02.32.00
*/
class DevTools extends CMSPlugin implements SubscriberInterface
{
protected $autoloadLanguage = true;
public static function getSubscribedEvents(): array
{
return [
'onAfterInitialise' => '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;
}
}
@@ -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 &amp; 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 &amp; 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."
@@ -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."
@@ -0,0 +1,242 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoWaaS Firewall</name>
<element>mokowaas_firewall</element>
<author>Moko Consulting</author>
<creationDate>2026-06-02</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.32.21</version>
<description>PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSFirewall</namespace>
<files>
<folder>src</folder>
<folder>sql</folder>
<folder>services</folder>
<folder>language</folder>
</files>
<install>
<sql><file driver="mysql" charset="utf8">sql/install.mysql.sql</file></sql>
</install>
<uninstall>
<sql><file driver="mysql" charset="utf8">sql/uninstall.mysql.sql</file></sql>
</uninstall>
<languages folder="language">
<language tag="en-GB">en-GB/plg_system_mokowaas_firewall.ini</language>
<language tag="en-GB">en-GB/plg_system_mokowaas_firewall.sys.ini</language>
</languages>
<config>
<fields name="params">
<!-- Network & Session -->
<fieldset name="basic"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_BASIC"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_BASIC_DESC">
<field name="force_https" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FORCE_HTTPS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FORCE_HTTPS_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="admin_session_timeout" type="number"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_SESSION_TIMEOUT_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_SESSION_TIMEOUT_DESC"
default="60" hint="Minutes (0 = Joomla default)" />
<field name="trusted_ips" type="subform"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_TRUSTED_IPS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_TRUSTED_IPS_DESC"
formsource="plugins/system/mokowaas/forms/trusted_ip_entry.xml"
multiple="true"
layout="joomla.form.field.subform.repeatable-table"
groupByFieldset="false"
buttons="add,remove,move" />
</fieldset>
<!-- WAF Shields -->
<fieldset name="waf"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_WAF"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_WAF_DESC">
<field name="waf_enabled" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_ENABLED_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_ENABLED_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="waf_sqli" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_SQLI_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_SQLI_DESC"
class="btn-group btn-group-yesno"
showon="waf_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="waf_xss" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_XSS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_XSS_DESC"
class="btn-group btn-group-yesno"
showon="waf_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="waf_mua" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_DESC"
class="btn-group btn-group-yesno"
showon="waf_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="waf_mua_blocklist" type="textarea"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LIST_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LIST_DESC"
rows="4" filter="raw"
default="sqlmap,nikto,nmap,havij,w3af,acunetix,nessus,openvas,masscan,gobuster,dirbuster,wpscan,joomscan"
showon="waf_enabled:1[AND]waf_mua:1" />
<field name="waf_rfi" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_RFI_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_RFI_DESC"
class="btn-group btn-group-yesno"
showon="waf_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="waf_dfi" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_DFI_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_DFI_DESC"
class="btn-group btn-group-yesno"
showon="waf_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<!-- Access Control -->
<fieldset name="access_control"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_ACCESS"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_ACCESS_DESC">
<field name="ip_blocklist" type="subform"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_IP_BLOCKLIST_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_IP_BLOCKLIST_DESC"
formsource="plugins/system/mokowaas/forms/trusted_ip_entry.xml"
multiple="true"
layout="joomla.form.field.subform.repeatable-table"
groupByFieldset="false"
buttons="add,remove,move" />
<field name="admin_secret" type="text"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_DESC"
default="" filter="raw" hint="Leave empty to disable" />
<field name="admin_secret_redirect" type="text"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_REDIRECT_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_REDIRECT_DESC"
default="" filter="url" hint="Empty = 403 Forbidden"
showon="admin_secret!:" />
<field name="block_frontend_superuser" type="radio" default="0"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FE_SU_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FE_SU_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<!-- File & Template Protection -->
<fieldset name="protection"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PROTECTION"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PROTECTION_DESC">
<field name="block_sensitive_files" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FILES_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FILES_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="block_direct_php" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_PHP_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_PHP_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="block_template_switch" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_TMPL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_TMPL_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<!-- Password Policy -->
<fieldset name="password_policy"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PASSWORD"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PASSWORD_DESC">
<field name="password_min_length" type="number" default="12"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_LENGTH_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_LENGTH_DESC" />
<field name="password_require_uppercase" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_UPPER_LABEL"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="password_require_number" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_NUMBER_LABEL"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="password_require_special" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_SPECIAL_LABEL"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<!-- Upload Restrictions -->
<fieldset name="uploads"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_UPLOADS"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_UPLOADS_DESC">
<field name="upload_allowed_types" type="text"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_TYPES_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_TYPES_DESC"
default="jpg,jpeg,png,gif,webp,svg,pdf,doc,docx,xls,xlsx" />
<field name="upload_max_size_mb" type="number"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_SIZE_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_SIZE_DESC"
default="100" />
</fieldset>
</fields>
</config>
</extension>
@@ -0,0 +1,34 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas_firewall
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Moko\Plugin\System\MokoWaaSFirewall\Extension\Firewall;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->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;
}
);
}
};
@@ -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;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS `#__mokowaas_waf_log`;
@@ -0,0 +1,625 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas_firewall
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Plugin\System\MokoWaaSFirewall\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;
use Joomla\Event\SubscriberInterface;
use Moko\Plugin\System\MokoWaaS\Helper\MokoWaaSHelper;
/**
* MokoWaaS Firewall Plugin
*
* Web Application Firewall with security shields, IP management,
* request inspection, and access control.
*
* @since 02.32.00
*/
class Firewall extends CMSPlugin implements SubscriberInterface
{
protected $autoloadLanguage = true;
private const BLOCKED_FILES = [
'htaccess.txt', 'web.config.txt', 'configuration.php-dist',
'README.txt', 'LICENSE.txt', 'joomla.xml', 'robots.txt.dist',
];
private const BLOCKED_PHP_DIRS = [
'/images/', '/media/', '/tmp/', '/cache/', '/logs/',
];
private const DEFAULT_MUA_BLOCKLIST = 'sqlmap,nikto,nmap,havij,w3af,acunetix,nessus,openvas,masscan,gobuster,dirbuster,wpscan,joomscan';
public static function getSubscribedEvents(): array
{
return [
'onAfterInitialise' => '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 '<!DOCTYPE html><html><head><title>403 Forbidden</title></head>'
. '<body><h1>403 Forbidden</h1><p>Your request has been blocked by the security firewall.</p></body></html>';
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);
}
}
}
@@ -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."
@@ -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."
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoWaaS Monitor</name>
<element>mokowaas_monitor</element>
<author>Moko Consulting</author>
<creationDate>2026-06-02</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.32.21</version>
<description>PLG_SYSTEM_MOKOWAAS_MONITOR_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSMonitor</namespace>
<files>
<folder>src</folder>
<folder>services</folder>
<folder>language</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/plg_system_mokowaas_monitor.ini</language>
<language tag="en-GB">en-GB/plg_system_mokowaas_monitor.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic"
label="PLG_SYSTEM_MOKOWAAS_MONITOR_FIELDSET_BASIC"
description="PLG_SYSTEM_MOKOWAAS_MONITOR_FIELDSET_BASIC_DESC">
<field name="heartbeat_enabled" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_MONITOR_HEARTBEAT_LABEL"
description="PLG_SYSTEM_MOKOWAAS_MONITOR_HEARTBEAT_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
</fields>
</config>
</extension>
@@ -0,0 +1,34 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas_monitor
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Moko\Plugin\System\MokoWaaSMonitor\Extension\Monitor;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->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;
}
);
}
};
@@ -0,0 +1,135 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas_monitor
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Plugin\System\MokoWaaSMonitor\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
use Joomla\Event\SubscriberInterface;
use Moko\Plugin\System\MokoWaaS\Helper\MokoWaaSHelper;
/**
* MokoWaaS Health Monitor Plugin
*
* Provides Grafana heartbeat integration and site health diagnostics.
* The detailed 14-check health endpoint remains in the core plugin's API
* for now; this plugin handles the proactive monitoring side.
*
* @since 02.32.00
*/
class Monitor extends CMSPlugin implements SubscriberInterface
{
protected $autoloadLanguage = true;
private const HEARTBEAT_URL = 'https://bench.mokoconsulting.tech/api/waas-heartbeat';
private const HEARTBEAT_KEY = 'moko-waas-hb-2026-x9k4m';
public static function getSubscribedEvents(): array
{
return [
'onExtensionAfterSave' => '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'
);
}
}
}
@@ -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)."
@@ -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."
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoWaaS Tenant</name>
<element>mokowaas_tenant</element>
<author>Moko Consulting</author>
<creationDate>2026-06-02</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.32.21</version>
<description>PLG_SYSTEM_MOKOWAAS_TENANT_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSTenant</namespace>
<files>
<folder>src</folder>
<folder>services</folder>
<folder>language</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/plg_system_mokowaas_tenant.ini</language>
<language tag="en-GB">en-GB/plg_system_mokowaas_tenant.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic"
label="PLG_SYSTEM_MOKOWAAS_TENANT_FIELDSET_BASIC"
description="PLG_SYSTEM_MOKOWAAS_TENANT_FIELDSET_BASIC_DESC">
<field name="restrict_installer" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_INSTALLER_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_INSTALLER_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="allow_extension_updates" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_TENANT_ALLOW_UPDATES_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TENANT_ALLOW_UPDATES_DESC"
class="btn-group btn-group-yesno"
showon="restrict_installer:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="hide_sysinfo" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_TENANT_HIDE_SYSINFO_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TENANT_HIDE_SYSINFO_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="restrict_global_config" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_CONFIG_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_CONFIG_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="restrict_template_editing" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_TEMPLATE_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TENANT_RESTRICT_TEMPLATE_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="disable_install_url" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_TENANT_DISABLE_INSTALL_URL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TENANT_DISABLE_INSTALL_URL_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="hidden_menu_items" type="textarea"
label="PLG_SYSTEM_MOKOWAAS_TENANT_HIDDEN_MENUS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TENANT_HIDDEN_MENUS_DESC"
rows="5" filter="raw" />
</fieldset>
</fields>
</config>
</extension>
@@ -0,0 +1,34 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas_tenant
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Moko\Plugin\System\MokoWaaSTenant\Extension\Tenant;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->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;
}
);
}
};
@@ -0,0 +1,207 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas_tenant
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Plugin\System\MokoWaaSTenant\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;
use Joomla\Event\SubscriberInterface;
use Moko\Plugin\System\MokoWaaS\Helper\MokoWaaSHelper;
/**
* MokoWaaS Tenant Restrictions Plugin
*
* Restricts non-master user access to installer, sysinfo, global config,
* template editing, and specified admin menu items.
*
* @since 02.32.00
*/
class Tenant extends CMSPlugin implements SubscriberInterface
{
protected $autoloadLanguage = true;
public static function getSubscribedEvents(): array
{
return [
'onAfterRoute' => '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));
}
}
@@ -12,8 +12,8 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.31.00</version>
<version>02.31.00</version>
<version>02.32.21</version>
<version>02.32.21</version>
<description>PLG_TASK_MOKOWAASDEMO_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSDemo</namespace>
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.31.00</version>
<version>02.32.21</version>
<description>PLG_TASK_MOKOWAASSYNC_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSSync</namespace>
@@ -7,8 +7,8 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.31.00</version>
<version>02.31.00</version>
<version>02.32.21</version>
<version>02.32.21</version>
<description>Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.</description>
<namespace path="src">Moko\Plugin\WebServices\MokoWaaS</namespace>
<files>
@@ -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']
);
}
}
@@ -7,8 +7,8 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.31.00</version>
<version>02.31.00</version>
<version>02.32.21</version>
<version>02.32.21</version>
<description>Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds.</description>
<namespace path="src">Moko\Plugin\WebServices\PerfectPublisher</namespace>
<files>
@@ -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
*/
@@ -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)
*/
+10 -5
View File
@@ -2,27 +2,32 @@
<extension type="package" method="upgrade">
<name>Package - MokoWaaS</name>
<packagename>mokowaas</packagename>
<version>02.31.00</version>
<version>02.31.00</version>
<creationDate>2026-05-23</creationDate>
<version>02.32.21</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GNU General Public License version 3 or later; see LICENSE</license>
<description>MokoWaaS site management suite — branding, health monitoring, tenant restrictions, and REST API.</description>
<description>MokoWaaS site management suite — admin dashboard, security firewall, tenant restrictions, health monitoring, developer tools, and REST API.</description>
<scriptfile>script.php</scriptfile>
<files folder="packages">
<file type="plugin" id="plg_system_mokowaas" group="system">plg_system_mokowaas.zip</file>
<file type="plugin" id="plg_system_mokowaas_firewall" group="system">plg_system_mokowaas_firewall.zip</file>
<file type="plugin" id="plg_system_mokowaas_tenant" group="system">plg_system_mokowaas_tenant.zip</file>
<file type="plugin" id="plg_system_mokowaas_devtools" group="system">plg_system_mokowaas_devtools.zip</file>
<file type="plugin" id="plg_system_mokowaas_monitor" group="system">plg_system_mokowaas_monitor.zip</file>
<file type="component" id="com_mokowaas">com_mokowaas.zip</file>
<file type="module" id="mod_mokowaas_cpanel" client="administrator">mod_mokowaas_cpanel.zip</file>
<file type="plugin" id="plg_webservices_mokowaas" group="webservices">plg_webservices_mokowaas.zip</file>
<file type="plugin" id="plg_webservices_perfectpublisher" group="webservices">plg_webservices_perfectpublisher.zip</file>
<file type="plugin" id="plg_task_mokowaasdemo" group="task">plg_task_mokowaasdemo.zip</file>
<file type="plugin" id="plg_task_mokowaassync" group="task">plg_task_mokowaassync.zip</file>
<file type="template" id="mokoonyx" client="site">tpl_mokoonyx.zip</file>
</files>
<updateservers>
<server type="extension" priority="1" name="MokoWaaS Update Server">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml</server>
<server type="extension" priority="1" name="MokoWaaS Update Server">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml</server>
</updateservers>
</extension>
+213 -6
View File
@@ -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');
}
}
}