chore(release): stable release #134
@@ -0,0 +1,4 @@
|
||||
[submodule "src/packages/tpl_mokoonyx"]
|
||||
path = src/packages/tpl_mokoonyx
|
||||
url = https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
|
||||
branch = main
|
||||
@@ -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>
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -51,6 +51,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
|
||||
+25
-1
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
-->
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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';
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
+15
@@ -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."
|
||||
+3
@@ -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;
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
; MokoWaaS Firewall Plugin
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GPL-3.0-or-later
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL="System - MokoWaaS Firewall"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC="Web Application Firewall with security shields, IP management, request inspection, and access control."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_BASIC="Network & Session"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_BASIC_DESC="HTTPS, session timeout, and trusted IP settings."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FORCE_HTTPS_LABEL="Force HTTPS"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_SESSION_TIMEOUT_LABEL="Admin Session Timeout (minutes)"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_SESSION_TIMEOUT_DESC="Idle timeout for admin sessions. 0 = Joomla default. Master users and trusted IPs exempt."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_TRUSTED_IPS_LABEL="Trusted IPs"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_TRUSTED_IPS_DESC="IPs that bypass session timeout and WAF shields. Supports exact, CIDR, and wildcard."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_WAF="Web Application Firewall"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_WAF_DESC="Threat detection shields that inspect incoming requests."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_ENABLED_LABEL="Enable WAF"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_ENABLED_DESC="Master toggle for all WAF shields."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_SQLI_LABEL="SQLiShield"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_SQLI_DESC="Block SQL injection patterns in GET, POST, and COOKIE data."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_XSS_LABEL="XSSShield"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_XSS_DESC="Block cross-site scripting patterns in GET and POST data."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LABEL="MUAShield"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_DESC="Block known malicious user agents (scanners, bots, attack tools)."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LIST_LABEL="User Agent Blocklist"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LIST_DESC="Comma-separated user agent fragments to block."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_RFI_LABEL="RFIShield"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_RFI_DESC="Block remote file inclusion attempts (URLs in GET parameters)."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_DFI_LABEL="DFIShield"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_DFI_DESC="Block directory traversal and local file inclusion attempts."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_ACCESS="Access Control"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_ACCESS_DESC="IP blocking, admin secret URL, and login restrictions."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_IP_BLOCKLIST_LABEL="IP Deny List"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_IP_BLOCKLIST_DESC="Block specific IPs or CIDR ranges. Checked before all other shields."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_LABEL="Admin Secret URL Parameter"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_DESC="Require ?secret=VALUE to access /administrator. Leave empty to disable."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_REDIRECT_LABEL="Secret Failure Redirect"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_REDIRECT_DESC="URL to redirect when admin secret is missing. Empty = 403 Forbidden."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FE_SU_LABEL="Forbid Frontend Super User Login"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FE_SU_DESC="Prevent Super User accounts from logging in on the frontend."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PROTECTION="File & Template Protection"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PROTECTION_DESC="Block access to sensitive files and prevent template switching."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FILES_LABEL="Block Sensitive Files"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FILES_DESC="Block access to htaccess.txt, configuration.php-dist, and similar files."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_PHP_LABEL="Block Direct PHP Access"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_PHP_DESC="Block PHP execution in images/, media/, tmp/, cache/, logs/ directories."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_TMPL_LABEL="Block Template Switching"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_TMPL_DESC="Block tmpl= and template= URL parameters (tmpl=component allowed)."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PASSWORD="Password Policy"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PASSWORD_DESC="Minimum password complexity requirements."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_LENGTH_LABEL="Minimum Password Length"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_LENGTH_DESC="Minimum characters required."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_UPPER_LABEL="Require Uppercase"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_NUMBER_LABEL="Require Number"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_SPECIAL_LABEL="Require Special Character"
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_UPLOADS="Upload Restrictions"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_UPLOADS_DESC="Override Joomla upload settings at runtime."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_TYPES_LABEL="Allowed File Types"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_TYPES_DESC="Comma-separated permitted file extensions."
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_SIZE_LABEL="Max Upload Size (MB)"
|
||||
PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_SIZE_DESC="Maximum upload size in megabytes."
|
||||
+3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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."
|
||||
+3
@@ -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)."
|
||||
+3
@@ -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)
|
||||
*/
|
||||
|
||||
|
||||
Submodule
+1
Submodule src/packages/tpl_mokoonyx added at 16a7090f29
+10
-5
@@ -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
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user