feat: include children toggle, manifest fix, and bug fixes #88

Merged
jmiller merged 4 commits from dev into main 2026-05-23 22:45:56 +00:00
11 changed files with 610 additions and 595 deletions
+45 -106
View File
@@ -21,8 +21,8 @@
# FILE INFORMATION
DEFGROUP: MokoJoomTOS
INGROUP: plg_system_mokojoomtos
REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS
VERSION: 04.01.00
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS
VERSION: 04.02.01
PATH: ./CHANGELOG.md
BRIEF: Version history and release notes
-->
@@ -36,6 +36,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- enablePlugin() now called unconditionally in postflight() (#89)
- Replaced Table::addIncludePath() with bootComponent()->getMVCFactory() for Joomla 5 (#90)
- Added non-SEF URL fallback via Itemid matching (#91)
- enablePlugin() now fires on upgrade path (#92)
- Removed $_GET superglobal mutation (#93)
- Added params, metadata, attribs defaults to article creation (#94)
- Applied urldecode() to URI path before slug comparison (#95)
- Cast Registry return to array before iterating slugs (#96)
- Fixed MenuslugField separator `disable` to `disabled` property (#99)
### Added
- SEF disabled warning in MenuslugField dropdown (#97)
- Include Children toggle for offline-accessible menu items (defaults to Yes)
### Fixed (Manifest)
- Hardcode description in XML manifest (language variables don't resolve during install)
### Changed
- Stripped legacy mokojoomtos.php to minimal stub (#101)
- Converted script.php indentation from spaces to tabs (#102)
- Renamed installer class to PlgSystemMokojoomtosInstallerScript (#103)
## [04.01.00] - 2026-05-16
### Fixed
@@ -55,7 +82,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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
@@ -77,119 +103,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Added Gitea update server URL to plugin manifest (`updates.xml` on `main`)
- Removed obsolete `src/plugins/` legacy directory
- Fixed template chrome loading issue by changing event hook from onAfterInitialise to onAfterRoute
- Component-only view now properly applied when site is offline
### Removed
- Legacy duplicate manifest at `src/plugins/system/mokojoomtos/mokojoomtos.xml`
## [03.09.00] - 2026-02-28
### Changed
- Updated version number to 03.09.00 across all files
- Fixed template chrome loading issue by changing event hook from onAfterInitialise to onAfterRoute
### Fixed
- Template chrome (header, footer, modules) no longer loads when accessing TOS page in offline mode
- Component-only view now properly applied when site is offline
## [1.0.0] - 2026-01-16
### Added
- Initial template repository structure
- Comprehensive Makefile with 30+ build targets
- Complete README.md with usage documentation
- CONTRIBUTING.md with contribution guidelines
- SECURITY.md with security policy and best practices
- CHANGELOG.md for version tracking
- LICENSE file (GPL-3.0-or-later)
- EditorConfig for consistent code formatting
- Documentation structure in `docs/` directory
- Placeholders for component directories (`admin/`, `site/`, `media/`)
- MokoStandards compliance:
- File header standards with copyright and SPDX identifiers
- Joomla coding standards configuration
- PHPStan static analysis setup
- Dependency security auditing
- Makefile targets for:
- Dependency management (install-deps, update-deps)
- Code validation (lint, phpcs, phpstan)
- Testing (test, test-coverage)
- Building (build, build-assets)
- Development (dev-install, watch-assets)
- Release management (release, bump-version)
- Utility commands (clean, version, help)
- Documentation index files with MokoStandards metadata
### Documentation
- Comprehensive README with:
- Quick start guide
- Prerequisites and installation
- Usage examples
- Project structure overview
- Makefile command reference
- Standards compliance information
- Detailed CONTRIBUTING guide with:
- Commit message conventions
- DCO sign-off requirements
- Development setup instructions
- Code review process
- Security policy with:
- Vulnerability reporting procedures
- Security best practices
- Code examples for secure development
- Template structure for Joomla 4.x and 5.x compatibility
## Version Guidelines
### Version Numbering
This template follows [Semantic Versioning](https://semver.org/):
- **MAJOR** version for incompatible changes (e.g., 2.0.0)
- **MINOR** version for new features (e.g., 1.1.0)
- **PATCH** version for bug fixes (e.g., 1.0.1)
### Change Categories
- **Added**: New features
- **Changed**: Changes to existing functionality
- **Deprecated**: Soon-to-be removed features
- **Removed**: Removed features
- **Fixed**: Bug fixes
- **Security**: Security fixes
## Release Notes
### [1.0.0] Release Notes
This is the initial release of the MokoJoomTOS plugin. It provides a production-ready Joomla system plugin for offline TOS access with:
- **Complete build system** via comprehensive Makefile
- **MokoStandards compliance** out of the box
- **Developer-friendly** workflow with automation
- **Security-focused** with built-in best practices
- **Well-documented** with clear usage instructions
This template is ready for use in creating new Joomla components that follow organizational coding standards and best practices.
### Migration Guide
**For New Projects**: Simply use this template to create your new component repository.
**For Existing Projects**: Review the Makefile, documentation structure, and standards compliance files to gradually adopt features that benefit your project.
- Initial plugin release
- Offline mode bypass for configured menu slug
- Auto-provisioning installer (creates article, menu type, menu item)
- Component-only rendering (`tmpl=component`)
- Language files for en-GB and en-US
- MokoStandards compliance
## Links
- [Repository](https://github.com/mokoconsulting-tech/MokoJoomTOS)
- [Issues](https://github.com/mokoconsulting-tech/MokoJoomTOS/issues)
- [Pull Requests](https://github.com/mokoconsulting-tech/MokoJoomTOS/pulls)
- [MokoStandards](https://github.com/mokoconsulting-tech/MokoCodingDefaults)
- [Repository](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS)
- [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/issues)
- [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases)
[Unreleased]: https://github.com/mokoconsulting-tech/MokoJoomTOS/compare/v03.09.00...HEAD
[03.09.00]: https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/tag/v03.09.00
[1.0.0]: https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/tag/v1.0.0
[Unreleased]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/compare/stable...dev
[04.01.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable
[04.00.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/compare/v03.09.00...stable
[03.09.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/v03.09.00
[1.0.0]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/v1.0.0
@@ -11,10 +11,16 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
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."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
; Warnings
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
; Errors
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
@@ -11,10 +11,16 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
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."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
; Warnings
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
; Errors
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
@@ -11,6 +11,9 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
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."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
@@ -11,6 +11,9 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
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."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
+4 -94
View File
@@ -9,106 +9,16 @@
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
/**
* MokoJoomTOS Offline Mode Bypass Plugin (Legacy)
* MokoJoomTOS Legacy Entry Point
*
* Allows configured menu items to remain accessible when the site
* is in offline mode.
* This file is required by Joomla's plugin loader (<filename plugin="mokojoomtos">)
* but is NOT executed under Joomla 5's DI container. The actual plugin logic lives
* in src/Extension/MokoJoomTOS.php, bootstrapped via services/provider.php.
*
* @since 1.0.0
*/
class PlgSystemMokojoomtos extends CMSPlugin
{
/**
* 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;
/**
* 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();
// Only proceed if site is offline
if (!$config->get('offline'))
{
return;
}
// Get the configured slugs (stored as array for multi-select)
$slugs = $this->params->get('tos_slug', []);
// Handle legacy single-value string format
if (is_string($slugs))
{
$slugs = array_filter([trim($slugs)]);
}
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->app->input;
$input->set('tmpl', 'component');
// Also set in GET superglobal to ensure recognition
$_GET['tmpl'] = 'component';
return;
}
}
}
}
+13 -1
View File
@@ -38,7 +38,7 @@
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>04.02.01</version>
<description>PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION</description>
<description>Allows Terms of Service to be accessible via menu slug when site is offline</description>
<namespace path="src">Joomla\Plugin\System\MokoJoomTOS</namespace>
@@ -71,6 +71,18 @@
description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC"
multiple="true"
/>
<field
name="include_children"
type="radio"
label="PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL"
description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="help_spacer"
+368 -364
View File
@@ -13,394 +13,398 @@ use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Installer\InstallerScript;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Table\Table;
/**
* Installation script for MokoJoomTOS Offline Plugin
* Installation script for MokoJoomTOS Plugin
*
* @since 1.0.0
*/
class PlgSystemMokojoomtosOfflineInstallerScript extends InstallerScript
class PlgSystemMokojoomtosInstallerScript extends InstallerScript
{
/**
* Minimum Joomla version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumJoomla = '4.0.0';
/**
* Minimum Joomla version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumJoomla = '4.0.0';
/**
* Minimum PHP version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumPhp = '7.4.0';
/**
* Minimum PHP version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumPhp = '7.4.0';
/**
* Extension type (used by parent class)
*
* @var string
* @since 1.0.0
*/
protected $extension = 'plg_system_mokojoomtos';
/**
* Extension type (used by parent class)
*
* @var string
* @since 1.0.0
*/
protected $extension = 'plg_system_mokojoomtos';
/**
* Function called before plugin installation/update/uninstall
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return boolean True on success
*
* @since 1.0.0
*/
public function preflight($type, $parent)
{
// Check minimum requirements
if (!parent::preflight($type, $parent)) {
return false;
}
/**
* Function called before plugin installation/update/uninstall
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return boolean True on success
*
* @since 1.0.0
*/
public function preflight($type, $parent)
{
if (!parent::preflight($type, $parent)) {
return false;
}
return true;
}
return true;
}
/**
* Function called after plugin installation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function install($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '</p>';
}
/**
* Function called after plugin installation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function install($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '</p>';
}
/**
* Function called after plugin update
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function update($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '</p>';
}
/**
* Function called after plugin update
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function update($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '</p>';
}
/**
* Function called after plugin uninstallation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function uninstall($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '</p>';
}
/**
* Function called after plugin uninstallation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function uninstall($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '</p>';
}
/**
* Function called after extension installation/update/discover_install
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function postflight($type, $parent)
{
if ($type === 'install' || $type === 'discover_install') {
// Create Terms of Service article and menu item
$this->createTermsOfServiceSetup();
echo '<div class="alert alert-success">';
echo '<h4>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '</h4>';
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '</p>';
echo '</div>';
}
}
/**
* Function called after extension installation/update/discover_install
*
* Fixes #89: enablePlugin() is now called unconditionally for both
* install and upgrade paths.
* Fixes #92: enablePlugin() is called on upgrade to re-enable if disabled.
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function postflight($type, $parent)
{
// Always enable the plugin on install or upgrade
$this->enablePlugin();
/**
* Create Terms of Service article and menu item
*
* @return void
*
* @since 1.0.0
*/
private function createTermsOfServiceSetup()
{
try {
$db = Factory::getDbo();
// Check if Terms of Service article already exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'));
$db->setQuery($query);
$articleId = $db->loadResult();
// Create article if it doesn't exist
if (!$articleId) {
$articleId = $this->createTermsArticle();
}
if ($articleId) {
// Check if menu item already exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
->where($db->quoteName('published') . ' >= 0');
$db->setQuery($query);
$menuId = $db->loadResult();
// Create menu item if it doesn't exist
if (!$menuId) {
$this->createTermsMenuItem($articleId);
}
}
} catch (\Exception $e) {
Log::add('Error creating Terms of Service setup: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
if ($type === 'install' || $type === 'discover_install') {
$this->createTermsOfServiceSetup();
/**
* Create Terms of Service article
*
* @return int|null Article ID or null on failure
*
* @since 1.0.0
*/
private function createTermsArticle()
{
try {
$db = Factory::getDbo();
echo '<div class="alert alert-success">';
echo '<h4>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '</h4>';
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '</p>';
echo '</div>';
}
}
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_content/tables');
$table = Table::getInstance('Content', 'Joomla\\Component\\Content\\Administrator\\Table\\');
/**
* Create Terms of Service article and menu item
*
* @return void
*
* @since 1.0.0
*/
private function createTermsOfServiceSetup()
{
try {
$db = Factory::getDbo();
if (!$table) {
Log::add('Failed to get Content table instance', Log::WARNING, 'jerror');
return null;
}
// Check if Terms of Service article already exists (with catid filter)
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
->where($db->quoteName('state') . ' >= 0');
$db->setQuery($query);
$articleId = $db->loadResult();
// Get Uncategorised category ID dynamically
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('uncategorised'))
->where($db->quoteName('published') . ' = 1');
$db->setQuery($query);
$catId = (int) $db->loadResult();
if (!$articleId) {
$articleId = $this->createTermsArticle();
}
if (!$catId) {
Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror');
return null;
}
if ($articleId) {
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
->where($db->quoteName('published') . ' >= 0');
$db->setQuery($query);
$menuId = $db->loadResult();
// Get current user ID for article ownership
$createdBy = Factory::getApplication()->getIdentity()->id ?: 0;
if (!$menuId) {
$this->createTermsMenuItem($articleId);
}
}
} catch (\Exception $e) {
Log::add('Error creating Terms of Service setup: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
$data = [
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'introtext' => '<h2>Terms of Service</h2><p>Welcome to our Terms of Service page.</p><p>This page will remain accessible even when the site is in offline/maintenance mode.</p>',
'fulltext' => '',
'state' => 1,
'catid' => $catId,
'created' => Factory::getDate()->toSql(),
'created_by' => $createdBy,
'language' => '*',
'access' => 1, // Public
];
// Bind data to table object first
if (!$table->bind($data)) {
Log::add('Failed to bind data to Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
// Check data validity
if (!$table->check()) {
Log::add('Content table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
// Save the table
if (!$table->store()) {
Log::add('Failed to store Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
echo '<p class="alert alert-info">✓ Created Terms of Service article</p>';
return $table->id;
} catch (\Exception $e) {
Log::add('Error creating Terms of Service article: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
return null;
}
/**
* Create Terms of Service article using Joomla 5 MVCFactory
*
* Fixes #90: Uses bootComponent()->getMVCFactory() instead of
* the removed Table::addIncludePath() / Table::getInstance().
* Fixes #94: Includes params, metadata, and attribs defaults.
*
* @return int|null Article ID or null on failure
*
* @since 1.0.0
*/
private function createTermsArticle()
{
try {
$db = Factory::getDbo();
$app = Factory::getApplication();
/**
* Create Terms of Service menu item
*
* @param int $articleId The article ID to link to
*
* @return void
*
* @since 1.0.0
*/
private function createTermsMenuItem($articleId)
{
try {
$db = Factory::getDbo();
// Check if "Legal" menu type exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu_types'))
->where($db->quoteName('menutype') . ' = ' . $db->quote('legal'));
$db->setQuery($query);
$legalMenuExists = $db->loadResult();
// Create "Legal" menu type if it doesn't exist
if (!$legalMenuExists) {
$this->createLegalMenuType();
}
// Get com_content component ID dynamically
$query = $db->getQuery(true)
->select('extension_id')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_content'));
$db->setQuery($query);
$componentId = (int) $db->loadResult();
// Get content table via MVCFactory (Joomla 4/5 compatible)
$table = $app->bootComponent('com_content')
->getMVCFactory()
->createTable('Article', 'Administrator');
if (!$componentId) {
Log::add('Could not determine com_content component ID', Log::WARNING, 'jerror');
return;
}
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/tables');
$table = Table::getInstance('Menu', 'Joomla\\Component\\Menus\\Administrator\\Table\\');
if (!$table) {
Log::add('Failed to get Menu table instance', Log::WARNING, 'jerror');
return;
}
$data = [
'menutype' => 'legal',
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'link' => 'index.php?option=com_content&view=article&id=' . $articleId,
'type' => 'component',
'published' => 1,
'parent_id' => 1,
'component_id' => $componentId,
'level' => 1,
'language' => '*',
'access' => 1, // Public
'params' => '{"show_title":"1","link_titles":"0","show_intro":"","info_block_position":"","show_category":"0","link_category":"0","show_parent_category":"0","link_parent_category":"0","show_author":"0","link_author":"0","show_create_date":"0","show_modify_date":"0","show_publish_date":"0","show_item_navigation":"0","show_icons":"0","show_print_icon":"0","show_email_icon":"0","show_hits":"0","show_noauth":"0","urls_position":"","menu-anchor_title":"","menu-anchor_css":"","menu_image":"","menu_text":1,"page_title":"","show_page_heading":0,"page_heading":"","pageclass_sfx":"","menu-meta_description":"","menu-meta_keywords":"","robots":"","secure":0}',
];
// Set the location in the menu tree
$table->setLocation($data['parent_id'], 'last-child');
// Bind data to table object
if (!$table->bind($data)) {
Log::add('Failed to bind data to Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
// Check data validity
if (!$table->check()) {
Log::add('Menu table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
// Save the menu item
if (!$table->store()) {
Log::add('Failed to store Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
echo '<p class="alert alert-info">✓ Created Terms of Service menu item in Legal menu with slug: terms-of-service</p>';
// Enable the plugin
$this->enablePlugin();
} catch (\Exception $e) {
Log::add('Error creating Terms of Service menu item: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
if (!$table) {
Log::add('Failed to get Content table instance', Log::WARNING, 'jerror');
return null;
}
/**
* Create Legal menu type
*
* @return void
*
* @since 1.0.0
*/
private function createLegalMenuType()
{
try {
$db = Factory::getDbo();
// Insert the Legal menu type
$query = $db->getQuery(true)
->insert($db->quoteName('#__menu_types'))
->columns($db->quoteName(['menutype', 'title', 'description']))
->values(
$db->quote('legal') . ', ' .
$db->quote('Legal') . ', ' .
$db->quote('Legal documents and policies menu')
);
$db->setQuery($query);
$db->execute();
echo '<p class="alert alert-info">✓ Created Legal menu type</p>';
} catch (\Exception $e) {
Log::add('Error creating Legal menu type: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
// Get Uncategorised category ID dynamically
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('uncategorised'))
->where($db->quoteName('published') . ' = 1');
$db->setQuery($query);
$catId = (int) $db->loadResult();
/**
* Enable the plugin after installation
*
* @return void
*
* @since 1.0.0
*/
private function enablePlugin()
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
$db->setQuery($query);
$db->execute();
echo '<p class="alert alert-success">✓ Plugin enabled automatically</p>';
} catch (\Exception $e) {
Log::add('Error enabling plugin: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
if (!$catId) {
Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror');
return null;
}
$createdBy = $app->getIdentity()->id ?: 0;
$data = [
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'introtext' => '<h2>Terms of Service</h2><p>Welcome to our Terms of Service page.</p><p>This page will remain accessible even when the site is in offline/maintenance mode.</p>',
'fulltext' => '',
'state' => 1,
'catid' => $catId,
'created' => Factory::getDate()->toSql(),
'created_by' => $createdBy,
'language' => '*',
'access' => 1,
'params' => '{}',
'metadata' => '{"robots":"","author":"","rights":""}',
'attribs' => '{}',
];
if (!$table->bind($data)) {
Log::add('Failed to bind data to Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
if (!$table->check()) {
Log::add('Content table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
if (!$table->store()) {
Log::add('Failed to store Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
echo '<p class="alert alert-info">Created Terms of Service article</p>';
return $table->id;
} catch (\Exception $e) {
Log::add('Error creating Terms of Service article: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
return null;
}
/**
* Create Terms of Service menu item using Joomla 5 MVCFactory
*
* Fixes #90: Uses bootComponent()->getMVCFactory() instead of
* the removed Table::addIncludePath() / Table::getInstance().
*
* @param int $articleId The article ID to link to
*
* @return void
*
* @since 1.0.0
*/
private function createTermsMenuItem($articleId)
{
try {
$db = Factory::getDbo();
$app = Factory::getApplication();
// Check if "Legal" menu type exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu_types'))
->where($db->quoteName('menutype') . ' = ' . $db->quote('legal'));
$db->setQuery($query);
$legalMenuExists = $db->loadResult();
if (!$legalMenuExists) {
$this->createLegalMenuType();
}
// Get com_content component ID dynamically
$query = $db->getQuery(true)
->select('extension_id')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_content'));
$db->setQuery($query);
$componentId = (int) $db->loadResult();
if (!$componentId) {
Log::add('Could not determine com_content component ID', Log::WARNING, 'jerror');
return;
}
// Get menu table via MVCFactory (Joomla 4/5 compatible)
$table = $app->bootComponent('com_menus')
->getMVCFactory()
->createTable('Menu', 'Administrator');
if (!$table) {
Log::add('Failed to get Menu table instance', Log::WARNING, 'jerror');
return;
}
$data = [
'menutype' => 'legal',
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'link' => 'index.php?option=com_content&view=article&id=' . $articleId,
'type' => 'component',
'published' => 1,
'parent_id' => 1,
'component_id' => $componentId,
'level' => 1,
'language' => '*',
'access' => 1,
'params' => '{"show_title":"1","link_titles":"0","show_intro":"","info_block_position":"","show_category":"0","link_category":"0","show_parent_category":"0","link_parent_category":"0","show_author":"0","link_author":"0","show_create_date":"0","show_modify_date":"0","show_publish_date":"0","show_item_navigation":"0","show_icons":"0","show_print_icon":"0","show_email_icon":"0","show_hits":"0","show_noauth":"0","urls_position":"","menu-anchor_title":"","menu-anchor_css":"","menu_image":"","menu_text":1,"page_title":"","show_page_heading":0,"page_heading":"","pageclass_sfx":"","menu-meta_description":"","menu-meta_keywords":"","robots":"","secure":0}',
];
$table->setLocation($data['parent_id'], 'last-child');
if (!$table->bind($data)) {
Log::add('Failed to bind data to Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
if (!$table->check()) {
Log::add('Menu table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
if (!$table->store()) {
Log::add('Failed to store Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
echo '<p class="alert alert-info">Created Terms of Service menu item in Legal menu</p>';
} catch (\Exception $e) {
Log::add('Error creating Terms of Service menu item: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Create Legal menu type
*
* @return void
*
* @since 1.0.0
*/
private function createLegalMenuType()
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->insert($db->quoteName('#__menu_types'))
->columns($db->quoteName(['menutype', 'title', 'description']))
->values(
$db->quote('legal') . ', ' .
$db->quote('Legal') . ', ' .
$db->quote('Legal documents and policies menu')
);
$db->setQuery($query);
$db->execute();
echo '<p class="alert alert-info">Created Legal menu type</p>';
} catch (\Exception $e) {
// Duplicate key is expected if race condition — safe to ignore
Log::add('Error creating Legal menu type: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Enable the plugin after installation
*
* @return void
*
* @since 1.0.0
*/
private function enablePlugin()
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
$db->setQuery($query);
$db->execute();
} catch (\Exception $e) {
Log::add('Error enabling plugin: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
}
+139 -25
View File
@@ -10,6 +10,7 @@ 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;
@@ -53,23 +54,21 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
* 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()
{
$app = $this->getApplication();
// Only process for site application
if (!$this->getApplication()->isClient('site'))
if (!$app->isClient('site'))
{
return;
}
// Get the global configuration
$config = $this->getApplication()->getConfig();
$config = $app->getConfig();
// Only proceed if site is offline
if (!$config->get('offline'))
@@ -77,7 +76,7 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
return;
}
// Get the configured slugs (stored as array for multi-select)
// Get the configured slugs — cast to array to handle stdClass from Registry (#96)
$slugs = $this->params->get('tos_slug', []);
// Handle legacy single-value string format
@@ -85,46 +84,161 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
{
$slugs = array_filter([trim($slugs)]);
}
else
{
$slugs = (array) $slugs;
}
if (empty($slugs))
{
return;
}
// Get the current URI path
$uri = Uri::getInstance();
$path = trim($uri->getPath(), '/');
$includeChildren = (int) $this->params->get('include_children', 1);
// Remove the base path if present
// Try SEF path matching first, then fall back to Itemid matching (#91)
if ($this->matchByPath($slugs, $config, $app, $includeChildren))
{
return;
}
$this->matchByItemId($slugs, $config, $app, $includeChildren);
}
/**
* Match the current request path against configured slugs (SEF mode)
*
* @param array $slugs Configured slug values
* @param object $config Joomla configuration object
* @param object $app Application instance
* @param integer $includeChildren Whether to include child menu items
*
* @return boolean True if a match was found and offline mode was bypassed
*
* @since 4.1.0
*/
private function matchByPath(array $slugs, $config, $app, int $includeChildren = 1): bool
{
$uri = Uri::getInstance();
$path = urldecode(trim($uri->getPath(), '/'));
// Remove the base path if present (subdirectory installs)
$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
// Skip if path is empty or just index.php (non-SEF)
if (empty($path) || $path === 'index.php')
{
return false;
}
foreach ($slugs as $slug)
{
$slug = trim($slug);
$slug = trim((string) $slug);
if (empty($slug))
{
continue;
}
if ($path === $slug || strpos($path, $slug . '/') === 0)
$isMatch = ($path === $slug)
|| ($includeChildren && strpos($path, $slug . '/') === 0);
if ($isMatch)
{
// 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;
$this->bypassOffline($config, $app);
return true;
}
}
return false;
}
/**
* Match the current request Itemid against menu items for configured slugs (non-SEF fallback)
*
* When SEF URLs are disabled, the path is just index.php so we match by
* checking if the requested Itemid belongs to a menu item whose path
* matches a configured slug.
*
* @param array $slugs Configured slug values
* @param object $config Joomla configuration object
* @param object $app Application instance
* @param integer $includeChildren Whether to include child menu items
*
* @return boolean True if a match was found and offline mode was bypassed
*
* @since 4.1.0
*/
private function matchByItemId(array $slugs, $config, $app, int $includeChildren = 1): bool
{
$itemId = (int) $app->input->getInt('Itemid', 0);
if (!$itemId)
{
return false;
}
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('path'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('id') . ' = ' . $itemId)
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('client_id') . ' = 0');
$db->setQuery($query);
$menuPath = $db->loadResult();
if (!$menuPath)
{
return false;
}
$menuPath = trim($menuPath, '/');
foreach ($slugs as $slug)
{
$slug = trim((string) $slug);
if (empty($slug))
{
continue;
}
$isMatch = ($menuPath === $slug)
|| ($includeChildren && strpos($menuPath, $slug . '/') === 0);
if ($isMatch)
{
$this->bypassOffline($config, $app);
return true;
}
}
}
catch (\Exception $e)
{
// Silently fail — do not bypass offline mode on error
}
return false;
}
/**
* Bypass offline mode and set component-only view for this request
*
* @param object $config Joomla configuration object
* @param object $app Application instance
*
* @return void
*
* @since 4.1.0
*/
private function bypassOffline($config, $app): void
{
$config->set('offline', 0);
$app->input->set('tmpl', 'component');
}
}
+19 -1
View File
@@ -42,6 +42,24 @@ class MenuslugField extends ListField
{
$options = parent::getOptions();
// Warn if SEF URLs are disabled (#97)
try
{
$sef = Factory::getApplication()->get('sef', true);
if (!$sef)
{
$options[] = (object) [
'value' => '',
'text' => Text::_('PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING'),
'disabled' => true
];
}
}
catch (\Exception $e)
{
// Ignore — field still works without the warning
}
try
{
$db = Factory::getDbo();
@@ -70,7 +88,7 @@ class MenuslugField extends ListField
$options[] = (object) [
'value' => '',
'text' => '──────────────',
'disable' => true
'disabled' => true
];
}
$lastMenuType = $item->menutype;
+4 -4
View File
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 04.02.01
VERSION: 04.01.00
-->
<updates>
@@ -10,13 +10,13 @@
<description>System - Moko Terms of Service update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.02.01</version>
<version>04.01.00</version>
<client>site</client>
<folder>system</folder>
<tags><tag>development</tag></tags>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/dev</infourl>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/dev/plg_system_mokojoomtos-04.02.01.zip</downloadurl>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.01.00.zip</downloadurl>
</downloads>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>