Merge pull request 'Release 02.03.12: Package structure, site aliases, plugin protection' (#35) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Failing after 2s
Joomla: Repo Health / Scripts governance (push) Successful in 3s
Joomla: Repo Health / Repository health (push) Failing after 3s

This commit was merged in pull request #35.
This commit is contained in:
2026-05-24 23:23:39 +00:00
14 changed files with 562 additions and 391 deletions
+2
View File
@@ -203,3 +203,5 @@ venv/
*.coverage
hypothesis/
profile.ps1
TODO.md
+1 -1
View File
@@ -18,7 +18,7 @@
</governance>
<build>
<language>PHP</language>
<package-type>plugin</package-type>
<package-type>package</package-type>
<entry-point>src/</entry-point>
</build>
</moko-platform>
+27 -13
View File
@@ -77,37 +77,32 @@ jobs:
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump patch version
BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .)
VERSION=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
# Read current version from manifest (priority) or README — no bump yet
VERSION=$(php ${MOKO_API}/version_read.php --path .)
echo "Version: ${VERSION}"
# Update platform-specific manifest
# Ensure platform-specific manifest matches
php ${MOKO_API}/version_set_platform.php --path . --version "${VERSION}"
# Commit version bump
# Git setup for later commits
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Detect element from Joomla/Dolibarr manifest
set +o pipefail
PLATFORM="${{ steps.platform.outputs.platform }}"
EXT_ELEMENT=$(php ${MOKO_API}/manifest_read.php --path . --field name 2>/dev/null | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true)
# For Joomla, prefer <element> tag
if [ "$PLATFORM" = "joomla" ]; then
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
MANIFEST=$(find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" -print0 2>/dev/null | xargs -0 grep -l '<extension' 2>/dev/null | head -1 || true)
if [ -n "$MANIFEST" ]; then
ELEM=$(grep -oP "<element>\K[^<]+" "$MANIFEST" 2>/dev/null | head -1)
ELEM=$(grep -oP "<element>\K[^<]+" "$MANIFEST" 2>/dev/null | head -1 || true)
[ -n "$ELEM" ] && EXT_ELEMENT="$ELEM"
fi
fi
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
set -o pipefail
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
@@ -244,3 +239,22 @@ jobs:
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
fi
done
- name: "Post-release version bump"
run: |
MOKO_API="/tmp/moko-platform-api/cli"
VERSION="${{ steps.meta.outputs.version }}"
# Bump patch for next dev cycle
BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .)
NEXT=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
[ -z "$NEXT" ] && exit 0
# Update platform-specific manifest to next version
php ${MOKO_API}/version_set_platform.php --path . --version "${NEXT}"
git add -A
git diff --cached --quiet || {
git commit -m "chore: update development channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
+29 -3
View File
@@ -27,13 +27,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Removed
- Removed deploy-manual.yml workflow — switching to Joomla update server method for extension distribution
### Planned
- License/subscription check
- System email template branding (DB approach)
## [02.03.10] - 2026-05-24
### Added
- Canonical URL injection for alias domains (prevents SEO duplication)
- Primary Domain config field in Site Aliases tab
- Heartbeat registration for alias domains (each alias gets Grafana datasource)
- Plugin protection: hidden from non-master users, disable/uninstall blocked
- Dynamic plugin version read from manifest XML (no more hardcoded strings)
- Package structure: `pkg_mokowaas` with system plugin, webservices plugin, and component
### Changed
- Alias offline mode uses Joomla's native template offline.php (not custom HTML)
- Alias detection simplified: direct lookup in aliases list (no primary host comparison)
- handleSiteAlias() moved to onAfterRoute (client type resolved at that point)
- Package script.php enables plugins on every install/update and sends heartbeat
### Fixed
- Alias domain matching: strip trailing slashes, handle Joomla subform stdClass format
- Backend redirect: use primary_domain setting instead of Uri::root() (returned alias domain on mirrors)
- CI: version_bump reads manifest XML with priority over README.md VERSION header
- CI: version bump occurs after release build, not before
- CI: pipefail disabled during element detection (SIGPIPE on find|head)
- CI: pkg_pkg_ prefix duplication in zip names and updates.xml URLs
- CI: updates_xml_build preserves existing channel entries (stable not wiped by dev releases)
### Removed
- deploy-manual.yml workflow — using Joomla update server for distribution
- Accidentally committed profile.ps1 and TODO.md
## [02.01.43] - 2026-05-23
### Added
+26 -320
View File
@@ -5,356 +5,62 @@
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
This program is free software; you can redistribute it and modify it under the terms of the GNU General Public License version 3 or later.
This program is distributed in the hope that it will be useful but without warranty.
You should have received a copy of the GNU General Public License in LICENSE.md.
# FILE INFORMATION
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.03.02
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.03.12
PATH: /README.md
BRIEF: Rebranding plugin for MokoWaaS platform
NOTE: Internal WaaS identity abstraction layer
BRIEF: MokoWaaS platform plugin for Joomla
-->
# MokoWaaS Plugin
# MokoWaaS
[![Version](https://img.shields.io/badge/version-02.03.00-blue.svg?logo=v&logoColor=white)](https://github.com/mokoconsulting-tech/MokoWaaS/releases/tag/v02)
[![Version](https://img.shields.io/badge/version-02.03.11-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases)
[![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE)
[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net)
MokoWaaS is a Joomla 5.x / 6.x system plugin that provides a configurable white-label identity layer for the MokoWaaS platform. It replaces all visible Joomla branding with your own brand name, company name, and support URLs — configurable from the plugin admin without code changes.
## Table of Contents
- [Overview](#overview)
- [Features](#features)
- [System Requirements](#system-requirements)
- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [Technical Implementation](#technical-implementation)
- [Repository Structure](#repository-structure)
- [Development](#development)
- [Documentation](#documentation)
- [Support](#support)
- [License](#license)
- [Changelog](#changelog)
## Overview
The MokoWaaS plugin operationalizes a unified naming convention, brand-controlled visuals, and enforced terminology across all tenant sites. This ensures consistent service delivery within the WaaS (Website as a Service) framework by abstracting all upstream Joomla identifiers behind MokoWaaS-compliant terminology.
MokoWaaS is a Joomla 5.x / 6.x system plugin package that provides white-label branding, security hardening, tenant restrictions, health monitoring, and multi-domain management for the MokoWaaS platform.
## Features
- **Template-Based Overrides**: 50+ language keys with `{{BRAND_NAME}}`, `{{COMPANY_NAME}}`, `{{SUPPORT_URL}}` placeholders
- **Configurable Brand**: Change brand name, company, and support URL from plugin config — takes effect immediately
- **Safe Override Merging**: Sentinel-block pattern preserves existing site overrides during install/update
- **Clean Uninstall**: Only MokoWaaS keys are removed; all other overrides are preserved
- **Joomla 5.x / 6.x Compatible**: Built using modern Joomla plugin architecture with dependency injection
- **Multi-Language Support**: en-GB and en-US locales
- **Admin & Frontend Coverage**: Dashboard, footer, login, installer, system info, update component, error pages, and more
- **Health Monitoring**: 16 diagnostic checks via `/?mokowaas=health` — database, filesystem, cache, extensions, Akeeba Backup, Admin Tools, SSL, cron, errors, DB size, content, users, mail, SEO, templates, config
- **Grafana Integration**: Auto-provisions Infinity datasource via heartbeat receiver — 9-row dashboard with all health metrics
- **ntfy Notifications**: Heartbeat events pushed to `mokowaas-heartbeat` topic
- **Plugin Protection**: Hidden from non-super-admins, self-healing lock, uninstall blocked
- **Governance Compliant**: Aligned with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)
- **White-Label Branding** — configurable brand name, company, support URL, colors, favicon, custom CSS
- **Tenant Restrictions** — master user enforcement, installer/sysinfo/config/template access control
- **Health Monitoring** — 16 diagnostic checks via `/?mokowaas=health` with Grafana auto-provisioning
- **Site Aliases** — per-alias offline mode, robots directives, backend redirect, canonical URLs
- **Remote API** — 6 endpoints (health, install, update, cache, backup, info)
- **Security Hardening** — HTTPS enforcement, session timeouts, password policy, upload restrictions
- **Plugin Protection** — protected status, hidden from non-master users, disable/uninstall blocked
## System Requirements
## Requirements
- **Joomla**: 5.0+ or 6.x
- **PHP**: 8.1 or higher (8.3+ for Joomla 6)
- **Extensions**: Standard Joomla PHP extensions
- **Permissions**: Write access to language override directories
- Joomla 5.0+ or 6.x
- PHP 8.1+ (8.3+ for Joomla 6)
## Installation
### Method 1: Via Joomla Extension Manager (Recommended)
Download the latest `pkg_mokowaas-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases) and install via **System → Install → Upload Package File**.
1. Download the latest release package from the releases page
2. Log into your Joomla Administrator panel
3. Navigate to **System → Extensions → Install**
4. Click **Upload Package File**
5. Select the downloaded `.zip` file
6. Click **Upload & Install**
7. Navigate to **System → Plugins**
8. Search for "MokoWaaS Brand"
9. Enable the plugin
10. Clear Joomla cache
### Method 2: Manual Installation
1. Extract the plugin package
2. Upload contents to your Joomla installation's `/tmp` directory
3. Install via Joomla Extension Manager → Install from Folder
4. Enable the plugin as described above
### Post-Installation
After installation, verify the branding is active:
- Check the administrator footer for "Powered by MokoWaaS"
- Verify the control panel shows "Welcome to MokoWaaS!"
- Clear browser cache if branding doesn't appear immediately
### Automatic Updates
This plugin supports Joomla's automatic update system. Once installed:
1. Navigate to **System → Update → Extensions**
2. The plugin will automatically check for updates from the MokoWaaS update server
3. When a new version is available, it will appear in the update list
4. Click **Update** to install the latest version
The update server URL is configured in the plugin manifest and points to:
```
https://raw.githubusercontent.com/mokoconsulting-tech/MokoWaaS/main/updates.xml
```
Updates are published automatically when new releases are created through the GitHub release workflow.
## Usage
Once installed and enabled, the plugin automatically replaces Joomla branding with your configured values. No code changes needed.
### Changing the Brand Name
1. Navigate to **System → Plugins → System - MokoWaaS**
2. Set **Brand Name** to your desired name (e.g., "MyPlatform")
3. Set **Company Name** to your company (e.g., "My Company Inc.")
4. Set **Support URL** to your support site (e.g., "https://support.mycompany.com")
5. Click **Save & Close**
6. The new branding appears immediately across admin and frontend
### What Gets Rebranded
| Area | Example |
| ---- | ------- |
| Admin footer | "Powered by [YourBrand](https://your-url)" |
| Dashboard | "Welcome to YourBrand!" |
| Quick Icons | "YourBrand is up to date." |
| System Info | "YourBrand Version" |
| Login page | "YourBrand Administrator Login" |
| Update component | "YourBrand Update" |
| Frontend footer | "Powered by [YourBrand](https://your-url)" |
| Error pages | No Joomla references |
## Configuration
The plugin provides the following configuration options accessible through **System → Plugins → System - MokoWaaS**:
### Parameters
| Parameter | Type | Default | Description |
| --------- | ---- | ------- | ----------- |
| Enable Branding | Yes/No | Yes | Master toggle for all branding overrides |
| Brand Name | Text | MokoWaaS | Replaces "Joomla" throughout the interface |
| Company Name | Text | Moko Consulting | Used in support/attribution links |
| Support URL | URL | https://mokoconsulting.tech | Destination for help and documentation links |
See the [Configuration Guide](docs/guides/configuration-guide.md) for detailed documentation on how overrides work.
## Technical Implementation
### Architecture
The plugin follows Joomla 5.x system plugin architecture:
```
PlgSystemMokoWaaS
├── Event Handlers
│ ├── onAfterInitialise - Framework initialization hook
│ └── onAfterRoute - Route determination hook
├── Dependency Injection
│ └── ServiceProvider - DI container registration
└── Language Integration
└── Native Override System - Joomla's built-in override mechanism
```
### Core Components
1. **mokowaas.php**
- Main plugin class extending `CMSPlugin`
- Implements system event handlers
- Namespace: `Moko\Plugin\System\MokoWaaS`
2. **mokowaas.xml**
- Plugin manifest defining metadata and structure
- Joomla 5.x namespace configuration
- File and folder definitions
3. **services/provider.php**
- Dependency injection service provider
- Registers plugin with Joomla's DI container
- Joomla 5.x compatibility layer
4. **language/en-GB/**
- Plugin-specific language strings
- Installation and configuration UI text
5. **language/overrides/**
- Frontend language override files
- Replaces Joomla terminology with MokoWaaS branding
6. **administrator/language/overrides/**
- Administrator language override files
- Backend-specific branding replacements
### Language Override Integration
The plugin leverages Joomla's native language override system rather than programmatically loading strings. Language override files are placed in standard Joomla locations:
- Frontend: `language/overrides/{locale}.override.ini`
- Administrator: `administrator/language/overrides/{locale}.override.ini`
Joomla automatically loads these overrides during initialization, ensuring optimal performance and compatibility.
## Repository Structure
```
mokowaas/
├── src/ # Plugin source files
│ ├── mokowaas.php # Main plugin class
│ ├── mokowaas.xml # Plugin manifest
│ ├── services/
│ │ └── provider.php # DI service provider
│ ├── language/
│ │ ├── en-GB/ # Plugin language files
│ │ └── overrides/ # Frontend language overrides
│ └── administrator/
│ └── language/
│ └── overrides/ # Admin language overrides
├── docs/ # Documentation
│ ├── index.md # Documentation index
│ ├── plugin-basic.md # Plugin overview
│ ├── guides/ # Operational guides
│ └── reference/ # Reference materials
├── scripts/ # Build and validation scripts
│ ├── validate_manifest.sh
│ ├── verify_changelog.sh
│ └── update_changelog.sh
├── .github/ # GitHub workflows
│ └── workflows/
│ ├── build.yml
│ ├── ci.yml
│ └── release_from_version.yml
├── CHANGELOG.md # Version history
├── README.md # This file
├── LICENSE.md # GPL-3.0-or-later license
├── CONTRIBUTING.md # Contribution guidelines
└── CODE_OF_CONDUCT.md # Community guidelines
```
## Development
### Building the Plugin
Build the installable plugin package from source:
```bash
cd src
zip -r ../mokowaas_v01.04.00.zip . -x "*.git*"
```
### Running Validation Scripts
```bash
# Validate plugin manifest
./scripts/validate_manifest.sh
# Verify changelog format
./scripts/verify_changelog.sh
```
### PHP Syntax Validation
```bash
cd src
find . -name "*.php" -exec php -l {} \;
```
### Automated Build via GitHub Actions
The repository includes automated workflows:
- **build.yml**: Creates ZIP package on release
- **ci.yml**: Runs validation checks on pull requests
- **release_from_version.yml**: Automates release process
After installation, the package auto-enables and sets protected status.
## Documentation
Comprehensive documentation is available in the `/docs` directory:
Full documentation is available on the [MokoWaaS Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki):
- **[Plugin Overview](docs/plugin-basic.md)**: Detailed plugin documentation
- **[Installation Guide](docs/guides/installation-guide.md)**: Step-by-step installation
- **[Build Guide](docs/guides/build-guide.md)**: Building and packaging
- **[Configuration Guide](docs/guides/configuration-guide.md)**: Configuration options
- **[Operations Guide](docs/guides/operations-guide.md)**: Operational procedures
- **[Troubleshooting Guide](docs/guides/troubleshooting-guide.md)**: Common issues
All documentation follows the [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) documentation framework.
## Support
### Getting Help
- **Documentation**: Check the `/docs` directory for detailed guides
- **Issues**: Submit issues through the GitHub issue tracker
- **Service Support**: For operational issues, submit a ticket through the Moko Consulting service channel
### Reporting Issues
When reporting issues, include:
- Joomla version
- PHP version
- Plugin version
- Steps to reproduce
- Expected vs actual behavior
- Relevant error messages or logs
- [Configuration Guide](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Configuration)
- [Health Monitoring](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Health-Monitoring)
- [Site Aliases](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Site-Aliases)
- [API Endpoints](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/API-Endpoints)
- [Grafana Integration](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Grafana-Integration)
## License
This project is licensed under the GNU General Public License version 3 or later (GPL-3.0-or-later).
See [LICENSE.md](LICENSE.md) for the full license text.
## Versioning
This extension follows the [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) version governance model using semantic versioning: `MAJOR.MINOR.PATCH`
Current version: **02.01.18**
GPL-3.0-or-later — see [LICENSE.md](LICENSE.md)
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for a complete version history.
### Recent Changes (v02.01.18 - 2026-04-23)
- Always install and lock MokoOnyx template on install/update
- Always unlock MokoCassiopeia on install/update (allow uninstall)
- Bundle MokoOnyx payload (replaces MokoCassiopeia payload)
- Update payload workflow to fetch MokoOnyx from Gitea releases
## Contributing
We welcome contributions! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on:
- Code of conduct
- Development workflow
- Coding standards
- Pull request process
- Documentation requirements
## Acknowledgments
- Built for the MokoWaaS platform
- Follows [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)
- Designed for Joomla 5.x architecture
- Maintained by Moko Consulting
See [CHANGELOG.md](CHANGELOG.md)
---
-12
View File
@@ -1,12 +0,0 @@
# TODO
> **Note:** This file is not tracked in version control (.gitignore). It is for local task tracking only.
## Critical
-
## Normal
-
## Low
-
+1 -1
View File
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>01.00.00</version>
<version>02.03.11</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>
<administration>
@@ -65,6 +65,39 @@ class MokoWaaS extends CMSPlugin
*/
private const HEARTBEAT_KEY = 'moko-waas-hb-2026-x9k4m';
/**
* Get the plugin version from the manifest XML.
*
* @return string Version string (e.g. '02.03.04')
*
* @since 02.03.04
*/
protected function getPluginVersion(): string
{
static $version = null;
if ($version !== null)
{
return $version;
}
$manifestFile = JPATH_PLUGINS . '/system/mokowaas/mokowaas.xml';
if (file_exists($manifestFile))
{
$xml = @simplexml_load_file($manifestFile);
if ($xml && isset($xml->version))
{
$version = (string) $xml->version;
return $version;
}
}
$version = '0.0.0';
return $version;
}
/**
* Load the language file on instantiation.
*
@@ -96,9 +129,6 @@ class MokoWaaS extends CMSPlugin
// Security: HTTPS redirect (runs for all clients)
$this->enforceHttps();
// Site alias handling: offline page and backend redirect
$this->handleSiteAlias();
// MokoWaaS API endpoints (run before routing)
$mokoAction = $this->app->input->get('mokowaas', '');
@@ -863,12 +893,16 @@ class MokoWaaS extends CMSPlugin
*/
public function onAfterRoute()
{
// Site alias handling: offline page and backend redirect
$this->handleSiteAlias();
if (!$this->app->isClient('administrator'))
{
return;
}
$this->enforceAdminRestrictions();
$this->protectPlugin();
}
/**
@@ -902,6 +936,186 @@ class MokoWaaS extends CMSPlugin
}
$this->injectFavicon($doc);
// Hide MokoWaaS from plugin list for non-master users
if (!$this->isMasterUser())
{
$this->hidePluginFromList($doc);
}
}
/**
* Hide MokoWaaS plugin and package from the extensions list via JS.
*
* @param \Joomla\CMS\Document\HtmlDocument $doc Document object
*
* @return void
*
* @since 02.03.04
*/
protected function hidePluginFromList($doc)
{
$option = $this->app->input->get('option', '');
$view = $this->app->input->get('view', '');
if ($option !== 'com_plugins' && $option !== 'com_installer')
{
return;
}
$doc->addScriptDeclaration("
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('tr').forEach(function(row) {
var text = row.textContent || '';
if (text.indexOf('mokowaas') !== -1 || text.indexOf('MokoWaaS') !== -1) {
row.style.display = 'none';
}
});
});
");
}
/**
* Protect the plugin from being disabled or uninstalled by non-master users.
* Does NOT self-heal (no lock) — master users can still disable if needed.
*
* @return void
*
* @since 02.03.04
*/
protected function protectPlugin()
{
// Ensure protected flag is set (self-healing — runs once per session)
static $flagChecked = false;
if (!$flagChecked)
{
$flagChecked = true;
$this->ensureProtectedFlag();
}
if ($this->isMasterUser())
{
return;
}
$option = $this->app->input->get('option', '');
$task = $this->app->input->get('task', '');
// Block non-master from uninstalling MokoWaaS
if ($option === 'com_installer' && strpos($task, 'manage.remove') !== false)
{
$cid = $this->app->input->get('cid', [], 'array');
if ($this->isOurExtension($cid))
{
$this->app->enqueueMessage('MokoWaaS cannot be uninstalled.', 'error');
$this->app->redirect('index.php?option=com_installer&view=manage');
}
}
// Block non-master from disabling via list toggle
if ($option === 'com_plugins' && strpos($task, 'plugins.publish') !== false)
{
$cid = $this->app->input->get('cid', [], 'array');
if ($this->isOurExtension($cid))
{
$this->app->enqueueMessage('MokoWaaS cannot be disabled.', 'error');
$this->app->redirect('index.php?option=com_plugins');
}
}
}
/**
* Ensure the protected flag is set on MokoWaaS extensions in the DB.
*
* Sets protected=1, locked=0 so the extension can't be disabled or
* uninstalled but can still receive updates and config changes.
*
* @return void
*
* @since 02.03.10
*/
protected function ensureProtectedFlag()
{
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('protected') . ' = 1')
->set($db->quoteName('locked') . ' = 0')
->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas')
. ' OR ' . $db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas') . ')')
->where($db->quoteName('protected') . ' = 0');
$db->setQuery($query);
$db->execute();
}
catch (\Throwable $e)
{
// Non-critical
}
}
/**
* Check if any of the given extension IDs belong to MokoWaaS.
*
* @param array $ids Extension IDs to check
*
* @return bool
*
* @since 02.03.04
*/
protected function isOurExtension(array $ids): bool
{
if (empty($ids))
{
return false;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('extension_id') . ' IN (' . implode(',', array_map('intval', $ids)) . ')')
->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas')
. ' OR ' . $db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas') . ')');
return (int) $db->setQuery($query)->loadResult() > 0;
}
/**
* Prevent non-master users from disabling the plugin via save.
*
* @param string $context Extension context
* @param object $table Extension table row
* @param bool $isNew Whether this is a new record
*
* @return bool False to cancel save
*
* @since 02.03.04
*/
public function onExtensionBeforeSave($context, $table, $isNew)
{
if ($context !== 'com_plugins.plugin')
{
return true;
}
if ($table->element !== 'mokowaas' || $table->folder !== 'system')
{
return true;
}
// Non-master users cannot disable the plugin
if (!$this->isMasterUser() && (int) $table->enabled === 0)
{
$this->app->enqueueMessage('MokoWaaS cannot be disabled.', 'error');
$table->enabled = 1;
}
return true;
}
/**
@@ -1301,7 +1515,7 @@ class MokoWaaS extends CMSPlugin
'users' => $users,
'extensions' => $extensions,
'brand' => $this->params->get('brand_name', 'MokoWaaS'),
'plugin_version' => '02.01.39',
'plugin_version' => $this->getPluginVersion(),
]);
}
@@ -1455,7 +1669,7 @@ class MokoWaaS extends CMSPlugin
return [
'brand' => $this->params->get('brand_name', 'MokoWaaS'),
'plugin_version' => '02.01.22',
'plugin_version' => $this->getPluginVersion(),
'joomla_version' => JVERSION,
'php_version' => PHP_VERSION,
'server_name' => $config->get('sitename', ''),
@@ -2579,12 +2793,84 @@ class MokoWaaS extends CMSPlugin
*
* @since 02.01.43
*/
/**
* Get the primary domain from Joomla config or by exclusion from aliases.
*
* @return string Primary domain hostname
*
* @since 02.03.05
*/
protected function getPrimaryHost(): string
{
// Try plugin's primary_domain setting first
$primaryDomain = $this->params->get('primary_domain', '');
if (!empty($primaryDomain))
{
return trim($primaryDomain);
}
// Try Joomla's $live_site
$liveSite = Factory::getConfig()->get('live_site', '');
if (!empty($liveSite))
{
$host = parse_url($liveSite, PHP_URL_HOST);
if ($host)
{
return $host;
}
}
// Fallback: if current host is NOT in the aliases list, it's the primary
$currentHost = $_SERVER['HTTP_HOST'] ?? '';
$aliases = $this->params->get('site_aliases', '');
if (!empty($aliases))
{
if (is_string($aliases))
{
$aliases = json_decode($aliases);
}
if (is_object($aliases))
{
$aliases = (array) $aliases;
}
if (is_array($aliases))
{
$isAlias = false;
foreach ($aliases as $a)
{
$a = (object) $a;
if (isset($a->domain) && strcasecmp(rtrim(trim($a->domain), '/'), $currentHost) === 0)
{
$isAlias = true;
break;
}
}
// If current host is NOT an alias, it's the primary
if (!$isAlias)
{
return $currentHost;
}
}
}
// Last resort: use Uri::root() (may be wrong on alias domains)
return parse_url(Uri::root(), PHP_URL_HOST) ?: $currentHost;
}
protected function getCurrentAlias()
{
$currentHost = $_SERVER['HTTP_HOST'] ?? '';
$primaryHost = parse_url(Uri::root(), PHP_URL_HOST);
if (empty($currentHost) || strcasecmp($currentHost, $primaryHost) === 0)
if (empty($currentHost))
{
return null;
}
@@ -2596,22 +2882,29 @@ class MokoWaaS extends CMSPlugin
return null;
}
// Subform returns JSON string or array
// Subform returns JSON string, array, or stdClass
if (is_string($aliases))
{
$aliases = json_decode($aliases, false);
$aliases = json_decode($aliases);
}
if (!is_array($aliases))
// Convert object to array (Joomla subform stores as {"key0":{...},"key1":{...}})
if (is_object($aliases))
{
$aliases = (array) $aliases;
}
if (!is_array($aliases) || empty($aliases))
{
return null;
}
// Look up the current host in the aliases list — if found, it's an alias
foreach ($aliases as $alias)
{
$alias = (object) $alias;
if (isset($alias->domain) && strcasecmp(trim($alias->domain), $currentHost) === 0)
if (isset($alias->domain) && strcasecmp(rtrim(trim($alias->domain), '/'), $currentHost) === 0)
{
return $alias;
}
@@ -2642,15 +2935,16 @@ class MokoWaaS extends CMSPlugin
if (!empty($alias->redirect_backend) && $alias->redirect_backend === '1'
&& $this->app->isClient('administrator'))
{
$primaryUrl = rtrim(Uri::root(), '/') . '/administrator' . Uri::getInstance()->toString(['path', 'query']);
$adminPath = str_replace(Uri::root() . 'administrator', '', Uri::getInstance()->toString(['path', 'query']));
$primaryUrl = rtrim(Uri::root(), '/') . '/administrator' . $adminPath;
$primaryHost = $this->getPrimaryHost();
$currentUri = Uri::getInstance();
$scheme = $currentUri->getScheme() ?: 'https';
$primaryUrl = $scheme . '://' . $primaryHost . $currentUri->toString(['path', 'query']);
$this->app->redirect($primaryUrl, 301);
}
// Offline: show maintenance page for frontend requests
if (!empty($alias->offline) && $alias->offline === '1'
// Offline: use Joomla's native offline mode for frontend requests
if (!empty($alias->offline) && (string) $alias->offline === '1'
&& $this->app->isClient('site'))
{
// Allow health API to still respond
@@ -2659,22 +2953,16 @@ class MokoWaaS extends CMSPlugin
return;
}
$message = $alias->offline_message ?? 'This site is currently offline for maintenance.';
$brandName = $this->params->get('brand_name', 'MokoWaaS');
// Set custom offline message if provided
$message = $alias->offline_message ?? '';
header('HTTP/1.1 503 Service Unavailable');
header('Retry-After: 3600');
header('Content-Type: text/html; charset=utf-8');
echo '<!DOCTYPE html><html><head><meta charset="utf-8">';
echo '<meta name="robots" content="noindex, nofollow">';
echo '<title>' . htmlspecialchars($brandName) . ' - Maintenance</title>';
echo '<style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f5f5f5;color:#333}';
echo '.container{text-align:center;padding:2rem;max-width:600px}h1{color:#1a2744;margin-bottom:1rem}p{font-size:1.1rem;line-height:1.6}</style>';
echo '</head><body><div class="container">';
echo '<h1>' . htmlspecialchars($brandName) . '</h1>';
echo '<p>' . htmlspecialchars($message) . '</p>';
echo '</div></body></html>';
$this->app->close();
if (!empty($message))
{
$this->app->getConfig()->set('offline_message', $message);
}
// Enable Joomla's native offline mode — renders through the template's offline.php
$this->app->getConfig()->set('offline', 1);
}
}
@@ -2702,6 +2990,12 @@ class MokoWaaS extends CMSPlugin
{
$doc->setMetaData('robots', $robots);
}
// Inject canonical URL pointing to the primary domain
$primaryHost = $this->getPrimaryHost();
$currentUri = Uri::getInstance();
$canonical = $currentUri->getScheme() . '://' . $primaryHost . $currentUri->toString(['path', 'query']);
$doc->addHeadLink($canonical, 'canonical');
}
// ------------------------------------------------------------------
@@ -2734,8 +3028,39 @@ class MokoWaaS extends CMSPlugin
$siteUrl = rtrim(Uri::root(), '/');
$siteName = Factory::getConfig()->get('sitename', 'Joomla');
// Register primary domain only — aliases should not get separate datasources
// Register primary domain
$this->sendHeartbeat($siteUrl, $siteName, $healthToken, $app);
// Register alias domains (subform format)
$aliases = $params->get('site_aliases', '');
if (!empty($aliases))
{
if (is_string($aliases))
{
$aliases = json_decode($aliases);
}
if (is_object($aliases))
{
$aliases = (array) $aliases;
}
if (is_array($aliases))
{
foreach ($aliases as $alias)
{
$alias = (object) $alias;
if (!empty($alias->domain))
{
$domain = rtrim(trim($alias->domain), '/');
$aliasUrl = 'https://' . preg_replace('#^https?://#i', '', $domain);
$this->sendHeartbeat($aliasUrl, $siteName, $healthToken, $app);
}
}
}
}
}
/**
@@ -133,6 +133,8 @@ PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
; ===== Site Aliases fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC="Configure additional domains that mirror this site. Each alias can have its own offline status, robots directive, and backend redirect behavior."
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_LABEL="Primary Domain"
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_DESC="The primary domain for this site (e.g. waas.dev.mokoconsulting.tech). Used for backend redirect on alias domains. Do not include https:// prefix."
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Domain Aliases"
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Add domain aliases that serve as mirrors of this site. Each alias gets its own Grafana monitoring datasource."
PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_LABEL="Domain"
@@ -133,6 +133,8 @@ PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
; ===== Site Aliases fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC="Configure additional domains that mirror this site. Each alias can have its own offline status, robots directive, and backend redirect behavior."
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_LABEL="Primary Domain"
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_DESC="The primary domain for this site (e.g. waas.dev.mokoconsulting.tech). Used for backend redirect on alias domains. Do not include https:// prefix."
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Domain Aliases"
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Add domain aliases that serve as mirrors of this site. Each alias gets its own Grafana monitoring datasource."
PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_LABEL="Domain"
@@ -30,7 +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.03.00</version>
<version>02.03.11</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>
@@ -273,6 +273,14 @@
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC"
>
<field
name="primary_domain"
type="text"
label="PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_LABEL"
description="PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_DESC"
default=""
hint="e.g. waas.dev.mokoconsulting.tech"
/>
<field
name="site_aliases"
type="subform"
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>01.00.00</version>
<version>02.03.11</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>
+1 -1
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade">
<name>MokoWaaS</name>
<packagename>mokowaas</packagename>
<version>02.03.02</version>
<version>02.03.11</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+104 -6
View File
@@ -15,7 +15,8 @@ use Joomla\CMS\Log\Log;
/**
* Package installation script for MokoWaaS.
*
* Auto-enables the system plugin and webservices plugin after install.
* Handles migration from standalone plugin to package, enables plugins,
* and triggers heartbeat registration on install/update.
*
* @since 2.2.0
*/
@@ -33,11 +34,14 @@ class Pkg_MokowaasInstallerScript
*/
public function postflight($type, $parent)
{
if ($type === 'install' || $type === 'discover_install')
{
$this->enablePlugin('system', 'mokowaas');
$this->enablePlugin('webservices', 'mokowaas');
}
$this->enablePlugin('system', 'mokowaas');
$this->enablePlugin('webservices', 'mokowaas');
// Mark MokoWaaS extensions as protected (prevents disable/uninstall at framework level)
$this->protectExtensions();
// Trigger heartbeat registration
$this->sendHeartbeat();
}
/**
@@ -69,4 +73,98 @@ class Pkg_MokowaasInstallerScript
Log::add('Error enabling plugin ' . $group . '/' . $element . ': ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Set the protected flag on all MokoWaaS extensions.
*
* Joomla's protected flag prevents disabling and uninstalling at the
* framework level — no plugin-side interception needed.
*
* @return void
*
* @since 02.03.10
*/
private function protectExtensions(): void
{
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('protected') . ' = 1')
->set($db->quoteName('locked') . ' = 0')
->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas')
. ' OR ' . $db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas') . ')');
$db->setQuery($query);
$db->execute();
}
catch (\Throwable $e)
{
Log::add('Error protecting MokoWaaS extensions: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Send heartbeat to the MokoWaaS monitoring receiver.
*
* @return void
*
* @since 02.03.08
*/
private function sendHeartbeat(): void
{
try
{
$db = Factory::getDbo();
$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'));
$params = json_decode((string) $db->setQuery($query)->loadResult());
$healthToken = $params->health_api_token ?? '';
if (empty($healthToken))
{
return;
}
$siteUrl = rtrim(\Joomla\CMS\Uri\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('https://bench.mokoconsulting.tech/api/waas-heartbeat/register');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-MokoWaaS-Key: moko-waas-hb-2026-x9k4m',
]);
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);
curl_close($ch);
if ($code >= 200 && $code < 300)
{
Factory::getApplication()->enqueueMessage('Grafana heartbeat: site registered', 'message');
}
}
catch (\Throwable $e)
{
// Silent failure — heartbeat is non-critical
}
}
}