fix(plugin): multi-select slugs, full path matching, Joomla 5 DI, pretty name #83

Merged
jmiller merged 22 commits from dev into main 2026-05-16 22:33:15 +00:00
19 changed files with 341 additions and 399 deletions
+4 -4
View File
@@ -8,18 +8,18 @@
<identity>
<name>MokoJoomTOS</name>
<org>MokoConsulting</org>
<description>A component to present a sites Term of Service and privacy policy even through offline.</description>
<description>Joomla system plugin to keep legal pages accessible during offline mode</description>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
<platform>joomla</platform>
<standards-version>04.07.00</standards-version>
<standards-version>05.00.00</standards-version>
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
<last-synced>2026-05-10T19:51:05+00:00</last-synced>
<last-synced>2026-05-16T17:30:00+00:00</last-synced>
</governance>
<build>
<language>PHP</language>
<package-type>joomla</package-type>
<package-type>joomla-extension</package-type>
<entry-point>src/</entry-point>
</build>
</moko-platform>
+3 -4
View File
@@ -55,12 +55,11 @@ jobs:
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
# Read platform from .gitea/manifest.xml
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .gitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./.gitea/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
+21 -1
View File
@@ -22,7 +22,7 @@
DEFGROUP: MokoJoomTOS
INGROUP: plg_system_mokojoomtos
REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS
VERSION: 03.09.00
VERSION: 04.00.00
PATH: ./CHANGELOG.md
BRIEF: Version history and release notes
-->
@@ -36,6 +36,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [04.00.00] - 2026-05-16
### Added
- Multi-select support: configure multiple menu items to remain accessible during offline mode
- `services/provider.php` for Joomla 5 dependency injection container registration
- Gitea-hosted update server URL in plugin manifest
### Changed
- Match against full menu route path instead of alias only (fixes nested routes like `/legal/terms-of-service`)
- Plugin pretty name updated to Joomla convention: "System - Moko Terms of Service"
- Renamed `.mokogitea/` to `.gitea/` for standard Gitea compatibility
- MenuslugField now stores and displays full route paths (e.g., `legal/terms-of-service`)
### Removed
- GitHub update server references (fully migrated to Gitea)
- Legacy `src/plugins/` directory
## [03.09.00] - 2026-05-16
### Fixed
+4 -4
View File
@@ -75,7 +75,7 @@ XML files use the MokoStandard header format:
DEFGROUP: MokoJoomTOS
INGROUP: plg_system_mokojoomtos
PATH: src/mokojoomtos.xml
VERSION: 03.09.00
VERSION: 04.00.00
BRIEF: [Brief description of file purpose]
=========================================================================
-->
@@ -88,7 +88,7 @@ Markdown files use an HTML comment format with the same structure.
- **DEFGROUP**: Top-level group (always `MokoJoomTOS` for this repo)
- **INGROUP**: Subgroup/component (always `plg_system_mokojoomtos`)
- **PATH**: Relative path from repository root (e.g., `src/mokojoomtos.xml`)
- **VERSION**: Current plugin version (currently `03.09.00`)
- **VERSION**: Current plugin version (currently `04.00.00`)
- **BRIEF**: One-line description of file's purpose
### Exempt Files
@@ -327,7 +327,7 @@ No automated test infrastructure exists in this repository. Manual testing requi
```bash
# Manual packaging (build scripts being migrated to scripts/ directory)
cd src/
zip -r ../plg_system_mokojoomtos-03.09.00.zip .
zip -r ../plg_system_mokojoomtos-04.00.00.zip .
```
Package should contain: `mokojoomtos.php`, `mokojoomtos.xml`, `script.php`, `src/`, `language/`, `administrator/`
@@ -397,7 +397,7 @@ Before opening a pull request, ensure:
- [ ] CHANGELOG.md updated with changes under correct version
- [ ] README.md updated if user-facing changes
- [ ] All markdown file headers include VERSION: 03.09.00
- [ ] All markdown file headers include VERSION: 04.00.00
- [ ] XML file headers include complete FILE INFORMATION block
## Version Management
+2 -2
View File
@@ -2,14 +2,14 @@
A Joomla system plugin that keeps your Terms of Service, Privacy Policy, or any legal page accessible to visitors -- even when the site is in offline (maintenance) mode.
![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-blue?style=flat-square&logo=joomla&logoColor=white) ![PHP](https://img.shields.io/badge/PHP-%E2%89%A58.1-777BB4?style=flat-square&logo=php&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square) ![Version](https://img.shields.io/badge/version-03.09.00-orange?style=flat-square) ![Type](https://img.shields.io/badge/type-system%20plugin-blueviolet?style=flat-square)
![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-blue?style=flat-square&logo=joomla&logoColor=white) ![PHP](https://img.shields.io/badge/PHP-%E2%89%A58.1-777BB4?style=flat-square&logo=php&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square) ![Version](https://img.shields.io/badge/version-04.00.00-orange?style=flat-square) ![Type](https://img.shields.io/badge/type-system%20plugin-blueviolet?style=flat-square)
| Field | Value |
|---|---|
| **Author** | [Moko Consulting](https://mokoconsulting.tech) |
| **License** | GPL-3.0-or-later |
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS) |
| **Version** | 03.09.00 |
| **Version** | 04.00.00 |
---
View File
-16
View File
@@ -1,16 +0,0 @@
# Docs Index: /templates/repos/joomla/component/docs
## Purpose
This index provides navigation to documentation within this folder.
## Metadata
- **Document Type:** index
- **Auto-generated:** This file is automatically generated by rebuild_indexes.py
## Revision History
| Change | Notes | Author |
| --- | --- | --- |
| Automated update | Generated by documentation index automation | rebuild_indexes.py |
-119
View File
@@ -1,119 +0,0 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: MokoJoomTOS.Documentation
INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS
PATH: /docs/update-server.md
VERSION: 04.04.00
BRIEF: How this extension's Joomla update server file (update.xml) is managed
-->
# Joomla Update Server
[![MokoStandards](https://img.shields.io/badge/MokoStandards-04.04.00-blue)](https://github.com/mokoconsulting-tech/MokoStandards)
This document explains how `update.xml` is automatically managed for this Joomla extension following the [Joomla Update Server specification](https://docs.joomla.org/Deploying_an_Update_Server).
## How It Works
Joomla checks for extension updates by fetching an XML file from the URL defined in the `<updateservers>` tag in the extension's XML manifest. MokoStandards generates this file automatically.
### Automatic Generation
| Event | Workflow | `<tag>` | `<version>` |
|-------|----------|---------|-------------|
| Merge to `main` | `auto-release.yml` | `stable` | `XX.YY.ZZ` |
| Push to `dev/**` | `deploy-dev.yml` | `development` | `development` |
| Push to `rc/**` | `deploy-dev.yml` | `rc` | `XX.YY.ZZ-rc` |
### Generated XML Structure
```xml
<?xml version="1.0" encoding="utf-8"?>
<updates>
<update>
<name>Extension Name</name>
<description>Extension Name update</description>
<element>com_extensionname</element>
<type>component</type>
<version>01.02.03</version>
<client>site</client>
<folder>system</folder> <!-- plugins only -->
<tags>
<tag>stable</tag>
</tags>
<infourl title="Extension Name">https://github.com/.../releases/tag/v01.02.03</infourl>
<downloads>
<downloadurl type="full" format="zip">https://github.com/.../releases/download/v01.02.03/com_ext-01.02.03.zip</downloadurl>
</downloads>
<targetplatform name="joomla" version="((5\.[0-9])|(6\.[0-9]))" />
<php_minimum>8.2</php_minimum> <!-- if present in manifest -->
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
</updates>
```
### Metadata Source
All metadata is extracted from the extension's XML manifest (`src/*.xml`) at build time:
| XML Element | Source | Notes |
|-------------|--------|-------|
| `<name>` | `<name>` in manifest | Extension display name |
| `<element>` | `<element>` in manifest | Must match installed extension identifier |
| `<type>` | `type` attribute on `<extension>` | `component`, `module`, `plugin`, `library`, `package`, `template` |
| `<client>` | `client` attribute on `<extension>` | `site` or `administrator`**required for plugins and modules** |
| `<folder>` | `group` attribute on `<extension>` | Plugin group (e.g., `system`, `content`) — **required for plugins** |
| `<targetplatform>` | `<targetplatform>` in manifest | Falls back to Joomla 5.x / 6.x if not specified |
| `<php_minimum>` | `<php_minimum>` in manifest | Included only if present |
### Extension Manifest Setup
Your XML manifest must include an `<updateservers>` tag pointing to the `update.xml` on the `main` branch:
```xml
<extension type="component" client="site" method="upgrade">
<name>My Extension</name>
<element>com_myextension</element>
<!-- ... -->
<updateservers>
<server type="extension" name="My Extension Updates">
https://raw.githubusercontent.com/mokoconsulting-tech/MokoJoomTOS/main/update.xml
</server>
</updateservers>
</extension>
```
### Branch Lifecycle
```
dev/XX.YY.ZZ → rc/XX.YY.ZZ → main → version/XX.YY
(development) (rc) (stable) (frozen snapshot)
```
1. **Development** (`dev/**`): `update.xml` with `<tag>development</tag>`, download points to branch archive
2. **Release Candidate** (`rc/**`): `update.xml` with `<tag>rc</tag>`, version set to `XX.YY.ZZ-rc`
3. **Stable Release** (merge to `main`): `update.xml` with `<tag>stable</tag>`, download points to GitHub Release asset
4. **Frozen Snapshot** (`version/XX.YY`): immutable, never force-pushed
### Health Checks
The `repo_health.yml` workflow verifies on every commit:
- `update.xml` exists in the repository root
- XML manifest exists with `<extension>` tag
- `<version>`, `<name>`, `<author>`, `<namespace>` tags present
- Extension `type` attribute is valid
- Language `.ini` files exist
- `index.html` directory listing protection in `src/`, `src/admin/`, `src/site/`
---
*Managed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). See [docs/workflows/update-server.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/update-server.md) for the full specification.*
@@ -3,13 +3,13 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when the site is in offline mode. Simply configure the menu slug (e.g., 'terms-of-service') and that page will remain accessible even when the site is offline."
; Configuration
PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Terms of Service Menu Slug"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Enter the menu slug for your Terms of Service page (e.g., 'terms-of-service'). This page will be accessible even when the site is offline. The slug must match the menu item alias exactly."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
@@ -3,5 +3,5 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when site is offline"
@@ -3,13 +3,13 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when the site is in offline mode. Simply configure the menu slug (e.g., 'terms-of-service') and that page will remain accessible even when the site is offline."
; Configuration
PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Terms of Service Menu Slug"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Enter the menu slug for your Terms of Service page (e.g., 'terms-of-service'). This page will be accessible even when the site is offline. The slug must match the menu item alias exactly."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
@@ -3,5 +3,5 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when site is offline"
@@ -3,13 +3,13 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when the site is in offline mode. Simply configure the menu slug (e.g., 'terms-of-service') and that page will remain accessible even when the site is offline."
; Configuration
PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Terms of Service Menu Slug"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Enter the menu slug for your Terms of Service page (e.g., 'terms-of-service'). This page will be accessible even when the site is offline. The slug must match the menu item alias exactly."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
@@ -3,13 +3,13 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when the site is in offline mode. Simply configure the menu slug (e.g., 'terms-of-service') and that page will remain accessible even when the site is offline."
; Configuration
PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Terms of Service Menu Slug"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Enter the menu slug for your Terms of Service page (e.g., 'terms-of-service'). This page will be accessible even when the site is offline. The slug must match the menu item alias exactly."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
+81 -72
View File
@@ -8,98 +8,107 @@
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
/**
* MokoJoomTOS Offline Mode Bypass Plugin
* MokoJoomTOS Offline Mode Bypass Plugin (Legacy)
*
* Allows Terms of Service menu to be accessible via slug when the site
* Allows configured menu items to remain accessible when the site
* is in offline mode.
*
* @since 1.0.0
*/
class PlgSystemMokojoomtos extends CMSPlugin
{
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 1.0.0
*/
protected $autoloadLanguage = true;
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 1.0.0
*/
protected $autoloadLanguage = true;
/**
* Application object
*
* @var \Joomla\CMS\Application\CMSApplication
* @since 1.0.0
*/
protected $app;
/**
* Application object
*
* @var \Joomla\CMS\Application\CMSApplication
* @since 1.0.0
*/
protected $app;
/**
* After route event handler
*
* Checks if the current request is for the Terms of Service slug and if
* the site is in offline mode. If both conditions are met, temporarily
* disables offline mode and sets component-only view for this request.
*
* This event fires after routing but before template selection, making it
* the correct place to set tmpl=component to prevent template chrome loading.
*
* @return void
*
* @since 03.09.00
*/
public function onAfterRoute()
{
// Only process for site application
if (!$this->app->isClient('site'))
{
return;
}
/**
* After route event handler
*
* @return void
*
* @since 04.00.00
*/
public function onAfterRoute()
{
// Only process for site application
if (!$this->app->isClient('site'))
{
return;
}
// Get the global configuration
$config = $this->app->getConfig();
// Get the global configuration
$config = $this->app->getConfig();
// Only proceed if site is offline
if (!$config->get('offline'))
{
return;
}
// Only proceed if site is offline
if (!$config->get('offline'))
{
return;
}
// Get the configured Terms of Service slug
$tosSlug = trim($this->params->get('tos_slug', 'terms-of-service'));
// Get the configured slugs (stored as array for multi-select)
$slugs = $this->params->get('tos_slug', []);
if (empty($tosSlug))
{
return;
}
// Handle legacy single-value string format
if (is_string($slugs))
{
$slugs = array_filter([trim($slugs)]);
}
// Get the current URI path
$uri = Uri::getInstance();
$path = trim($uri->getPath(), '/');
if (empty($slugs))
{
return;
}
// Remove the base path if present
$base = trim(Uri::base(true), '/');
if (!empty($base) && strpos($path, $base) === 0)
{
$path = trim(substr($path, strlen($base)), '/');
}
// Get the current URI path
$uri = Uri::getInstance();
$path = trim($uri->getPath(), '/');
// Check if the path matches the Terms of Service slug
if ($path === $tosSlug || strpos($path, $tosSlug . '/') === 0)
{
// Temporarily disable offline mode for this request
$config->set('offline', 0);
// Remove the base path if present
$base = trim(Uri::base(true), '/');
if (!empty($base) && strpos($path, $base) === 0)
{
$path = trim(substr($path, strlen($base)), '/');
}
// Set component-only view (no template chrome)
$input = $this->app->input;
$input->set('tmpl', 'component');
// Check if the path matches any configured slug
foreach ($slugs as $slug)
{
$slug = trim($slug);
if (empty($slug))
{
continue;
}
// Also set in GET superglobal to ensure recognition
$_GET['tmpl'] = 'component';
}
}
if ($path === $slug || strpos($path, $slug . '/') === 0)
{
// Temporarily disable offline mode for this request
$config->set('offline', 0);
// Set component-only view (no template chrome)
$input = $this->app->input;
$input->set('tmpl', 'component');
// Also set in GET superglobal to ensure recognition
$_GET['tmpl'] = 'component';
return;
}
}
}
}
+5 -5
View File
@@ -25,19 +25,19 @@
DEFGROUP: MokoJoomTOS
INGROUP: plg_system_mokojoomtos
PATH: src/mokojoomtos.xml
VERSION: 03.09.00
VERSION: 04.00.00
BRIEF: Plugin manifest XML file for MokoJoomTOS system plugin
=========================================================================
-->
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_mokojoomtos</name>
<author>Moko Consulting</author>
<creationDate>2026-01-01</creationDate>
<creationDate>2026-05-16</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>03.09.00</version>
<version>00.00.01</version>
<description>PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\MokoJoomTOS</namespace>
@@ -47,6 +47,7 @@
<files>
<filename plugin="mokojoomtos">mokojoomtos.php</filename>
<folder>src</folder>
<folder>services</folder>
<folder>language</folder>
<folder>administrator</folder>
</files>
@@ -68,8 +69,7 @@
type="menuslug"
label="PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL"
description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC"
default="terms-of-service"
required="true"
multiple="true"
/>
<field
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* @package MokoJoomTOS
* @subpackage plg_system_mokojoomtos
* @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 Joomla\Plugin\System\MokoJoomTOS\Extension\MokoJoomTOS;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 04.00.00
*/
public function register(Container $container)
{
$container->set(
PluginInterface::class,
function (Container $container) {
$plugin = new MokoJoomTOS(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'mokojoomtos')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
+97 -90
View File
@@ -10,7 +10,6 @@ namespace Joomla\Plugin\System\MokoJoomTOS\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
use Joomla\Event\SubscriberInterface;
@@ -18,106 +17,114 @@ use Joomla\Event\SubscriberInterface;
/**
* MokoJoomTOS Offline Mode Bypass Plugin
*
* Allows Terms of Service menu to be accessible via slug when the site
* Allows configured menu items to remain accessible when the site
* is in offline mode.
*
* @since 1.0.0
*/
final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
{
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 1.0.0
*/
protected $autoloadLanguage = true;
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 1.0.0
*/
protected $autoloadLanguage = true;
/**
* Application object
*
* @var \Joomla\CMS\Application\CMSApplication
* @since 1.0.0
*/
protected $app;
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 1.0.0
*/
public static function getSubscribedEvents(): array
{
return [
'onAfterRoute' => 'onAfterRoute',
];
}
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 1.0.0
*/
public static function getSubscribedEvents(): array
{
return [
'onAfterRoute' => 'onAfterRoute',
];
}
/**
* After route event handler
*
* Checks if the current request matches any configured menu slug and if
* the site is in offline mode. If both conditions are met, temporarily
* disables offline mode and sets component-only view for this request.
*
* This event fires after routing but before template selection, making it
* the correct place to set tmpl=component to prevent template chrome loading.
*
* @return void
*
* @since 1.0.0
*/
public function onAfterRoute()
{
// Only process for site application
if (!$this->getApplication()->isClient('site'))
{
return;
}
/**
* After route event handler
*
* Checks if the current request is for the Terms of Service slug and if
* the site is in offline mode. If both conditions are met, temporarily
* disables offline mode and sets component-only view for this request.
*
* This event fires after routing but before template selection, making it
* the correct place to set tmpl=component to prevent template chrome loading.
*
* @return void
*
* @since 1.0.0
*/
public function onAfterRoute()
{
// Only process for site application
if (!$this->app->isClient('site'))
{
return;
}
// Get the global configuration
$config = $this->getApplication()->getConfig();
// Get the global configuration
$config = $this->app->getConfig();
// Only proceed if site is offline
if (!$config->get('offline'))
{
return;
}
// Only proceed if site is offline
if (!$config->get('offline'))
{
return;
}
// Get the configured Terms of Service slug
$tosSlug = trim($this->params->get('tos_slug', 'terms-of-service'));
if (empty($tosSlug))
{
return;
}
// Get the configured slugs (stored as array for multi-select)
$slugs = $this->params->get('tos_slug', []);
// Get the current URI path
$uri = Uri::getInstance();
$path = trim($uri->getPath(), '/');
// Remove the base path if present
$base = trim(Uri::base(true), '/');
if (!empty($base) && strpos($path, $base) === 0)
{
$path = trim(substr($path, strlen($base)), '/');
}
// Handle legacy single-value string format
if (is_string($slugs))
{
$slugs = array_filter([trim($slugs)]);
}
// Check if the path matches the Terms of Service slug
if ($path === $tosSlug || strpos($path, $tosSlug . '/') === 0)
{
// Temporarily disable offline mode for this request
$config->set('offline', 0);
// Set component-only view (no template chrome)
// This ensures clean display without full site template
$input = $this->app->input;
$input->set('tmpl', 'component');
// Also set it in the GET superglobal to ensure it's recognized
$_GET['tmpl'] = 'component';
}
}
if (empty($slugs))
{
return;
}
// Get the current URI path
$uri = Uri::getInstance();
$path = trim($uri->getPath(), '/');
// Remove the base path if present
$base = trim(Uri::base(true), '/');
if (!empty($base) && strpos($path, $base) === 0)
{
$path = trim(substr($path, strlen($base)), '/');
}
// Check if the path matches any configured slug
foreach ($slugs as $slug)
{
$slug = trim($slug);
if (empty($slug))
{
continue;
}
if ($path === $slug || strpos($path, $slug . '/') === 0)
{
// Temporarily disable offline mode for this request
$config->set('offline', 0);
// Set component-only view (no template chrome)
$input = $this->getApplication()->input;
$input->set('tmpl', 'component');
// Also set in GET superglobal to ensure recognition
$_GET['tmpl'] = 'component';
return;
}
}
}
}
+66 -68
View File
@@ -17,83 +17,81 @@ use Joomla\CMS\Language\Text;
/**
* Menu Slug Field
*
* Provides a dropdown list of menu items with their aliases (slugs)
* Provides a multi-select dropdown of menu items with their full route paths
*
* @since 1.0.0
*/
class MenuslugField extends ListField
{
/**
* The form field type.
*
* @var string
* @since 1.0.0
*/
protected $type = 'Menuslug';
/**
* The form field type.
*
* @var string
* @since 1.0.0
*/
protected $type = 'Menuslug';
/**
* Method to get the field options.
*
* @return array The field option objects.
*
* @since 1.0.0
*/
protected function getOptions()
{
$options = parent::getOptions();
/**
* Method to get the field options.
*
* @return array The field option objects.
*
* @since 1.0.0
*/
protected function getOptions()
{
$options = parent::getOptions();
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['alias', 'title', 'menutype']))
->from($db->quoteName('#__menu'))
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('client_id') . ' = 0')
->where($db->quoteName('alias') . ' != ' . $db->quote(''))
->order($db->quoteName('menutype') . ', ' . $db->quoteName('title'));
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['path', 'alias', 'title', 'menutype']))
->from($db->quoteName('#__menu'))
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('client_id') . ' = 0')
->where($db->quoteName('alias') . ' != ' . $db->quote(''))
->order($db->quoteName('menutype') . ', ' . $db->quoteName('title'));
$db->setQuery($query);
$menuItems = $db->loadObjectList();
$db->setQuery($query);
$menuItems = $db->loadObjectList();
if ($menuItems)
{
$lastMenuType = '';
foreach ($menuItems as $item)
{
// Add menu type separator for better organization
if ($item->menutype !== $lastMenuType)
{
if ($lastMenuType !== '')
{
// Add a separator between menu types
$options[] = (object) [
'value' => '',
'text' => '──────────────',
'disable' => true
];
}
$lastMenuType = $item->menutype;
}
if ($menuItems)
{
$lastMenuType = '';
$displayText = $item->title !== '' ? $item->title : ucwords(str_replace(['-', '_'], ' ', $item->alias));
$options[] = (object) [
'value' => $item->alias,
'text' => $displayText . ' (' . $item->alias . ')'
];
}
}
}
catch (\Exception $e)
{
// Log error but don't break the form
Factory::getApplication()->enqueueMessage(
Text::sprintf('PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS', $e->getMessage()),
'warning'
);
}
foreach ($menuItems as $item)
{
// Add menu type separator for better organization
if ($item->menutype !== $lastMenuType)
{
if ($lastMenuType !== '')
{
$options[] = (object) [
'value' => '',
'text' => '──────────────',
'disable' => true
];
}
$lastMenuType = $item->menutype;
}
return $options;
}
$displayText = $item->title !== '' ? $item->title : ucwords(str_replace(['-', '_'], ' ', $item->alias));
$options[] = (object) [
'value' => $item->path,
'text' => $displayText . ' (/' . $item->path . ')'
];
}
}
}
catch (\Exception $e)
{
Factory::getApplication()->enqueueMessage(
Text::sprintf('PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS', $e->getMessage()),
'warning'
);
}
return $options;
}
}