diff --git a/.gitignore b/.gitignore index 4229e70..2f7a4ac 100644 --- a/.gitignore +++ b/.gitignore @@ -203,3 +203,5 @@ venv/ *.coverage hypothesis/ +profile.ps1 +TODO.md diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index e60e8c0..e4b34d7 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -18,7 +18,7 @@ PHP - plugin + package src/ diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 57d3380..903f1b9 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -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 tag if [ "$PLATFORM" = "joomla" ]; then - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1 || true) + MANIFEST=$(find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" -print0 2>/dev/null | xargs -0 grep -l '/dev/null | head -1 || true) if [ -n "$MANIFEST" ]; then - ELEM=$(grep -oP "\K[^<]+" "$MANIFEST" 2>/dev/null | head -1) + ELEM=$(grep -oP "\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 + } diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ce925..fa34726 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index fadf282..a341c73 100644 --- a/README.md +++ b/README.md @@ -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) --- diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 488a8ab..0000000 --- a/TODO.md +++ /dev/null @@ -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 - - diff --git a/src/packages/com_mokowaas/mokowaas.xml b/src/packages/com_mokowaas/mokowaas.xml index 5481612..155f0b4 100644 --- a/src/packages/com_mokowaas/mokowaas.xml +++ b/src/packages/com_mokowaas/mokowaas.xml @@ -7,7 +7,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 01.00.00 + 02.03.11 Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups. Moko\Component\MokoWaaS\Api diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 9766490..62e935b 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -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 ''; - echo ''; - echo '' . htmlspecialchars($brandName) . ' - Maintenance'; - echo ''; - echo '
'; - echo '

' . htmlspecialchars($brandName) . '

'; - echo '

' . htmlspecialchars($message) . '

'; - echo '
'; - $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); + } + } + } + } } /** diff --git a/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini b/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini index 415df26..aa455c4 100644 --- a/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini +++ b/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini @@ -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" diff --git a/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini b/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini index 62cfa64..71d1574 100644 --- a/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini +++ b/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini @@ -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" diff --git a/src/packages/plg_system_mokowaas/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml index 6aa8e25..e7f7e1c 100644 --- a/src/packages/plg_system_mokowaas/mokowaas.xml +++ b/src/packages/plg_system_mokowaas/mokowaas.xml @@ -30,7 +30,7 @@ GNU General Public License version 3 or later; see LICENSE.md hello@mokoconsulting.tech https://mokoconsulting.tech - 02.03.00 + 02.03.11 This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform. Moko\Plugin\System\MokoWaaS script.php @@ -273,6 +273,14 @@ label="PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL" description="PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC" > + GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 01.00.00 + 02.03.11 Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info. Moko\Plugin\WebServices\MokoWaaS diff --git a/src/pkg_mokowaas.xml b/src/pkg_mokowaas.xml index 108d625..5fb2655 100644 --- a/src/pkg_mokowaas.xml +++ b/src/pkg_mokowaas.xml @@ -2,7 +2,7 @@ MokoWaaS mokowaas - 02.03.02 + 02.03.11 2026-05-23 Moko Consulting hello@mokoconsulting.tech diff --git a/src/script.php b/src/script.php index bc35c26..cc58abf 100644 --- a/src/script.php +++ b/src/script.php @@ -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 + } + } }