Compare commits

...

10 Commits

Author SHA1 Message Date
Jonathan Miller b170894228 chore: bump version to 02.34.11-dev, update updates.xml
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:37:21 -05:00
Jonathan Miller 082fa0798c feat: add auto-category menu module for knowledge base sections (#184)
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 21s
New module mod_mokowaas_categories auto-discovers article categories
from a configurable root and renders them as a collapsible sidebar tree.
Supports configurable depth, article counts, empty category filtering,
and ACL-aware access. Matches existing MokoWaaS sidebar styling.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:34:17 -05:00
Jonathan Miller d1ee2ef3f4 fix: API controller execute() signatures compatible with BaseController (#183)
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
All API controllers now accept the $task parameter required by
Joomla\CMS\MVC\Controller\BaseController::execute($task).

Closes #183

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:25:18 -05:00
Jonathan Miller 7f9b59a36d chore: update updates.xml for 02.34.10-dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:21:50 -05:00
Jonathan Miller 79047e37b5 chore: bump version to 02.34.10-dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 38s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:08:03 -05:00
Jonathan Miller 3d5f9346c6 chore: bump version to 02.34.09-dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:04:35 -05:00
Jonathan Miller 93c82a9cee refactor: strip core plugin to heartbeat-only config
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Remove branding, security hardening, demo mode, content sync, and API
actions from the core plugin. Config now shows only the heartbeat token.
Extension class reduced from 4226 to 2051 lines. Deleted 5 Field classes,
3 Service classes, 2 form XMLs, and media assets. Core retains health
checks, Grafana provisioning, site aliases, and extension cascade.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 05:47:13 -05:00
Jonathan Miller 384b8824c6 refactor: remove tpl_mokoonyx submodule
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Template is managed independently; submodule reference no longer needed.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 22:18:47 -05:00
Jonathan Miller e01791ae68 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 22:13:50 -05:00
Jonathan Miller e42d6e7596 fix: hardcode branding values in overrides, adjust menu indent, add reset download keys
Hardcode {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders
to literal values in all language override .ini files. Adjust admin menu
indent (2rem parent, 2.5rem child). Add one-shot reset download keys
toggle to DevTools plugin.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 22:10:28 -05:00
84 changed files with 750 additions and 5684 deletions
-4
View File
@@ -1,4 +0,0 @@
[submodule "src/packages/tpl_mokoonyx"]
path = src/packages/tpl_mokoonyx
url = https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
branch = main
+1 -1
View File
@@ -9,7 +9,7 @@
<display-name>Package - MokoWaaS</display-name>
<org>MokoConsulting</org>
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
<version>02.34.08</version>
<version>02.34.11</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 02.34.08
# VERSION: 02.34.11
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+1 -1
View File
@@ -14,7 +14,7 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./CHANGELOG.md
VERSION: 02.34.08
VERSION: 02.34.11
BRIEF: Version history using `Keep a Changelog`
-->
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.08
VERSION: 02.34.11
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand
INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand
VERSION: 02.34.08
VERSION: 02.34.11
PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand
-->
+1 -1
View File
@@ -15,7 +15,7 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./LICENSE.md
VERSION: 02.34.08
VERSION: 02.34.11
BRIEF: Project license (GPL-3.0-or-later)
-->
GNU GENERAL PUBLIC LICENSE
+1 -1
View File
@@ -9,7 +9,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.34.08
VERSION: 02.34.11
PATH: /README.md
BRIEF: MokoWaaS platform plugin for Joomla
-->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL]
PATH: /SECURITY.md
VERSION: 02.34.08
VERSION: 02.34.11
BRIEF: Security vulnerability reporting and handling policy
-->
+2 -2
View File
@@ -11,13 +11,13 @@
INGROUP: MokoWaaS.Build
REPO: https://github.com/mokoconsulting-tech/mokowaas
FILE: build-guide.md
VERSION: 02.34.08
VERSION: 02.34.11
PATH: /docs/guides/
BRIEF: Build and packaging guide for the MokoWaaS system plugin
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
-->
# MokoWaaS Build Guide (VERSION: 02.34.08)
# MokoWaaS Build Guide (VERSION: 02.34.11)
## 1. Purpose
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.08
VERSION: 02.34.11
PATH: /docs/guides/configuration-guide.md
BRIEF: Configuration guide for the MokoWaaS system plugin
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
-->
# MokoWaaS Configuration Guide (VERSION: 02.34.08)
# MokoWaaS Configuration Guide (VERSION: 02.34.11)
## 1. Objective
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.08
VERSION: 02.34.11
PATH: /docs/guides/installation-guide.md
BRIEF: Installation guide for the MokoWaaS system plugin
NOTE: First document in the guide set
-->
# MokoWaaS Installation Guide (VERSION: 02.34.08)
# MokoWaaS Installation Guide (VERSION: 02.34.11)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.08
VERSION: 02.34.11
PATH: /docs/guides/operations-guide.md
BRIEF: Operational guide for administering and managing the MokoWaaS system plugin
NOTE: Defines lifecycle, responsibilities, and operational behaviors
-->
# MokoWaaS Operations Guide (VERSION: 02.34.08)
# MokoWaaS Operations Guide (VERSION: 02.34.11)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.08
VERSION: 02.34.11
PATH: /docs/guides/rollback-and-recovery-guide.md
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
NOTE: Completes the core guide set for WaaS plugin governance
-->
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.34.08)
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.34.11)
## Introduction
+2 -2
View File
@@ -7,13 +7,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.08
VERSION: 02.34.11
PATH: /docs/guides/testing-guide.md
BRIEF: Testing guide for MokoWaaS v02.01.08
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
-->
# MokoWaaS Testing Guide (VERSION: 02.34.08)
# MokoWaaS Testing Guide (VERSION: 02.34.11)
## 1. Prerequisites
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.08
VERSION: 02.34.11
PATH: /docs/guides/troubleshooting-guide.md
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin
NOTE: Designed for administrators and WaaS operations teams
-->
# MokoWaaS Troubleshooting Guide (VERSION: 02.34.08)
# MokoWaaS Troubleshooting Guide (VERSION: 02.34.11)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.08
VERSION: 02.34.11
PATH: /docs/guides/upgrade-and-versioning-guide.md
BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin
NOTE: Defines release flow, version rules, and upgrade validation
-->
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.34.08)
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.34.11)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.08
VERSION: 02.34.11
PATH: /docs/index.md
BRIEF: Master index of all documentation for the MokoWaaS plugin
NOTE: Automatically maintained index for all guide canvases
-->
# MokoWaaS Documentation Index (VERSION: 02.34.08)
# MokoWaaS Documentation Index (VERSION: 02.34.11)
## Introduction
+2 -2
View File
@@ -11,12 +11,12 @@
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: /docs/plugin-basic.md
VERSION: 02.34.08
VERSION: 02.34.11
BRIEF: Baseline documentation for the MokoWaaS system plugin
NOTE: Foundational reference for internal and external stakeholders
-->
# MokoWaaS Plugin Overview (VERSION: 02.34.08)
# MokoWaaS Plugin Overview (VERSION: 02.34.11)
## Introduction
+1 -1
View File
@@ -10,7 +10,7 @@ DEFGROUP: MokoWaaS.Documentation
INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoWaaS
PATH: /docs/update-server.md
VERSION: 02.34.08
VERSION: 02.34.11
BRIEF: How this extension's Joomla update server file (update.xml) is managed
-->
@@ -29,7 +29,7 @@ class CacheController extends BaseController
*
* @since 1.0.0
*/
public function execute(): void
public function execute($task = 'cache'): void
{
$app = Factory::getApplication();
@@ -42,7 +42,7 @@ class InstallController extends BaseController
*
* @since 02.21.00
*/
public function execute(): void
public function execute($task = 'install'): void
{
$app = Factory::getApplication();
@@ -104,7 +104,7 @@ class PluginsController extends BaseController
*
* @return void
*/
public function execute(): void
public function execute($task = 'plugins'): void
{
$app = Factory::getApplication();
$user = $app->getIdentity();
@@ -35,7 +35,7 @@ class ResetController extends BaseController
*
* @since 02.21.00
*/
public function execute(): void
public function execute($task = 'reset'): void
{
$app = Factory::getApplication();
@@ -68,7 +68,7 @@ class SnapshotController extends BaseController
*
* @since 02.21.00
*/
public function execute(): void
public function execute($task = 'snapshot'): void
{
$app = Factory::getApplication();
@@ -26,7 +26,7 @@ use Joomla\Registry\Registry;
*/
class SyncController extends BaseController
{
public function execute(): void
public function execute($task = 'sync'): void
{
$app = Factory::getApplication();
@@ -24,7 +24,7 @@ use Joomla\CMS\MVC\Controller\BaseController;
*/
class SyncReceiveController extends BaseController
{
public function execute(): void
public function execute($task = 'syncReceive'): void
{
$app = Factory::getApplication();
@@ -29,7 +29,7 @@ class UpdateController extends BaseController
*
* @since 1.0.0
*/
public function execute(): void
public function execute($task = 'update'): void
{
$app = Factory::getApplication();
+2 -2
View File
@@ -8,7 +8,7 @@
DEFGROUP: Joomla.Component
INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.34.00
VERSION: 02.34.11
PATH: /mokowaas.xml
BRIEF: Component manifest for MokoWaaS admin dashboard and REST API
-->
@@ -20,7 +20,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>MokoWaaS admin dashboard and REST API. Provides a control panel for managing MokoWaaS feature plugins, site health monitoring, and remote management endpoints.</description>
<namespace path="src">Moko\Component\MokoWaaS</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>MOD_MOKOWAAS_CACHE_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCache</namespace>
@@ -0,0 +1,24 @@
MOD_MOKOWAAS_CATEGORIES="MokoWaaS Categories"
MOD_MOKOWAAS_CATEGORIES_DESC="Auto-discovers article categories and renders them as a collapsible tree menu. Ideal for knowledge base and help sections."
MOD_MOKOWAAS_CATEGORIES_ROOT_LABEL="Root Category"
MOD_MOKOWAAS_CATEGORIES_ROOT_DESC="Select a parent category. Only its children (and their subcategories) will be displayed. Leave as All to show the entire category tree."
MOD_MOKOWAAS_CATEGORIES_ALL_CATEGORIES="- All Categories -"
MOD_MOKOWAAS_CATEGORIES_DEPTH_LABEL="Maximum Depth"
MOD_MOKOWAAS_CATEGORIES_DEPTH_DESC="How many levels deep to display. 1 shows only top-level categories, 2 adds one level of subcategories, etc."
MOD_MOKOWAAS_CATEGORIES_COUNT_LABEL="Show Article Count"
MOD_MOKOWAAS_CATEGORIES_COUNT_DESC="Display the number of published articles next to each category name."
MOD_MOKOWAAS_CATEGORIES_EMPTY_LABEL="Show Empty Categories"
MOD_MOKOWAAS_CATEGORIES_EMPTY_DESC="Display categories that have no published articles. Only applies when Show Article Count is enabled."
MOD_MOKOWAAS_CATEGORIES_MENUITEM_LABEL="Target Menu Item"
MOD_MOKOWAAS_CATEGORIES_MENUITEM_DESC="The menu item to use as the base for category links. This sets the Itemid parameter for proper template and menu highlighting."
MOD_MOKOWAAS_CATEGORIES_ORDER_LABEL="Category Ordering"
MOD_MOKOWAAS_CATEGORIES_ORDER_DESC="How to sort categories within each level."
MOD_MOKOWAAS_CATEGORIES_ORDER_TREE="Tree Order (default)"
MOD_MOKOWAAS_CATEGORIES_ORDER_TITLE="Alphabetical"
MOD_MOKOWAAS_CATEGORIES_ORDER_CREATED="Date Created"
@@ -0,0 +1,2 @@
MOD_MOKOWAAS_CATEGORIES="MokoWaaS Categories"
MOD_MOKOWAAS_CATEGORIES_DESC="Auto-discovers article categories and renders them as a collapsible tree menu. Ideal for knowledge base and help sections."
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="module" client="administrator" method="upgrade">
<name>mod_mokowaas_categories</name>
<author>Moko Consulting</author>
<creationDate>2026-06-06</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.11</version>
<description>MOD_MOKOWAAS_CATEGORIES_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCategories</namespace>
<files>
<folder module="mod_mokowaas_categories">services</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/mod_mokowaas_categories.ini</language>
<language tag="en-GB">en-GB/mod_mokowaas_categories.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field name="root_category" type="category"
extension="com_content"
label="MOD_MOKOWAAS_CATEGORIES_ROOT_LABEL"
description="MOD_MOKOWAAS_CATEGORIES_ROOT_DESC"
default="0"
show_root="true"
>
<option value="0">MOD_MOKOWAAS_CATEGORIES_ALL_CATEGORIES</option>
</field>
<field name="max_depth" type="number"
label="MOD_MOKOWAAS_CATEGORIES_DEPTH_LABEL"
description="MOD_MOKOWAAS_CATEGORIES_DEPTH_DESC"
default="3"
min="1"
max="10"
/>
<field name="show_article_count" type="radio"
label="MOD_MOKOWAAS_CATEGORIES_COUNT_LABEL"
description="MOD_MOKOWAAS_CATEGORIES_COUNT_DESC"
default="1"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="show_empty" type="radio"
label="MOD_MOKOWAAS_CATEGORIES_EMPTY_LABEL"
description="MOD_MOKOWAAS_CATEGORIES_EMPTY_DESC"
default="0"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="menu_item_id" type="menuitem"
label="MOD_MOKOWAAS_CATEGORIES_MENUITEM_LABEL"
description="MOD_MOKOWAAS_CATEGORIES_MENUITEM_DESC"
default=""
/>
<field name="ordering" type="list"
label="MOD_MOKOWAAS_CATEGORIES_ORDER_LABEL"
description="MOD_MOKOWAAS_CATEGORIES_ORDER_DESC"
default="lft">
<option value="lft">MOD_MOKOWAAS_CATEGORIES_ORDER_TREE</option>
<option value="title">MOD_MOKOWAAS_CATEGORIES_ORDER_TITLE</option>
<option value="created_time">MOD_MOKOWAAS_CATEGORIES_ORDER_CREATED</option>
</field>
</fieldset>
</fields>
</config>
</extension>
@@ -0,0 +1,25 @@
<?php
/**
* @package MokoWaaS
* @subpackage mod_mokowaas_categories
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\Service\Provider\HelperFactory;
use Joomla\CMS\Extension\Service\Provider\Module;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->registerServiceProvider(new ModuleDispatcherFactory('\\Moko\\Module\\MokoWaaSCategories'));
$container->registerServiceProvider(new HelperFactory('\\Moko\\Module\\MokoWaaSCategories\\Administrator\\Helper'));
$container->registerServiceProvider(new Module());
}
};
@@ -0,0 +1,32 @@
<?php
/**
* @package MokoWaaS
* @subpackage mod_mokowaas_categories
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Module\MokoWaaSCategories\Administrator\Dispatcher;
defined('_JEXEC') or die;
use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
use Joomla\CMS\Helper\HelperFactoryAwareInterface;
use Joomla\CMS\Helper\HelperFactoryAwareTrait;
class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareInterface
{
use HelperFactoryAwareTrait;
protected function getLayoutData()
{
$data = parent::getLayoutData();
$params = $data['params'];
$helper = $this->getHelperFactory()->getHelper('CategoriesHelper');
$data['categories'] = $helper->getCategories($params);
return $data;
}
}
@@ -0,0 +1,148 @@
<?php
/**
* @package MokoWaaS
* @subpackage mod_mokowaas_categories
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Module\MokoWaaSCategories\Administrator\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
use Joomla\Registry\Registry;
class CategoriesHelper
{
/**
* Get category tree from a root category with configurable depth.
*
* @param Registry $params Module parameters
*
* @return array Nested array of category objects
*/
public function getCategories(Registry $params): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$rootId = (int) $params->get('root_category', 0);
$maxDepth = (int) $params->get('max_depth', 3);
$showEmpty = (int) $params->get('show_empty', 0);
$showCount = (int) $params->get('show_article_count', 1);
$ordering = $params->get('ordering', 'lft');
$user = Factory::getApplication()->getIdentity();
$accessLevels = $user->getAuthorisedViewLevels();
// Build base query
$query = $db->getQuery(true)
->select([
$db->quoteName('c.id'),
$db->quoteName('c.title'),
$db->quoteName('c.alias'),
$db->quoteName('c.parent_id'),
$db->quoteName('c.level'),
$db->quoteName('c.lft'),
$db->quoteName('c.rgt'),
$db->quoteName('c.description'),
])
->from($db->quoteName('#__categories', 'c'))
->where($db->quoteName('c.extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('c.published') . ' = 1')
->whereIn($db->quoteName('c.access'), $accessLevels);
// If a root category is set, constrain to its subtree
if ($rootId > 0)
{
$rootQuery = $db->getQuery(true)
->select([$db->quoteName('lft'), $db->quoteName('rgt'), $db->quoteName('level')])
->from($db->quoteName('#__categories'))
->where($db->quoteName('id') . ' = ' . $rootId);
$db->setQuery($rootQuery);
$root = $db->loadObject();
if (!$root)
{
return [];
}
$query->where($db->quoteName('c.lft') . ' > ' . (int) $root->lft)
->where($db->quoteName('c.rgt') . ' < ' . (int) $root->rgt)
->where($db->quoteName('c.level') . ' <= ' . ((int) $root->level + $maxDepth));
}
else
{
// No root — show from level 1 (skip the virtual root)
$query->where($db->quoteName('c.level') . ' >= 1')
->where($db->quoteName('c.level') . ' <= ' . $maxDepth);
}
// Article count subquery
if ($showCount)
{
$countSub = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__content', 'a'))
->where($db->quoteName('a.catid') . ' = ' . $db->quoteName('c.id'))
->where($db->quoteName('a.state') . ' = 1');
$query->select('(' . $countSub . ') AS ' . $db->quoteName('article_count'));
}
// Ordering
$validOrders = ['lft', 'title', 'created_time'];
$orderCol = \in_array($ordering, $validOrders, true) ? $ordering : 'lft';
$query->order($db->quoteName('c.' . $orderCol) . ' ASC');
$db->setQuery($query);
$categories = $db->loadObjectList() ?: [];
// Filter empty categories if configured
if (!$showEmpty && $showCount)
{
$categories = array_filter($categories, function ($cat) {
return (int) $cat->article_count > 0;
});
$categories = array_values($categories);
}
// Build nested tree
return $this->buildTree($categories, $rootId);
}
/**
* Build a nested tree from a flat list of categories.
*
* @param array $categories Flat list of category objects
* @param int $rootId Root category ID (0 for all)
*
* @return array Nested array with 'children' key on each node
*/
private function buildTree(array $categories, int $rootId): array
{
$map = [];
$tree = [];
foreach ($categories as $cat)
{
$cat->children = [];
$map[$cat->id] = $cat;
}
foreach ($categories as $cat)
{
$parentId = (int) $cat->parent_id;
if (isset($map[$parentId]))
{
$map[$parentId]->children[] = $cat;
}
else
{
$tree[] = $cat;
}
}
return $tree;
}
}
@@ -0,0 +1,138 @@
<?php
/**
* @package MokoWaaS
* @subpackage mod_mokowaas_categories
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
$categories = $categories ?? [];
$showCount = (int) ($params->get('show_article_count', 1));
$menuItemId = (int) $params->get('menu_item_id', 0);
if (empty($categories))
{
return;
}
// Detect active category from current URL
$app = \Joomla\CMS\Factory::getApplication();
$activeCatId = (int) $app->input->getInt('id', 0);
$currentView = $app->input->getCmd('view', '');
$isCatView = \in_array($currentView, ['category', 'categories'], true);
/**
* Build the link for a category.
*/
$buildLink = function (object $cat) use ($menuItemId): string {
$link = 'index.php?option=com_content&view=category&id=' . (int) $cat->id;
if ($menuItemId)
{
$link .= '&Itemid=' . $menuItemId;
}
return Route::_($link);
};
/**
* Check if a category or any of its descendants is the active category.
*/
$isActiveOrAncestor = function (object $cat) use ($activeCatId, $isCatView, &$isActiveOrAncestor): bool {
if (!$isCatView || !$activeCatId)
{
return false;
}
if ((int) $cat->id === $activeCatId)
{
return true;
}
foreach ($cat->children as $child)
{
if ($isActiveOrAncestor($child))
{
return true;
}
}
return false;
};
/**
* Render a category list recursively.
*/
$renderTree = function (array $categories, int $depth = 1) use (
&$renderTree, $buildLink, $isActiveOrAncestor, $showCount, $activeCatId, $isCatView
): void {
foreach ($categories as $cat):
$hasChildren = !empty($cat->children);
$isActive = $isCatView && (int) $cat->id === $activeCatId;
$isAncestor = $hasChildren && $isActiveOrAncestor($cat);
$liClass = 'item mokowaas-cat-item mokowaas-cat-level-' . $depth;
if ($isActive)
{
$liClass .= ' mm-active';
}
if ($hasChildren)
{
$liClass .= ' parent';
}
$aClass = ($hasChildren ? 'has-arrow' : 'no-dropdown');
if ($isActive)
{
$aClass .= ' mm-active';
}
$collapseClass = 'collapse-cat-level-' . ($depth + 1) . ' mm-collapse';
if ($isAncestor || $isActive)
{
$collapseClass .= ' mm-show';
}
$count = isset($cat->article_count) ? (int) $cat->article_count : 0;
?>
<li class="<?php echo $liClass; ?>">
<a class="<?php echo $aClass; ?>"
href="<?php echo $hasChildren ? '#' : $buildLink($cat); ?>"
<?php echo $isActive ? ' aria-current="page"' : ''; ?>>
<span class="icon-folder" aria-hidden="true"
style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
<span class="sidebar-item-title"><?php echo htmlspecialchars($cat->title); ?></span>
<?php if ($showCount): ?>
<span class="badge bg-secondary ms-auto"><?php echo $count; ?></span>
<?php endif; ?>
</a>
<?php if ($hasChildren): ?>
<ul class="<?php echo $collapseClass; ?>" style="padding-inline-start:0.75rem;">
<?php $renderTree($cat->children, $depth + 1); ?>
</ul>
<?php endif; ?>
</li>
<?php endforeach;
};
?>
<style>
.sidebar-wrapper .mokowaas-cat-item > a { padding-inline-start: 2rem; }
.sidebar-wrapper .mokowaas-cat-item > a .badge { font-size: 0.65em; padding: 0.15em 0.45em; }
.sidebar-wrapper .mokowaas-cat-level-2 > a { padding-inline-start: 2.5rem; }
.sidebar-wrapper .mokowaas-cat-level-3 > a { padding-inline-start: 3rem; }
.sidebar-wrapper .mokowaas-cat-level-4 > a { padding-inline-start: 3.5rem; }
</style>
<ul class="nav flex-column">
<?php $renderTree($categories); ?>
</ul>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>MOD_MOKOWAAS_CPANEL_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCpanel</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>MokoWaaS admin sidebar menu — renders a dedicated MokoWaaS section in the admin menu before Joomla's default menu.</description>
<namespace path="src">Moko\Module\MokoWaaSMenu</namespace>
@@ -112,8 +112,8 @@ $topCollapse = 'collapse-level-1 mm-collapse' . ($anyMokoActive ? ' mm-show' :
<style>
.sidebar-wrapper .item-level-1 > a { padding-inline-start: 1.5rem; }
.sidebar-wrapper .mokowaas-menu-item > a { padding-inline-start: 1rem; }
.sidebar-wrapper .mokowaas-menu-child > a { padding-inline-start: 1.5rem; }
.sidebar-wrapper .mokowaas-menu-item > a { padding-inline-start: 2rem; }
.sidebar-wrapper .mokowaas-menu-child > a { padding-inline-start: 2.5rem; }
</style>
<ul class="nav flex-column main-nav">
File diff suppressed because it is too large Load Diff
@@ -1,72 +0,0 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.08
* PATH: /src/Field/AllowedIpsField.php
* BRIEF: Custom form field that displays the current IP whitelist
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
class AllowedIpsField extends FormField
{
protected $type = 'AllowedIps';
protected function getInput()
{
$config = Factory::getApplication()->getConfig();
$allowedRaw = $config->get('mokowaas_allowed_ips', '');
$currentIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
if (empty($allowedRaw))
{
$status = '<span class="badge bg-danger">Not configured</span>';
$ipList = '<em>No IPs set — emergency access is blocked.</em>';
}
else
{
$ips = array_map('trim', explode(',', $allowedRaw));
$status = '<span class="badge bg-success">'
. count($ips) . ' IP(s) configured</span>';
$ipItems = [];
foreach ($ips as $ip)
{
$match = ($ip === $currentIp)
? ' <span class="badge bg-info">your IP</span>'
: '';
$ipItems[] = '<code>' . htmlspecialchars($ip)
. '</code>' . $match;
}
$ipList = implode(', ', $ipItems);
}
$yourIp = '<code>' . htmlspecialchars($currentIp) . '</code>';
return '<div class="alert alert-info mb-0">'
. '<strong>IP Whitelist:</strong> ' . $status . '<br>'
. '<strong>Allowed IPs:</strong> ' . $ipList . '<br>'
. '<strong>Your current IP:</strong> ' . $yourIp . '<br>'
. '<small class="text-muted">Set <code>public '
. '$mokowaas_allowed_ips = \'1.2.3.4,5.6.7.8\';</code>'
. ' in configuration.php to change.</small>'
. '</div>';
}
protected function getLabel()
{
return '';
}
}
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.08
* VERSION: 02.34.11
* PATH: /src/Field/CopyableTokenField.php
* BRIEF: Read-only token field with a copy-to-clipboard button
*/
@@ -1,40 +0,0 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.08
* PATH: /src/Field/CurrentIpField.php
* BRIEF: Read-only field that displays the current user's IP address
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
class CurrentIpField extends FormField
{
protected $type = 'CurrentIp';
protected function getInput()
{
$currentIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
return '<div class="alert alert-info mb-0 py-2">'
. '<strong>Your current IP:</strong> '
. '<code>' . htmlspecialchars($currentIp) . '</code> '
. '<small class="text-muted">&mdash; add this to the table below to keep your session alive.</small>'
. '</div>';
}
protected function getLabel()
{
return '';
}
}
@@ -1,237 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.08
* PATH: /src/Field/DemoTaskInfoField.php
* BRIEF: Read-only field showing scheduled task info with link to manage it
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\Router\Route;
/**
* Displays the demo reset scheduled task status: schedule, next run,
* last run, and a direct link to edit the task in Joomla's Scheduler.
*
* @since 02.29.00
*/
class DemoTaskInfoField extends FormField
{
protected $type = 'DemoTaskInfo';
protected function getInput()
{
// Query the scheduled task — if it exists and is enabled, demo mode is on
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset'));
$db->setQuery($query);
$task = $db->loadAssoc();
}
catch (\Throwable $e)
{
$task = null;
}
$newTaskLink = Route::_('index.php?option=com_scheduler&task=task.add');
if (!$task)
{
return '<div class="alert alert-info mb-0 py-2">'
. 'No demo reset task configured. '
. '<a href="' . $newTaskLink . '" class="alert-link">Create a Scheduled Task</a> '
. 'and select <strong>MokoWaaS Demo Reset</strong> to enable demo mode.</div>';
}
$taskId = (int) $task['id'];
$state = (int) $task['state'];
$siteTimezone = Factory::getApplication()->get('offset', 'UTC');
// Parse schedule from execution_rules
$rules = json_decode($task['execution_rules'] ?? '{}', true);
$ruleType = $rules['rule-type'] ?? '';
switch ($ruleType)
{
case 'cron-expression':
$schedule = $rules['cron-expression'] ?? '';
$friendlySchedule = $this->friendlySchedule($schedule);
break;
case 'interval-minutes':
$mins = (int) ($rules['interval-minutes'] ?? 0);
if ($mins >= 1440 && $mins % 1440 === 0)
{
$days = $mins / 1440;
$schedule = 'Every ' . $days . ' day' . ($days > 1 ? 's' : '');
}
elseif ($mins >= 60 && $mins % 60 === 0)
{
$hours = $mins / 60;
$schedule = 'Every ' . $hours . ' hour' . ($hours > 1 ? 's' : '');
}
else
{
$schedule = 'Every ' . $mins . ' minute' . ($mins !== 1 ? 's' : '');
}
$friendlySchedule = $schedule;
break;
case 'interval-hours':
$hours = (int) ($rules['interval-hours'] ?? 0);
$schedule = 'Every ' . $hours . ' hour' . ($hours !== 1 ? 's' : '');
$friendlySchedule = $schedule;
break;
case 'interval-days':
$days = (int) ($rules['interval-days'] ?? 0);
$schedule = 'Every ' . $days . ' day' . ($days !== 1 ? 's' : '');
$friendlySchedule = $schedule;
break;
default:
$schedule = $ruleType ?: 'Not set';
$friendlySchedule = 'Custom';
}
// Next execution
$nextExec = $task['next_execution'] ?? '';
$nextFormatted = 'Not scheduled';
$nextBadge = '';
if (!empty($nextExec) && $nextExec !== '0000-00-00 00:00:00')
{
try
{
$dt = new \DateTime($nextExec, new \DateTimeZone('UTC'));
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$nextFormatted = $dt->format('M j, Y g:i A T');
}
catch (\Throwable $e)
{
$nextFormatted = $nextExec;
}
$diff = strtotime($nextExec . ' UTC') - time();
if ($diff <= 0)
{
$nextBadge = '<span class="badge bg-warning text-dark">DUE</span>';
}
elseif ($diff < 3600)
{
$nextBadge = '<span class="badge bg-info">in ' . (int) ceil($diff / 60) . ' min</span>';
}
elseif ($diff < 86400)
{
$nextBadge = '<span class="badge bg-info">in ' . round($diff / 3600, 1) . 'h</span>';
}
else
{
$nextBadge = '<span class="badge bg-secondary">in ' . round($diff / 86400, 1) . 'd</span>';
}
}
// Last execution
$lastExec = $task['last_execution'] ?? '';
$lastFormatted = 'Never';
if (!empty($lastExec) && $lastExec !== '0000-00-00 00:00:00')
{
try
{
$dt = new \DateTime($lastExec, new \DateTimeZone('UTC'));
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$lastFormatted = $dt->format('M j, Y g:i A T');
}
catch (\Throwable $e)
{
$lastFormatted = $lastExec;
}
}
// State badge
$stateBadge = $state === 1
? '<span class="badge bg-success">Enabled</span>'
: '<span class="badge bg-danger">Disabled</span>';
// Link to edit the task
$editLink = Route::_('index.php?option=com_scheduler&task=task.edit&id=' . $taskId);
// Task params — default to On when keys are missing (matches form defaults)
$taskParams = json_decode($task['params'] ?? '{}', true) ?: [];
$bannerOn = !isset($taskParams['banner_enabled']) || (int) $taskParams['banner_enabled'] === 1;
$mediaOn = !isset($taskParams['include_media']) || (int) $taskParams['include_media'] === 1;
$countdownOn = !isset($taskParams['show_countdown']) || (int) $taskParams['show_countdown'] === 1;
// Check if snapshot exists
$snapshotExists = is_dir(JPATH_ROOT . '/mokowaas-snapshots/default');
// Build info card
return '<div class="card card-body bg-light py-2 px-3 mb-0">'
. '<table class="table table-sm table-borderless mb-1" style="max-width:550px">'
. '<tr><td class="text-muted" style="width:130px">Status</td><td>' . $stateBadge . '</td></tr>'
. '<tr><td class="text-muted">Schedule</td><td>' . htmlspecialchars($friendlySchedule) . '</td></tr>'
. '<tr><td class="text-muted">Next Reset</td><td>' . htmlspecialchars($nextFormatted) . ' ' . $nextBadge . '</td></tr>'
. '<tr><td class="text-muted">Last Reset</td><td>' . htmlspecialchars($lastFormatted) . '</td></tr>'
. '<tr><td class="text-muted">Runs</td><td>' . (int) ($task['times_executed'] ?? 0) . ' executed, ' . (int) ($task['times_failed'] ?? 0) . ' failed</td></tr>'
. '<tr><td class="text-muted">Baseline</td><td>' . ($snapshotExists ? '<span class="badge bg-success">Saved</span>' : '<span class="badge bg-warning text-dark">Not taken yet</span>') . '</td></tr>'
. '<tr><td class="text-muted">Banner</td><td>' . ($bannerOn ? 'On' : 'Off') . ($countdownOn ? ' + countdown' : '') . '</td></tr>'
. '<tr><td class="text-muted">Images</td><td>' . ($mediaOn ? 'Included' : 'Excluded') . '</td></tr>'
. '</table>'
. '<a href="' . $editLink . '" class="btn btn-sm btn-outline-primary">'
. '<span class="icon-cog" aria-hidden="true"></span> Manage Scheduled Task</a>'
. '</div>';
}
protected function getLabel()
{
return '<label class="form-label"><strong>Scheduled Reset</strong></label>';
}
/**
* Convert a cron expression to a human-readable string.
*
* @param string $cron Cron expression
*
* @return string
*/
private function friendlySchedule(string $cron): string
{
$map = [
'* * * * *' => 'Every minute',
'*/5 * * * *' => 'Every 5 minutes',
'*/15 * * * *' => 'Every 15 minutes',
'*/30 * * * *' => 'Every 30 minutes',
'0 */1 * * *' => 'Every hour',
'0 */4 * * *' => 'Every 4 hours',
'0 */6 * * *' => 'Every 6 hours',
'0 */12 * * *' => 'Every 12 hours',
'0 0 * * *' => 'Daily at midnight',
'0 6 * * *' => 'Daily at 6:00 AM',
'0 0 * * 0' => 'Weekly (Sunday)',
'0 0 1 * *' => 'Monthly (1st)',
];
return $map[$cron] ?? 'Custom';
}
}
@@ -1,156 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.08
* PATH: /src/Field/NextResetField.php
* BRIEF: Read-only field showing next reset time from Joomla scheduled task
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
/**
* Pulls the next execution time directly from the Joomla scheduled task
* (#__scheduler_tasks) and displays it formatted in the site timezone.
*
* @since 02.29.00
*/
class NextResetField extends FormField
{
protected $type = 'NextReset';
protected function getInput()
{
// Check if demo mode is enabled
$demoEnabled = false;
if ($this->form)
{
$demoEnabled = (int) $this->form->getValue('demo_mode_enabled', 'params', 0) === 1;
}
if (!$demoEnabled)
{
return '<span class="form-control-plaintext text-muted">Demo mode is off</span>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
// Query the actual next_execution from the scheduled task
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select([
$db->quoteName('next_execution'),
$db->quoteName('last_execution'),
$db->quoteName('state'),
])
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset'));
$db->setQuery($query);
$task = $db->loadAssoc();
}
catch (\Throwable $e)
{
$task = null;
}
if (!$task)
{
return '<div class="alert alert-secondary mb-0 py-2">No scheduled task found — save to create one automatically.</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
if ((int) $task['state'] !== 1)
{
return '<div class="alert alert-warning mb-0 py-2">Scheduled task is disabled.</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
$nextExec = $task['next_execution'];
$lastExec = $task['last_execution'];
if (empty($nextExec) || $nextExec === '0000-00-00 00:00:00')
{
return '<div class="alert alert-secondary mb-0 py-2">Waiting for first run...</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
// Convert to site timezone
$utcTimestamp = strtotime($nextExec);
$siteTimezone = Factory::getApplication()->get('offset', 'UTC');
try
{
$dt = new \DateTime('@' . $utcTimestamp);
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$formatted = $dt->format('l, F j, Y \a\t g:i A T');
}
catch (\Throwable $e)
{
$formatted = $nextExec . ' UTC';
}
// Relative time
$diff = $utcTimestamp - time();
$relative = '';
if ($diff <= 0)
{
$relative = '<span class="badge bg-warning text-dark">overdue</span>';
}
elseif ($diff < 3600)
{
$mins = (int) ceil($diff / 60);
$relative = '<span class="badge bg-info">in ' . $mins . ' min</span>';
}
elseif ($diff < 86400)
{
$hours = round($diff / 3600, 1);
$relative = '<span class="badge bg-info">in ' . $hours . 'h</span>';
}
else
{
$days = round($diff / 86400, 1);
$relative = '<span class="badge bg-secondary">in ' . $days . 'd</span>';
}
// Last run info
$lastInfo = '';
if (!empty($lastExec) && $lastExec !== '0000-00-00 00:00:00')
{
try
{
$lastDt = new \DateTime($lastExec);
$lastDt->setTimezone(new \DateTimeZone($siteTimezone));
$lastInfo = '<small class="text-muted ms-2">Last run: ' . $lastDt->format('M j, g:i A') . '</small>';
}
catch (\Throwable $e)
{
// skip
}
}
return '<div class="d-flex align-items-center gap-2 flex-wrap">'
. '<span class="form-control-plaintext" style="font-weight:500">'
. '<span class="icon-calendar" aria-hidden="true"></span> '
. htmlspecialchars($formatted) . '</span> '
. $relative
. $lastInfo
. '<input type="hidden" name="' . $this->name . '" value="' . htmlspecialchars($nextExec) . '" />'
. '</div>';
}
}
@@ -1,175 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.08
* PATH: /src/Field/SnapshotTablesField.php
* BRIEF: Multi-select list field that loads DB tables with sensible defaults
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
/**
* Renders a multi-select list box of all Joomla database tables, with
* content-related tables pre-selected by default.
*
* @since 02.26.00
*/
class SnapshotTablesField extends FormField
{
protected $type = 'SnapshotTables';
/**
* Tables selected by default when no value is stored yet.
*
* @var array
* @since 02.25.00
*/
private const DEFAULT_TABLES = [
'#__content',
'#__categories',
'#__fields',
'#__fields_values',
'#__fields_groups',
'#__menu',
'#__menu_types',
'#__modules',
'#__modules_menu',
'#__users',
'#__user_usergroup_map',
'#__user_profiles',
'#__tags',
'#__contentitem_tag_map',
'#__assets',
];
/**
* Table suffixes grouped by category.
*
* @var array
* @since 02.25.00
*/
private const TABLE_GROUPS = [
'Content' => ['content', 'categories', 'fields', 'fields_values', 'fields_groups', 'tags', 'contentitem_tag_map', 'ucm_content', 'ucm_history'],
'Users' => ['users', 'user_usergroup_map', 'user_profiles', 'usergroups', 'user_keys', 'user_mfa'],
'Menus' => ['menu', 'menu_types'],
'Modules' => ['modules', 'modules_menu'],
'Assets' => ['assets'],
];
protected function getInput()
{
$db = Factory::getDbo();
$prefix = $db->getPrefix();
$tables = $db->getTableList();
// Resolve selected values
$selected = $this->value;
if ($selected === null || $selected === '')
{
$selected = self::DEFAULT_TABLES;
}
elseif (is_string($selected))
{
$selected = array_filter(array_map('trim', explode("\n", $selected)));
}
$selected = (array) $selected;
// Flatten nested arrays from broken save format [["#__content"],["#__categories"]]
$selected = array_map(function ($v) {
return is_array($v) ? reset($v) : $v;
}, $selected);
// Group tables
$grouped = [];
foreach ($tables as $table)
{
if (strpos($table, $prefix) !== 0)
{
continue;
}
$suffix = substr($table, strlen($prefix));
$logical = '#__' . $suffix;
$group = 'Other';
foreach (self::TABLE_GROUPS as $groupName => $patterns)
{
if (in_array($suffix, $patterns, true))
{
$group = $groupName;
break;
}
}
$grouped[$group][] = $logical;
}
// Build HTML select with optgroups
$size = (int) ($this->element['size'] ?? 15);
$html = '<select name="' . $this->name . '" id="' . $this->id . '"'
. ' multiple="multiple" size="' . $size . '"'
. ' class="form-select">';
$priority = ['Content', 'Users', 'Menus', 'Modules', 'Assets'];
foreach ($priority as $g)
{
if (!empty($grouped[$g]))
{
$html .= '<optgroup label="' . $g . '">';
foreach ($grouped[$g] as $t)
{
$sel = in_array($t, $selected, true) ? ' selected="selected"' : '';
$html .= '<option value="' . htmlspecialchars($t) . '"' . $sel . '>' . htmlspecialchars($t) . '</option>';
}
$html .= '</optgroup>';
unset($grouped[$g]);
}
}
if (!empty($grouped['Other']))
{
$html .= '<optgroup label="Other">';
foreach ($grouped['Other'] as $t)
{
$sel = in_array($t, $selected, true) ? ' selected="selected"' : '';
$html .= '<option value="' . htmlspecialchars($t) . '"' . $sel . '>' . htmlspecialchars($t) . '</option>';
}
$html .= '</optgroup>';
}
$html .= '</select>';
// "Reset to defaults" link
$defaultsJson = htmlspecialchars(json_encode(self::DEFAULT_TABLES), ENT_QUOTES, 'UTF-8');
$html .= '<div class="mt-1">'
. '<a href="#" class="small" onclick="'
. 'var sel=document.getElementById(\'' . $this->id . '\');'
. 'var defs=' . $defaultsJson . ';'
. 'Array.from(sel.options).forEach(function(o){o.selected=defs.indexOf(o.value)!==-1;});'
. 'return false;'
. '"><span class="icon-refresh" aria-hidden="true"></span> Reset to defaults</a>'
. '</div>';
return $html;
}
}
@@ -1,819 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
* VERSION: 02.34.08
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
*/
namespace Moko\Plugin\System\MokoWaaS\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
/**
* Content Sync Receiver — applies incoming sync payload to the local site.
*
* Processes categories, articles, menu types, menu items, and modules
* from a JSON payload sent by a source MokoWaaS site. Content is matched
* by alias (upsert pattern): existing content is updated, new content
* is inserted.
*
* @since 02.21.00
*/
class ContentSyncReceiver
{
/**
* @var \Joomla\Database\DatabaseInterface
* @since 02.21.00
*/
private $db;
/**
* Warnings collected during sync.
*
* @var array
* @since 02.21.00
*/
private array $warnings = [];
/**
* Cache of resolved category paths → local IDs.
*
* @var array
* @since 02.21.00
*/
private array $catPathCache = [];
/**
* Constructor.
*
* @param \Joomla\Database\DatabaseInterface|null $db Database driver
*
* @since 02.21.00
*/
public function __construct($db = null)
{
$this->db = $db ?: Factory::getDbo();
}
/**
* Process an incoming sync payload.
*
* @param array $payload Decoded JSON payload from the source site
*
* @return array Result summary with per-type counts and warnings
*
* @since 02.21.00
*/
public function receive(array $payload): array
{
if (empty($payload['mokowaas_sync']))
{
return ['status' => 'error', 'message' => 'Invalid payload — missing mokowaas_sync version'];
}
$this->warnings = [];
$results = [];
// Apply in dependency order
try
{
$results['categories'] = $this->applyCategories($payload['categories'] ?? []);
}
catch (\Throwable $e)
{
$results['categories'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Categories failed: ' . $e->getMessage();
}
try
{
$results['articles'] = $this->applyArticles($payload['articles'] ?? []);
}
catch (\Throwable $e)
{
$results['articles'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Articles failed: ' . $e->getMessage();
}
try
{
$results['menu_types'] = $this->applyMenuTypes($payload['menu_types'] ?? []);
}
catch (\Throwable $e)
{
$results['menu_types'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Menu types failed: ' . $e->getMessage();
}
try
{
$results['menu_items'] = $this->applyMenuItems($payload['menu_items'] ?? []);
}
catch (\Throwable $e)
{
$results['menu_items'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Menu items failed: ' . $e->getMessage();
}
try
{
$results['modules'] = $this->applyModules($payload['modules'] ?? []);
}
catch (\Throwable $e)
{
$results['modules'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Modules failed: ' . $e->getMessage();
}
Log::add(
sprintf('Content sync received from %s', $payload['source_site'] ?? 'unknown'),
Log::INFO,
'mokowaas'
);
return [
'status' => 'ok',
'message' => 'Sync applied',
'source_site' => $payload['source_site'] ?? '',
'results' => $results,
'warnings' => $this->warnings,
];
}
/**
* Apply categories — sorted by path depth (shallow first).
*
* @param array $categories Category data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyCategories(array $categories): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
// Sort by path depth — parents before children
usort($categories, function ($a, $b) {
return substr_count($a['path'], '/') - substr_count($b['path'], '/');
});
foreach ($categories as $cat)
{
$alias = $cat['alias'] ?? '';
$path = $cat['path'] ?? $alias;
if (empty($alias) || !preg_match('/^[a-z0-9\-\/]+$/i', $path))
{
$this->warnings[] = 'Skipped category with invalid alias/path: ' . $alias;
continue;
}
// Resolve parent ID from path
$parentId = 1; // Root
$pathParts = explode('/', $path);
if (count($pathParts) > 1)
{
$parentPath = implode('/', array_slice($pathParts, 0, -1));
$parentId = $this->resolveCategoryPath($parentPath);
if ($parentId === 0)
{
$this->warnings[] = 'Parent category not found for: ' . $path;
$parentId = 1;
}
}
// Check if category exists
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__categories'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('parent_id') . ' = ' . (int) $parentId);
$db->setQuery($query);
$existingId = (int) $db->loadResult();
$now = Factory::getDate()->toSql();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__categories'))
->set($db->quoteName('title') . ' = ' . $db->quote($cat['title']))
->set($db->quoteName('description') . ' = ' . $db->quote($cat['description'] ?? ''))
->set($db->quoteName('published') . ' = ' . (int) ($cat['published'] ?? 1))
->set($db->quoteName('access') . ' = ' . (int) ($cat['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($cat['language'] ?? '*'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($cat['params'] ?? new \stdClass)))
->set($db->quoteName('metadata') . ' = ' . $db->quote(json_encode($cat['metadata'] ?? new \stdClass)))
->set($db->quoteName('modified_time') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
$this->catPathCache[$path] = $existingId;
}
else
{
$obj = (object) [
'title' => $cat['title'],
'alias' => $alias,
'path' => $path,
'extension' => 'com_content',
'description' => $cat['description'] ?? '',
'published' => (int) ($cat['published'] ?? 1),
'access' => (int) ($cat['access'] ?? 1),
'language' => $cat['language'] ?? '*',
'params' => json_encode($cat['params'] ?? new \stdClass),
'metadata' => json_encode($cat['metadata'] ?? new \stdClass),
'parent_id' => $parentId,
'level' => count($pathParts),
'lft' => 0,
'rgt' => 0,
'created_time' => $now,
'modified_time' => $now,
];
$db->insertObject('#__categories', $obj, 'id');
$inserted++;
$this->catPathCache[$path] = (int) $obj->id;
// Rebuild category tree for this extension
$this->rebuildCategoryTree();
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply articles — resolve category by alias path, upsert by alias.
*
* @param array $articles Article data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyArticles(array $articles): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
foreach ($articles as $article)
{
$alias = $article['alias'] ?? '';
if (empty($alias))
{
continue;
}
// Resolve category
$catPath = $article['catid_alias_path'] ?? 'uncategorised';
$catId = $this->resolveCategoryPath($catPath);
if ($catId === 0)
{
$catId = 2; // Joomla's built-in Uncategorised
$this->warnings[] = 'Category "' . $catPath . '" not found for article "' . $alias . '" — assigned to Uncategorised';
}
// Check if article exists
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__content'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias));
$db->setQuery($query);
$existingId = (int) $db->loadResult();
$now = Factory::getDate()->toSql();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__content'))
->set($db->quoteName('title') . ' = ' . $db->quote($article['title']))
->set($db->quoteName('introtext') . ' = ' . $db->quote($article['introtext'] ?? ''))
->set($db->quoteName('fulltext') . ' = ' . $db->quote($article['fulltext'] ?? ''))
->set($db->quoteName('state') . ' = ' . (int) ($article['state'] ?? 1))
->set($db->quoteName('catid') . ' = ' . $catId)
->set($db->quoteName('access') . ' = ' . (int) ($article['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($article['language'] ?? '*'))
->set($db->quoteName('featured') . ' = ' . (int) ($article['featured'] ?? 0))
->set($db->quoteName('metadata') . ' = ' . $db->quote(json_encode($article['metadata'] ?? new \stdClass)))
->set($db->quoteName('attribs') . ' = ' . $db->quote(json_encode($article['attribs'] ?? new \stdClass)))
->set($db->quoteName('images') . ' = ' . $db->quote(json_encode($article['images'] ?? new \stdClass)))
->set($db->quoteName('urls') . ' = ' . $db->quote(json_encode($article['urls'] ?? new \stdClass)))
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
}
else
{
$obj = (object) [
'title' => $article['title'],
'alias' => $alias,
'introtext' => $article['introtext'] ?? '',
'fulltext' => $article['fulltext'] ?? '',
'state' => (int) ($article['state'] ?? 1),
'catid' => $catId,
'access' => (int) ($article['access'] ?? 1),
'language' => $article['language'] ?? '*',
'featured' => (int) ($article['featured'] ?? 0),
'publish_up' => $article['publish_up'] ?? $now,
'publish_down' => $article['publish_down'],
'metadata' => json_encode($article['metadata'] ?? new \stdClass),
'attribs' => json_encode($article['attribs'] ?? new \stdClass),
'images' => json_encode($article['images'] ?? new \stdClass),
'urls' => json_encode($article['urls'] ?? new \stdClass),
'created' => $now,
'modified' => $now,
];
$db->insertObject('#__content', $obj, 'id');
$inserted++;
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply menu types — insert if not exists.
*
* @param array $menuTypes Menu type data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyMenuTypes(array $menuTypes): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
foreach ($menuTypes as $mt)
{
$menutype = $mt['menutype'] ?? '';
if (empty($menutype))
{
continue;
}
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__menu_types'))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype));
$db->setQuery($query);
if ($db->loadResult())
{
$query = $db->getQuery(true)
->update($db->quoteName('#__menu_types'))
->set($db->quoteName('title') . ' = ' . $db->quote($mt['title'] ?? $menutype))
->set($db->quoteName('description') . ' = ' . $db->quote($mt['description'] ?? ''))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype));
$db->setQuery($query);
$db->execute();
$updated++;
}
else
{
$obj = (object) [
'title' => $mt['title'] ?? $menutype,
'menutype' => $menutype,
'description' => $mt['description'] ?? '',
];
$db->insertObject('#__menu_types', $obj);
$inserted++;
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply menu items — resolve parent aliases and {catid:path} tokens.
*
* @param array $items Menu item data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyMenuItems(array $items): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
// Sort: root items first, then children
usort($items, function ($a, $b) {
$aIsRoot = empty($a['parent_alias']);
$bIsRoot = empty($b['parent_alias']);
if ($aIsRoot && !$bIsRoot) return -1;
if (!$aIsRoot && $bIsRoot) return 1;
return 0;
});
// Resolve component IDs
$compQuery = $db->getQuery(true)
->select([$db->quoteName('extension_id'), $db->quoteName('element')])
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($compQuery);
$compMap = [];
foreach ($db->loadAssocList() ?: [] as $c)
{
$compMap[$c['element']] = (int) $c['extension_id'];
}
foreach ($items as $item)
{
$alias = $item['alias'] ?? '';
$menutype = $item['menutype'] ?? '';
if (empty($alias) || empty($menutype))
{
continue;
}
// Resolve parent
$parentId = 1; // Root menu item
if (!empty($item['parent_alias']))
{
$parentId = $this->resolveMenuAlias($menutype, $item['parent_alias']);
if ($parentId === 0)
{
$this->warnings[] = 'Parent menu item "' . $item['parent_alias'] . '" not found for "' . $alias . '"';
$parentId = 1;
}
}
// Resolve {catid:path} tokens in link
$link = $item['link'] ?? '';
if (preg_match_all('/\{catid:([^}]+)\}/', $link, $matches))
{
foreach ($matches[1] as $i => $catPath)
{
$localCatId = $this->resolveCategoryPath($catPath);
if ($localCatId > 0)
{
$link = str_replace($matches[0][$i], (string) $localCatId, $link);
}
else
{
$this->warnings[] = 'Could not resolve {catid:' . $catPath . '} in menu item "' . $alias . '"';
$link = str_replace($matches[0][$i], '0', $link);
}
}
}
$componentId = $compMap[$item['component_name'] ?? ''] ?? 0;
// Check if menu item exists
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype))
->where($db->quoteName('client_id') . ' = 0');
$db->setQuery($query);
$existingId = (int) $db->loadResult();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__menu'))
->set($db->quoteName('title') . ' = ' . $db->quote($item['title']))
->set($db->quoteName('link') . ' = ' . $db->quote($link))
->set($db->quoteName('type') . ' = ' . $db->quote($item['type'] ?? 'component'))
->set($db->quoteName('published') . ' = ' . (int) ($item['published'] ?? 1))
->set($db->quoteName('access') . ' = ' . (int) ($item['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($item['language'] ?? '*'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($item['params'] ?? new \stdClass)))
->set($db->quoteName('parent_id') . ' = ' . $parentId)
->set($db->quoteName('component_id') . ' = ' . $componentId)
->set($db->quoteName('home') . ' = ' . (int) ($item['home'] ?? 0))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
}
else
{
$obj = (object) [
'menutype' => $menutype,
'title' => $item['title'],
'alias' => $alias,
'path' => $alias,
'link' => $link,
'type' => $item['type'] ?? 'component',
'published' => (int) ($item['published'] ?? 1),
'parent_id' => $parentId,
'level' => $parentId <= 1 ? 1 : 2,
'component_id' => $componentId,
'access' => (int) ($item['access'] ?? 1),
'language' => $item['language'] ?? '*',
'params' => json_encode($item['params'] ?? new \stdClass),
'home' => (int) ($item['home'] ?? 0),
'client_id' => 0,
'lft' => 0,
'rgt' => 0,
];
$db->insertObject('#__menu', $obj, 'id');
$inserted++;
}
}
// Rebuild menu tree for each affected menutype
$affectedMenuTypes = array_unique(array_column($items, 'menutype'));
foreach ($affectedMenuTypes as $mt)
{
$this->rebuildMenuTree($mt);
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply modules — upsert by title+position+client_id, rebuild menu assignments.
*
* @param array $modules Module data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyModules(array $modules): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
foreach ($modules as $mod)
{
$title = $mod['title'] ?? '';
$position = $mod['position'] ?? '';
$clientId = (int) ($mod['client_id'] ?? 0);
if (empty($title))
{
continue;
}
// Check existence by title + position + client_id
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__modules'))
->where($db->quoteName('title') . ' = ' . $db->quote($title))
->where($db->quoteName('position') . ' = ' . $db->quote($position))
->where($db->quoteName('client_id') . ' = ' . $clientId);
$db->setQuery($query);
$existingId = (int) $db->loadResult();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__modules'))
->set($db->quoteName('module') . ' = ' . $db->quote($mod['module'] ?? ''))
->set($db->quoteName('content') . ' = ' . $db->quote($mod['content'] ?? ''))
->set($db->quoteName('published') . ' = ' . (int) ($mod['published'] ?? 1))
->set($db->quoteName('access') . ' = ' . (int) ($mod['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($mod['language'] ?? '*'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($mod['params'] ?? new \stdClass)))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
$moduleId = $existingId;
}
else
{
$obj = (object) [
'title' => $title,
'module' => $mod['module'] ?? '',
'position' => $position,
'content' => $mod['content'] ?? '',
'published' => (int) ($mod['published'] ?? 1),
'access' => (int) ($mod['access'] ?? 1),
'language' => $mod['language'] ?? '*',
'params' => json_encode($mod['params'] ?? new \stdClass),
'client_id' => $clientId,
'ordering' => 0,
];
$db->insertObject('#__modules', $obj, 'id');
$inserted++;
$moduleId = (int) $obj->id;
}
// Rebuild menu assignments
$query = $db->getQuery(true)
->delete($db->quoteName('#__modules_menu'))
->where($db->quoteName('moduleid') . ' = ' . $moduleId);
$db->setQuery($query);
$db->execute();
$assignment = $mod['menu_assignment'] ?? [];
$assignType = (int) ($assignment['assignment'] ?? 0);
$aliases = $assignment['menu_item_aliases'] ?? [];
if ($assignType === 0 || empty($aliases))
{
// All pages
$obj = (object) ['moduleid' => $moduleId, 'menuid' => 0];
$db->insertObject('#__modules_menu', $obj);
}
else
{
foreach ($aliases as $aliasRef)
{
// Format: "menutype:alias"
$parts = explode(':', $aliasRef, 2);
if (count($parts) !== 2)
{
continue;
}
$menuId = $this->resolveMenuAlias($parts[0], $parts[1]);
if ($menuId > 0)
{
$menuidValue = $assignType === -1 ? -$menuId : $menuId;
$obj = (object) ['moduleid' => $moduleId, 'menuid' => $menuidValue];
$db->insertObject('#__modules_menu', $obj);
}
else
{
$this->warnings[] = 'Module "' . $title . '": menu item "' . $aliasRef . '" not found';
}
}
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Resolve a category alias path to a local category ID.
*
* @param string $path Slash-delimited alias path (e.g. "blog/news")
*
* @return int Category ID, or 0 if not found
*
* @since 02.21.00
*/
private function resolveCategoryPath(string $path): int
{
if (isset($this->catPathCache[$path]))
{
return $this->catPathCache[$path];
}
$db = $this->db;
$segments = explode('/', $path);
$parentId = 1;
foreach ($segments as $segment)
{
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__categories'))
->where($db->quoteName('alias') . ' = ' . $db->quote($segment))
->where($db->quoteName('parent_id') . ' = ' . $parentId)
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'));
$db->setQuery($query);
$id = (int) $db->loadResult();
if ($id === 0)
{
$this->catPathCache[$path] = 0;
return 0;
}
$parentId = $id;
}
$this->catPathCache[$path] = $parentId;
return $parentId;
}
/**
* Resolve a menu item alias to a local menu ID.
*
* @param string $menutype Menu type key
* @param string $alias Menu item alias
*
* @return int Menu item ID, or 0 if not found
*
* @since 02.21.00
*/
private function resolveMenuAlias(string $menutype, string $alias): int
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype))
->where($db->quoteName('client_id') . ' = 0');
$db->setQuery($query);
return (int) $db->loadResult();
}
/**
* Rebuild the nested set (lft/rgt) for the category tree.
*
* Uses Joomla's built-in Table rebuild method.
*
* @return void
*
* @since 02.21.00
*/
private function rebuildCategoryTree(): void
{
try
{
$table = \Joomla\CMS\Table\Table::getInstance('Category');
$table->rebuild();
}
catch (\Throwable $e)
{
$this->warnings[] = 'Category tree rebuild failed: ' . $e->getMessage();
}
}
/**
* Rebuild the nested set (lft/rgt) for a menu type.
*
* @param string $menutype Menu type to rebuild
*
* @return void
*
* @since 02.21.00
*/
private function rebuildMenuTree(string $menutype): void
{
try
{
$table = \Joomla\CMS\Table\Table::getInstance('Menu');
$table->rebuild();
}
catch (\Throwable $e)
{
$this->warnings[] = 'Menu tree rebuild failed for "' . $menutype . '": ' . $e->getMessage();
}
}
}
@@ -1,634 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncService.php
* VERSION: 02.34.08
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites
*/
namespace Moko\Plugin\System\MokoWaaS\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Uri\Uri;
/**
* Content Sync Service — builds a JSON payload of site content and pushes
* it to one or more remote MokoWaaS sites.
*
* Content is matched by alias on the receiving end (upsert-by-alias).
* Category IDs in menu item links are encoded as {catid:alias/path} tokens
* so the receiver can resolve them to local IDs.
*
* @since 02.21.00
*/
class ContentSyncService
{
/**
* Maximum items per content type to prevent unbounded memory.
*
* @var int
* @since 02.21.00
*/
private const MAX_ITEMS = 2000;
/**
* HTTP timeout for push requests in seconds.
*
* @var int
* @since 02.21.00
*/
private const HTTP_TIMEOUT = 60;
/**
* @var \Joomla\Database\DatabaseInterface
* @since 02.21.00
*/
private $db;
/**
* Category ID → alias path map cache.
*
* @var array
* @since 02.21.00
*/
private array $catPathMap = [];
/**
* Constructor.
*
* @param \Joomla\Database\DatabaseInterface|null $db Database driver
*
* @since 02.21.00
*/
public function __construct($db = null)
{
$this->db = $db ?: Factory::getDbo();
}
/**
* Build the full sync payload from local content.
*
* @return array Structured payload ready for JSON encoding
*
* @since 02.21.00
*/
public function buildPayload(): array
{
$this->catPathMap = $this->buildCategoryPathMap();
return [
'mokowaas_sync' => '1.0',
'source_site' => rtrim(Uri::root(), '/'),
'generated_at' => gmdate('Y-m-d\TH:i:s\Z'),
'categories' => $this->buildCategoryPayload(),
'articles' => $this->buildArticlePayload(),
'menu_types' => $this->buildMenuTypePayload(),
'menu_items' => $this->buildMenuItemPayload(),
'modules' => $this->buildModulePayload(),
];
}
/**
* Push the sync payload to a single target site.
*
* @param string $targetUrl Base URL of the target site
* @param string $token health_api_token for the target
*
* @return array Result with status, message, and per-type counts
*
* @since 02.21.00
*/
public function pushToTarget(string $targetUrl, string $token): array
{
$payload = $this->buildPayload();
$jsonBody = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$endpoint = rtrim($targetUrl, '/') . '/?mokowaas=sync-receive';
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Authorization: Bearer {$token}\r\nContent-Type: application/json\r\n",
'content' => $jsonBody,
'timeout' => self::HTTP_TIMEOUT,
'ignore_errors' => true,
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$response = @file_get_contents($endpoint, false, $context);
if ($response === false)
{
return [
'status' => 'error',
'target' => $targetUrl,
'message' => 'Connection failed — target unreachable',
];
}
// Parse HTTP status from response headers
$httpCode = 0;
if (isset($http_response_header[0]))
{
preg_match('/\d{3}/', $http_response_header[0], $matches);
$httpCode = (int) ($matches[0] ?? 0);
}
$result = json_decode($response, true);
if ($httpCode >= 400 || !$result)
{
return [
'status' => 'error',
'target' => $targetUrl,
'http_code' => $httpCode,
'message' => $result['error'] ?? $result['message'] ?? 'Unknown error from target',
];
}
$result['target'] = $targetUrl;
return $result;
}
/**
* Push content to all configured sync targets.
*
* @param array $targets Array of ['url' => ..., 'token' => ..., 'label' => ...]
*
* @return array Per-target results
*
* @since 02.21.00
*/
public function syncAllTargets(array $targets): array
{
$results = [];
foreach ($targets as $target)
{
$url = $target['url'] ?? '';
$token = $target['token'] ?? '';
$label = $target['label'] ?? $url;
if (empty($url) || empty($token))
{
$results[] = [
'status' => 'skipped',
'target' => $label,
'message' => 'Missing URL or token',
];
continue;
}
try
{
$result = $this->pushToTarget($url, $token);
$result['label'] = $label;
$results[] = $result;
}
catch (\Throwable $e)
{
$results[] = [
'status' => 'error',
'target' => $label,
'message' => $e->getMessage(),
];
}
}
Log::add(
sprintf('Content sync pushed to %d target(s)', count($targets)),
Log::INFO,
'mokowaas'
);
return [
'status' => 'ok',
'message' => sprintf('Sync completed for %d target(s)', count($results)),
'targets' => $results,
];
}
/**
* Build category ID → alias path map.
*
* @return array [id => 'parent-alias/child-alias']
*
* @since 02.21.00
*/
private function buildCategoryPathMap(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('alias'), $db->quoteName('parent_id')])
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('published') . ' != -2')
->where($db->quoteName('id') . ' > 1');
$db->setQuery($query);
$rows = $db->loadAssocList('id');
$map = [];
foreach ($rows as $id => $row)
{
$map[$id] = $this->resolvePathFromRows($id, $rows);
}
return $map;
}
/**
* Recursively build alias path for a category ID.
*
* @param int $id Category ID
* @param array $rows All category rows keyed by ID
*
* @return string Slash-delimited alias path
*
* @since 02.21.00
*/
private function resolvePathFromRows(int $id, array $rows): string
{
if (!isset($rows[$id]))
{
return '';
}
$row = $rows[$id];
$parentId = (int) $row['parent_id'];
if ($parentId <= 1 || !isset($rows[$parentId]))
{
return $row['alias'];
}
return $this->resolvePathFromRows($parentId, $rows) . '/' . $row['alias'];
}
/**
* Build category payload.
*
* @return array
*
* @since 02.21.00
*/
private function buildCategoryPayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('id'),
$db->quoteName('title'),
$db->quoteName('alias'),
$db->quoteName('description'),
$db->quoteName('published'),
$db->quoteName('access'),
$db->quoteName('language'),
$db->quoteName('params'),
$db->quoteName('metadata'),
])
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('published') . ' != -2')
->where($db->quoteName('id') . ' > 1')
->order($db->quoteName('lft') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
$categories = [];
foreach ($rows as $row)
{
$categories[] = [
'title' => $row['title'],
'alias' => $row['alias'],
'path' => $this->catPathMap[(int) $row['id']] ?? $row['alias'],
'description' => $row['description'] ?? '',
'published' => (int) $row['published'],
'access' => (int) $row['access'],
'language' => $row['language'],
'params' => json_decode($row['params'] ?: '{}', true),
'metadata' => json_decode($row['metadata'] ?: '{}', true),
];
}
return $categories;
}
/**
* Build article payload.
*
* @return array
*
* @since 02.21.00
*/
private function buildArticlePayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('title'),
$db->quoteName('alias'),
$db->quoteName('introtext'),
$db->quoteName('fulltext'),
$db->quoteName('state'),
$db->quoteName('catid'),
$db->quoteName('access'),
$db->quoteName('language'),
$db->quoteName('featured'),
$db->quoteName('publish_up'),
$db->quoteName('publish_down'),
$db->quoteName('metadata'),
$db->quoteName('attribs'),
$db->quoteName('images'),
$db->quoteName('urls'),
])
->from($db->quoteName('#__content'))
->where($db->quoteName('state') . ' != -2')
->order($db->quoteName('id') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
$articles = [];
foreach ($rows as $row)
{
$articles[] = [
'title' => $row['title'],
'alias' => $row['alias'],
'introtext' => $row['introtext'],
'fulltext' => $row['fulltext'],
'state' => (int) $row['state'],
'catid_alias_path' => $this->catPathMap[(int) $row['catid']] ?? 'uncategorised',
'access' => (int) $row['access'],
'language' => $row['language'],
'featured' => (int) $row['featured'],
'publish_up' => $row['publish_up'],
'publish_down' => $row['publish_down'],
'metadata' => json_decode($row['metadata'] ?: '{}', true),
'attribs' => json_decode($row['attribs'] ?: '{}', true),
'images' => json_decode($row['images'] ?: '{}', true),
'urls' => json_decode($row['urls'] ?: '{}', true),
];
}
return $articles;
}
/**
* Build menu type payload.
*
* @return array
*
* @since 02.21.00
*/
private function buildMenuTypePayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('title'),
$db->quoteName('menutype'),
$db->quoteName('description'),
])
->from($db->quoteName('#__menu_types'));
$db->setQuery($query);
return $db->loadAssocList() ?: [];
}
/**
* Build menu item payload with {catid:path} tokens in links.
*
* @return array
*
* @since 02.21.00
*/
private function buildMenuItemPayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('a.title'),
$db->quoteName('a.alias'),
$db->quoteName('a.menutype'),
$db->quoteName('a.parent_id'),
$db->quoteName('a.link'),
$db->quoteName('a.type'),
$db->quoteName('a.published'),
$db->quoteName('a.access'),
$db->quoteName('a.language'),
$db->quoteName('a.params'),
$db->quoteName('a.home'),
$db->quoteName('a.component_id'),
$db->quoteName('b.alias', 'parent_alias'),
])
->from($db->quoteName('#__menu', 'a'))
->leftJoin(
$db->quoteName('#__menu', 'b') . ' ON '
. $db->quoteName('a.parent_id') . ' = ' . $db->quoteName('b.id')
)
->where($db->quoteName('a.published') . ' != -2')
->where($db->quoteName('a.client_id') . ' = 0')
->where($db->quoteName('a.level') . ' >= 1')
->order($db->quoteName('a.lft') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
// Get component name map
$compQuery = $db->getQuery(true)
->select([$db->quoteName('extension_id'), $db->quoteName('element')])
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($compQuery);
$components = $db->loadAssocList('extension_id') ?: [];
$items = [];
foreach ($rows as $row)
{
$link = $row['link'];
// Encode category IDs in com_content links as {catid:path} tokens
if (preg_match('/option=com_content/', $link) && preg_match('/&id=(\d+)/', $link, $m))
{
$catId = (int) $m[1];
if (isset($this->catPathMap[$catId]))
{
$link = preg_replace('/&id=\d+/', '&id={catid:' . $this->catPathMap[$catId] . '}', $link);
}
}
$compName = '';
if (!empty($row['component_id']) && isset($components[$row['component_id']]))
{
$compName = $components[$row['component_id']]['element'];
}
// Root-level items have parent_id=1 (Joomla's root menu item)
$parentAlias = ((int) $row['parent_id'] <= 1) ? '' : ($row['parent_alias'] ?? '');
$items[] = [
'title' => $row['title'],
'alias' => $row['alias'],
'menutype' => $row['menutype'],
'parent_alias' => $parentAlias,
'link' => $link,
'type' => $row['type'],
'component_name' => $compName,
'published' => (int) $row['published'],
'access' => (int) $row['access'],
'language' => $row['language'],
'params' => json_decode($row['params'] ?: '{}', true),
'home' => (int) $row['home'],
];
}
return $items;
}
/**
* Build module payload with menu assignments.
*
* @return array
*
* @since 02.21.00
*/
private function buildModulePayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('id'),
$db->quoteName('title'),
$db->quoteName('module'),
$db->quoteName('position'),
$db->quoteName('content'),
$db->quoteName('published'),
$db->quoteName('access'),
$db->quoteName('language'),
$db->quoteName('params'),
$db->quoteName('client_id'),
])
->from($db->quoteName('#__modules'))
->where($db->quoteName('client_id') . ' = 0')
->where($db->quoteName('published') . ' != -2')
->order($db->quoteName('ordering') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
// Get all module-menu assignments
$mmQuery = $db->getQuery(true)
->select([
$db->quoteName('mm.moduleid'),
$db->quoteName('mm.menuid'),
$db->quoteName('m.alias', 'menu_alias'),
$db->quoteName('m.menutype'),
])
->from($db->quoteName('#__modules_menu', 'mm'))
->leftJoin(
$db->quoteName('#__menu', 'm') . ' ON '
. $db->quoteName('mm.menuid') . ' = ' . $db->quoteName('m.id')
);
$db->setQuery($mmQuery);
$allAssignments = $db->loadAssocList();
// Group assignments by module ID
$assignmentsByModule = [];
foreach ($allAssignments as $a)
{
$assignmentsByModule[(int) $a['moduleid']][] = $a;
}
$modules = [];
foreach ($rows as $row)
{
$moduleId = (int) $row['id'];
$assignments = $assignmentsByModule[$moduleId] ?? [];
// Determine assignment type: 0 = all pages, positive = selected, negative = excluded
$menuAliases = [];
$assignType = 0;
if (!empty($assignments))
{
$firstMenuId = (int) $assignments[0]['menuid'];
if ($firstMenuId === 0)
{
$assignType = 0; // All pages
}
elseif ($firstMenuId < 0)
{
$assignType = -1; // All except selected
foreach ($assignments as $a)
{
if (!empty($a['menu_alias']))
{
$menuAliases[] = $a['menutype'] . ':' . $a['menu_alias'];
}
}
}
else
{
$assignType = 1; // Selected only
foreach ($assignments as $a)
{
if (!empty($a['menu_alias']))
{
$menuAliases[] = $a['menutype'] . ':' . $a['menu_alias'];
}
}
}
}
$modules[] = [
'title' => $row['title'],
'module' => $row['module'],
'position' => $row['position'],
'content' => $row['content'],
'published' => (int) $row['published'],
'access' => (int) $row['access'],
'language' => $row['language'],
'params' => json_decode($row['params'] ?: '{}', true),
'client_id' => (int) $row['client_id'],
'menu_assignment' => [
'assignment' => $assignType,
'menu_item_aliases' => $menuAliases,
],
];
}
return $modules;
}
}
@@ -1,606 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php
* VERSION: 02.34.08
* BRIEF: Content-only snapshot/restore for demo site reset
*/
namespace Moko\Plugin\System\MokoWaaS\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
/**
* Demo Reset Service — content-only snapshot and restore.
*
* Only touches safe content tables (articles, categories, menus, modules,
* users, tags, fields). Never touches extensions, assets, sessions,
* schemas, update sites, or any system tables.
*
* @since 02.31.00
*/
class DemoResetService
{
private const MAX_NAME_LENGTH = 64;
private const BATCH_SIZE = 500;
/**
* Safe content tables to snapshot/restore.
* These can be wiped and restored without breaking the Joomla installation.
*/
private const SAFE_TABLES = [
// Content
'#__content',
'#__content_frontpage',
'#__categories',
'#__fields',
'#__fields_values',
'#__fields_groups',
'#__tags',
'#__contentitem_tag_map',
'#__ucm_content',
// Menus
'#__menu',
'#__menu_types',
// Modules
'#__modules',
'#__modules_menu',
// Users
'#__users',
'#__user_usergroup_map',
'#__user_profiles',
// Contact
'#__contact_details',
// Banners
'#__banners',
'#__banner_clients',
'#__banner_tracks',
// Community Builder
'#__comprofiler',
'#__comprofiler_fields',
'#__comprofiler_field_values',
'#__comprofiler_tabs',
'#__comprofiler_members',
'#__comprofiler_lists',
'#__comprofiler_plugin',
'#__comprofiler_userreports',
];
/**
* @var string
*/
private string $snapshotDir;
/**
* @var bool
*/
private bool $includeMedia;
/**
* @param bool $includeMedia Include /images/ directory
* @param string $baseDir Override snapshot root
*/
public function __construct(bool $includeMedia = true, string $baseDir = '')
{
$this->includeMedia = $includeMedia;
$this->snapshotDir = $baseDir ?: JPATH_ROOT . '/mokowaas-snapshots';
}
/**
* List all available snapshots.
*
* @return array
*/
public function listSnapshots(): array
{
$snapshots = [];
if (!is_dir($this->snapshotDir))
{
return $snapshots;
}
foreach (glob($this->snapshotDir . '/*/manifest.json') as $path)
{
$data = json_decode(file_get_contents($path), true);
if ($data && isset($data['name']))
{
$snapshots[$data['name']] = $data;
}
}
return $snapshots;
}
/**
* Create a content snapshot.
*
* @param string $name Snapshot name
*
* @return array Result
*/
public function createSnapshot(string $name): array
{
$this->validateSnapshotName($name);
$this->ensureSnapshotDir();
$path = $this->getSnapshotPath($name);
if (is_dir($path))
{
$this->removeDirectory($path);
}
mkdir($path, 0755, true);
$db = Factory::getDbo();
$prefix = $db->getPrefix();
$allTables = $db->getTableList();
$dumped = 0;
foreach (self::SAFE_TABLES as $logicalName)
{
$realName = str_replace('#__', $prefix, $logicalName);
if (!in_array($realName, $allTables))
{
continue;
}
$this->dumpTable($logicalName, $realName, $path, $db);
$dumped++;
}
// Media
$hasMedia = false;
if ($this->includeMedia && is_dir(JPATH_ROOT . '/images'))
{
$hasMedia = $this->zipDirectory(JPATH_ROOT . '/images', $path . '/media.zip');
}
$manifest = [
'name' => $name,
'created_at' => gmdate('Y-m-d\TH:i:s\Z'),
'type' => 'content-only',
'tables' => $dumped,
'has_media' => $hasMedia,
'joomla_version' => JVERSION,
];
file_put_contents($path . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
Log::add(sprintf('Demo snapshot "%s" created (%d tables, media=%s)', $name, $dumped, $hasMedia ? 'yes' : 'no'), Log::INFO, 'mokowaas');
return [
'status' => 'ok',
'message' => 'Snapshot created',
'name' => $name,
'tables' => $dumped,
'has_media' => $hasMedia,
];
}
/**
* Restore from a snapshot.
*
* @param string $name Snapshot name
*
* @return array Result
*/
public function restoreSnapshot(string $name): array
{
$this->validateSnapshotName($name);
$path = $this->getSnapshotPath($name);
$manifest = $path . '/manifest.json';
if (!file_exists($manifest))
{
throw new \RuntimeException('Snapshot not found: ' . $name);
}
$manifestData = json_decode(file_get_contents($manifest), true);
// Clear cache
try { Factory::getCache('')->clean(''); } catch (\Throwable $e) {}
$db = Factory::getDbo();
$prefix = $db->getPrefix();
$restored = 0;
$sqlFiles = glob($path . '/*.sql');
foreach ($sqlFiles as $sqlFile)
{
try
{
$this->restoreTable($sqlFile, $db, $prefix);
$restored++;
}
catch (\Throwable $e)
{
Log::add('Demo reset: failed to restore ' . basename($sqlFile) . ': ' . $e->getMessage(), Log::ERROR, 'mokowaas');
}
}
// Restore /images/
$mediaRestored = false;
if (($manifestData['has_media'] ?? false) && file_exists($path . '/media.zip'))
{
$this->clearDirectory(JPATH_ROOT . '/images');
$zip = new \ZipArchive();
if ($zip->open($path . '/media.zip') === true)
{
$zip->extractTo(JPATH_ROOT . '/images');
$zip->close();
$mediaRestored = true;
}
}
// Rebuild assets table to fix ACL after content restore
$this->rebuildAssets();
Log::add(sprintf('Demo site reset (%d tables, media=%s)', $restored, $mediaRestored ? 'yes' : 'no'), Log::WARNING, 'mokowaas');
return [
'status' => 'ok',
'message' => 'Site content restored',
'baseline' => $name,
'restored_tables' => $restored,
'media_restored' => $mediaRestored,
];
}
/**
* Delete a snapshot.
*/
public function deleteSnapshot(string $name): bool
{
$this->validateSnapshotName($name);
$path = $this->getSnapshotPath($name);
if (!is_dir($path))
{
return false;
}
$this->removeDirectory($path);
return true;
}
/**
* Rebuild the assets table after content restore.
*
* Deletes content-related asset entries (which now have stale IDs)
* and rebuilds them using Joomla's Table classes. Extension and
* component-level assets are left untouched.
*
* @return void
*/
private function rebuildAssets(): void
{
try
{
$db = Factory::getDbo();
// Delete content-level assets (articles, categories, modules, etc.)
// Keep component-level and root assets intact
$contentAssetPrefixes = [
'com_content.article.%',
'com_content.category.%',
'com_contact.contact.%',
'com_banners.banner.%',
'com_banners.category.%',
'com_modules.module.%',
'com_menus.menu.%',
'com_users.user.%',
];
foreach ($contentAssetPrefixes as $prefix)
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__assets'))
->where($db->quoteName('name') . ' LIKE ' . $db->quote($prefix))
);
$db->execute();
}
// Rebuild category tree (also fixes category assets)
$catTable = \Joomla\CMS\Table\Table::getInstance('Category');
if ($catTable)
{
$catTable->rebuild();
}
// Rebuild menu tree
$menuTable = \Joomla\CMS\Table\Table::getInstance('Menu');
if ($menuTable)
{
$menuTable->rebuild();
}
// Rebuild asset tree
$assetTable = \Joomla\CMS\Table\Table::getInstance('Asset');
if ($assetTable)
{
$assetTable->rebuild();
}
// Re-create assets for content items that lost theirs
$this->fixContentAssets($db);
}
catch (\Throwable $e)
{
Log::add('Asset rebuild warning: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
/**
* Re-create missing asset entries for content items.
*
* After deleting stale assets and restoring content, some items
* may reference asset_id=0. This creates new asset rows for them.
*
* @param \Joomla\Database\DatabaseInterface $db
*
* @return void
*/
private function fixContentAssets($db): void
{
// Fix articles with missing assets
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('title'), $db->quoteName('alias')])
->from($db->quoteName('#__content'))
->where($db->quoteName('asset_id') . ' = 0');
$db->setQuery($query);
$articles = $db->loadAssocList() ?: [];
// Find the com_content component asset as parent
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__assets'))
->where($db->quoteName('name') . ' = ' . $db->quote('com_content'))
);
$contentAssetId = (int) $db->loadResult();
foreach ($articles as $article)
{
$assetName = 'com_content.article.' . (int) $article['id'];
$asset = (object) [
'parent_id' => $contentAssetId ?: 1,
'lft' => 0,
'rgt' => 0,
'level' => 0,
'name' => $assetName,
'title' => $article['title'],
'rules' => '{}',
];
$db->insertObject('#__assets', $asset, 'id');
// Update content row with new asset_id
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__content'))
->set($db->quoteName('asset_id') . ' = ' . (int) $asset->id)
->where($db->quoteName('id') . ' = ' . (int) $article['id'])
);
$db->execute();
}
// Rebuild asset tree again after inserts
try
{
$assetTable = \Joomla\CMS\Table\Table::getInstance('Asset');
if ($assetTable)
{
$assetTable->rebuild();
}
}
catch (\Throwable $e)
{
// Best effort
}
}
// ------------------------------------------------------------------
// Private helpers
// ------------------------------------------------------------------
private function dumpTable(string $logicalName, string $realName, string $dir, $db): void
{
$safeFileName = str_replace('#__', 'jml__', $logicalName);
$fp = fopen($dir . '/' . $safeFileName . '.sql', 'w');
$columns = $db->getTableColumns($realName, false);
$colNames = array_keys($columns);
$quotedCols = array_map([$db, 'quoteName'], $colNames);
$colList = implode(', ', $quotedCols);
$offset = 0;
while (true)
{
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName($realName))
->setLimit(self::BATCH_SIZE, $offset);
$db->setQuery($query);
$rows = $db->loadAssocList();
if (empty($rows))
{
break;
}
$values = [];
foreach ($rows as $row)
{
$vals = [];
foreach ($colNames as $col)
{
$vals[] = $row[$col] === null ? 'NULL' : $db->quote($row[$col]);
}
$values[] = '(' . implode(', ', $vals) . ')';
}
fwrite($fp, 'INSERT INTO ' . $db->quoteName($realName) . ' (' . $colList . ') VALUES ' . "\n" . implode(",\n", $values) . ";\n\n");
$offset += self::BATCH_SIZE;
if (count($rows) < self::BATCH_SIZE)
{
break;
}
}
fclose($fp);
}
private function restoreTable(string $sqlFile, $db, string $prefix): void
{
$baseName = basename($sqlFile, '.sql');
$realTable = str_replace('jml__', $prefix, $baseName);
$db->setQuery('TRUNCATE TABLE ' . $db->quoteName($realTable));
$db->execute();
$sql = file_get_contents($sqlFile);
if (empty(trim($sql)))
{
return;
}
$statements = array_filter(
array_map('trim', explode(";\n", $sql)),
function ($s) { return !empty($s) && $s !== ';'; }
);
foreach ($statements as $statement)
{
$statement = rtrim($statement, ';');
if (empty($statement))
{
continue;
}
$db->setQuery($statement);
$db->execute();
}
}
private function zipDirectory(string $sourceDir, string $zipPath): bool
{
$zip = new \ZipArchive();
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true)
{
return false;
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($sourceDir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item)
{
$rel = str_replace('\\', '/', substr($item->getPathname(), strlen($sourceDir) + 1));
$item->isDir() ? $zip->addEmptyDir($rel) : $zip->addFile($item->getPathname(), $rel);
}
$zip->close();
return true;
}
private function ensureSnapshotDir(): void
{
if (!is_dir($this->snapshotDir))
{
mkdir($this->snapshotDir, 0755, true);
}
if (!file_exists($this->snapshotDir . '/.htaccess'))
{
file_put_contents($this->snapshotDir . '/.htaccess', "Deny from all\n");
}
}
private function getSnapshotPath(string $name): string
{
return $this->snapshotDir . '/' . $name;
}
private function validateSnapshotName(string $name): void
{
if ($name === '' || strlen($name) > self::MAX_NAME_LENGTH || !preg_match('/^[a-zA-Z0-9_-]+$/', $name))
{
throw new \InvalidArgumentException('Invalid snapshot name');
}
}
private function removeDirectory(string $dir): void
{
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item)
{
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
}
@rmdir($dir);
}
private function clearDirectory(string $dir): void
{
if (!is_dir($dir)) return;
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item)
{
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
}
}
}
@@ -1 +0,0 @@
<!DOCTYPE html><title></title>
@@ -10,111 +10,109 @@
; Version: 02.01.08
; File: en-GB.override.ini
; Path: administrator/language/overrides/en-GB.override.ini
; Brief: Admin override TEMPLATE — placeholders resolved at runtime/install.
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
; Brief: Admin language overrides — values are hardcoded.
; -----------------------------------------------------------------------------
; ===== Footer & template branding =====
TPL_ATUM_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
TPL_ATUM_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Control panel greetings =====
COM_CPANEL_WELCOME_TITLE="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_MSG_WELCOME="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_WELCOME_TITLE="Welcome to MokoWaaS!"
COM_CPANEL_MSG_WELCOME="Welcome to MokoWaaS!"
; ===== Help/Docs phrasing =====
COM_ADMIN_HELP_SITE="{{BRAND_NAME}} Help"
COM_ADMIN_HELPSITE_FIELD_LABEL="{{BRAND_NAME}} Help"
COM_ADMIN_HELP_SITE="MokoWaaS Help"
COM_ADMIN_HELPSITE_FIELD_LABEL="MokoWaaS Help"
; ===== Generic replacements =====
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
COM_INSTALLER_TYPE_JOOMLA="{{BRAND_NAME}} Package"
LIB_JOOMLA="{{BRAND_NAME}} Library"
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
COM_INSTALLER_TYPE_JOOMLA="MokoWaaS Package"
LIB_JOOMLA="MokoWaaS Library"
; ===== System messages =====
JERROR_JOOMLA="{{BRAND_NAME}} Error"
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
JERROR_JOOMLA="MokoWaaS Error"
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
; ===== AdminLogin Support =====
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit {{COMPANY_NAME}}:"
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to {{COMPANY_NAME}} support on the login screen."
TPL_ATUM_BACKEND_LOGIN="{{BRAND_NAME}} Administrator Login"
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit Moko Consulting:"
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to Moko Consulting support on the login screen."
TPL_ATUM_BACKEND_LOGIN="MokoWaaS Administrator Login"
; ===== Error messages =====
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
; ===== Admin-specific branding =====
COM_ADMIN_VIEW_HOME_TITLE="{{BRAND_NAME}} Control Panel"
JLIB_APPLICATION_ERROR_SAVE_FAILED="{{BRAND_NAME}} Error: Save failed"
COM_ADMIN_VIEW_HOME_TITLE="MokoWaaS Control Panel"
JLIB_APPLICATION_ERROR_SAVE_FAILED="MokoWaaS Error: Save failed"
; ===== Module list workaround (RegularLabs) =====
COM_MODULES_HEADING_POSITION="Position"
; ===== Extensions =====
COM_INSTALLER_TYPE_TYPE_JOOMLA="{{BRAND_NAME}}"
COM_INSTALLER_TYPE_TYPE_JOOMLA="MokoWaaS"
COM_INSTALLER_MSG_UPDATE_SUCCESS="Update installed successfully"
; ===== Dashboard =====
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Documentation</a></li><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Support</a></li></ul>"
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in {{BRAND_NAME}}"
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to MokoWaaS!"
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Documentation</a></li><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Support</a></li></ul>"
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in MokoWaaS"
; ===== Quick Icons =====
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking {{BRAND_NAME}}…"
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown {{BRAND_NAME}}…"
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="{{BRAND_NAME}} is up to date."
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking MokoWaaS…"
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown MokoWaaS…"
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="MokoWaaS is up to date."
; ===== System Info =====
COM_ADMIN_JOOMLA_VERSION="{{BRAND_NAME}} Version"
COM_ADMIN_HELP="{{BRAND_NAME}} Help"
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="{{BRAND_NAME}} Backward Compatibility Plugin"
COM_ADMIN_JOOMLA_VERSION="MokoWaaS Version"
COM_ADMIN_HELP="MokoWaaS Help"
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="MokoWaaS Backward Compatibility Plugin"
; ===== Installer =====
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install {{BRAND_NAME}} Extension"
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The {{BRAND_NAME}} package cannot be installed through the Extension Manager. Please use the {{BRAND_NAME}} Update component to update."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The {{BRAND_NAME}} temporary folder is not set."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The {{BRAND_NAME}} temporary folder is not writable or does not exist."
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your {{BRAND_NAME}} installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install MokoWaaS Extension"
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The MokoWaaS package cannot be installed through the Extension Manager. Please use the MokoWaaS Update component to update."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The MokoWaaS temporary folder is not set."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The MokoWaaS temporary folder is not writable or does not exist."
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your MokoWaaS installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
; ===== Global Configuration =====
COM_CONFIG_FIELD_METAVERSION_LABEL="{{BRAND_NAME}} Version"
COM_CONFIG_FIELD_METAVERSION_LABEL="MokoWaaS Version"
; ===== Update component =====
COM_JOOMLAUPDATE_CONFIGURATION="{{BRAND_NAME}} Update: Options"
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="{{BRAND_NAME}} Next"
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where {{BRAND_NAME}} gets its update information from."
COM_JOOMLAUPDATE_CONFIGURATION="MokoWaaS Update: Options"
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="MokoWaaS Next"
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where MokoWaaS gets its update information from."
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_LABEL="Update Channel"
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="{{BRAND_NAME}} Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="{{BRAND_NAME}} Update Component"
COM_JOOMLAUPDATE_NOCHANGE="{{BRAND_NAME}} is up to date."
COM_JOOMLAUPDATE_PREUPDATE_CHECK="{{BRAND_NAME}} Pre-Update Check"
COM_JOOMLAUPDATE_UPDATE_HEADER="{{BRAND_NAME}} Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="MokoWaaS Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="MokoWaaS Update Component"
COM_JOOMLAUPDATE_NOCHANGE="MokoWaaS is up to date."
COM_JOOMLAUPDATE_PREUPDATE_CHECK="MokoWaaS Pre-Update Check"
COM_JOOMLAUPDATE_UPDATE_HEADER="MokoWaaS Update"
COM_JOOMLAUPDATE_LIVEUPDATE="Live Update"
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for {{BRAND_NAME}} updates."
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for MokoWaaS updates."
; ===== Privacy =====
COM_PRIVACY_HEADING_CORE_CAPABILITIES="{{BRAND_NAME}} Core Capabilities"
COM_PRIVACY_HEADING_CORE_CAPABILITIES="MokoWaaS Core Capabilities"
; ===== Database & Library errors =====
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum {{BRAND_NAME}} version requirement of J%s"
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find {{BRAND_NAME}} XML setup file."
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum MokoWaaS version requirement of J%s"
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find MokoWaaS XML setup file."
; ===== Version and About =====
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ADMIN_HELP_DOCUMENTATION="{{BRAND_NAME}} Documentation"
COM_ADMIN_HELP_SUPPORT="{{BRAND_NAME}} Support"
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
COM_ADMIN_HELP_DOCUMENTATION="MokoWaaS Documentation"
COM_ADMIN_HELP_SUPPORT="MokoWaaS Support"
; ===== Akeeba Ticket System (ATS) =====
COM_ATS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
COM_ATS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
@@ -10,111 +10,109 @@
; Version: 02.01.08
; File: en-US.override.ini
; Path: administrator/language/overrides/en-US.override.ini
; Brief: Admin override TEMPLATE — placeholders resolved at runtime/install.
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
; Brief: Admin language overrides — values are hardcoded.
; -----------------------------------------------------------------------------
; ===== Footer & template branding =====
TPL_ATUM_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
TPL_ATUM_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Control panel greetings =====
COM_CPANEL_WELCOME_TITLE="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_MSG_WELCOME="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_WELCOME_TITLE="Welcome to MokoWaaS!"
COM_CPANEL_MSG_WELCOME="Welcome to MokoWaaS!"
; ===== Help/Docs phrasing =====
COM_ADMIN_HELP_SITE="{{BRAND_NAME}} Help"
COM_ADMIN_HELPSITE_FIELD_LABEL="{{BRAND_NAME}} Help"
COM_ADMIN_HELP_SITE="MokoWaaS Help"
COM_ADMIN_HELPSITE_FIELD_LABEL="MokoWaaS Help"
; ===== Generic replacements =====
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
COM_INSTALLER_TYPE_JOOMLA="{{BRAND_NAME}} Package"
LIB_JOOMLA="{{BRAND_NAME}} Library"
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
COM_INSTALLER_TYPE_JOOMLA="MokoWaaS Package"
LIB_JOOMLA="MokoWaaS Library"
; ===== System messages =====
JERROR_JOOMLA="{{BRAND_NAME}} Error"
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
JERROR_JOOMLA="MokoWaaS Error"
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
; ===== AdminLogin Support =====
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit {{COMPANY_NAME}}:"
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to {{COMPANY_NAME}} support on the login screen."
TPL_ATUM_BACKEND_LOGIN="{{BRAND_NAME}} Administrator Login"
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit Moko Consulting:"
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to Moko Consulting support on the login screen."
TPL_ATUM_BACKEND_LOGIN="MokoWaaS Administrator Login"
; ===== Error messages =====
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
; ===== Admin-specific branding =====
COM_ADMIN_VIEW_HOME_TITLE="{{BRAND_NAME}} Control Panel"
JLIB_APPLICATION_ERROR_SAVE_FAILED="{{BRAND_NAME}} Error: Save failed"
COM_ADMIN_VIEW_HOME_TITLE="MokoWaaS Control Panel"
JLIB_APPLICATION_ERROR_SAVE_FAILED="MokoWaaS Error: Save failed"
; ===== Module list workaround (RegularLabs) =====
COM_MODULES_HEADING_POSITION="Position"
; ===== Extensions =====
COM_INSTALLER_TYPE_TYPE_JOOMLA="{{BRAND_NAME}}"
COM_INSTALLER_TYPE_TYPE_JOOMLA="MokoWaaS"
COM_INSTALLER_MSG_UPDATE_SUCCESS="Update installed successfully"
; ===== Dashboard =====
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Documentation</a></li><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Support</a></li></ul>"
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in {{BRAND_NAME}}"
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to MokoWaaS!"
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Documentation</a></li><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Support</a></li></ul>"
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in MokoWaaS"
; ===== Quick Icons =====
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking {{BRAND_NAME}}…"
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown {{BRAND_NAME}}…"
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="{{BRAND_NAME}} is up to date."
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking MokoWaaS…"
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown MokoWaaS…"
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="MokoWaaS is up to date."
; ===== System Info =====
COM_ADMIN_JOOMLA_VERSION="{{BRAND_NAME}} Version"
COM_ADMIN_HELP="{{BRAND_NAME}} Help"
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="{{BRAND_NAME}} Backward Compatibility Plugin"
COM_ADMIN_JOOMLA_VERSION="MokoWaaS Version"
COM_ADMIN_HELP="MokoWaaS Help"
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="MokoWaaS Backward Compatibility Plugin"
; ===== Installer =====
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install {{BRAND_NAME}} Extension"
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The {{BRAND_NAME}} package cannot be installed through the Extension Manager. Please use the {{BRAND_NAME}} Update component to update."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The {{BRAND_NAME}} temporary folder is not set."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The {{BRAND_NAME}} temporary folder is not writable or does not exist."
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your {{BRAND_NAME}} installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install MokoWaaS Extension"
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The MokoWaaS package cannot be installed through the Extension Manager. Please use the MokoWaaS Update component to update."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The MokoWaaS temporary folder is not set."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The MokoWaaS temporary folder is not writable or does not exist."
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your MokoWaaS installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
; ===== Global Configuration =====
COM_CONFIG_FIELD_METAVERSION_LABEL="{{BRAND_NAME}} Version"
COM_CONFIG_FIELD_METAVERSION_LABEL="MokoWaaS Version"
; ===== Update component =====
COM_JOOMLAUPDATE_CONFIGURATION="{{BRAND_NAME}} Update: Options"
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="{{BRAND_NAME}} Next"
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where {{BRAND_NAME}} gets its update information from."
COM_JOOMLAUPDATE_CONFIGURATION="MokoWaaS Update: Options"
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="MokoWaaS Next"
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where MokoWaaS gets its update information from."
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_LABEL="Update Channel"
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="{{BRAND_NAME}} Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="{{BRAND_NAME}} Update Component"
COM_JOOMLAUPDATE_NOCHANGE="{{BRAND_NAME}} is up to date."
COM_JOOMLAUPDATE_PREUPDATE_CHECK="{{BRAND_NAME}} Pre-Update Check"
COM_JOOMLAUPDATE_UPDATE_HEADER="{{BRAND_NAME}} Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="MokoWaaS Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="MokoWaaS Update Component"
COM_JOOMLAUPDATE_NOCHANGE="MokoWaaS is up to date."
COM_JOOMLAUPDATE_PREUPDATE_CHECK="MokoWaaS Pre-Update Check"
COM_JOOMLAUPDATE_UPDATE_HEADER="MokoWaaS Update"
COM_JOOMLAUPDATE_LIVEUPDATE="Live Update"
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for {{BRAND_NAME}} updates."
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for MokoWaaS updates."
; ===== Privacy =====
COM_PRIVACY_HEADING_CORE_CAPABILITIES="{{BRAND_NAME}} Core Capabilities"
COM_PRIVACY_HEADING_CORE_CAPABILITIES="MokoWaaS Core Capabilities"
; ===== Database & Library errors =====
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum {{BRAND_NAME}} version requirement of J%s"
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find {{BRAND_NAME}} XML setup file."
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum MokoWaaS version requirement of J%s"
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find MokoWaaS XML setup file."
; ===== Version and About =====
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ADMIN_HELP_DOCUMENTATION="{{BRAND_NAME}} Documentation"
COM_ADMIN_HELP_SUPPORT="{{BRAND_NAME}} Support"
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
COM_ADMIN_HELP_DOCUMENTATION="MokoWaaS Documentation"
COM_ADMIN_HELP_SUPPORT="MokoWaaS Support"
; ===== Akeeba Ticket System (ATS) =====
COM_ATS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
COM_ATS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<field name="url" type="url"
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC"
required="true" hint="https://client.example.com" />
<field name="token" type="text"
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC"
required="true" hint="health_api_token from target site" />
<field name="label" type="text"
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC"
hint="e.g. Client A" />
</form>
@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<field
name="ip"
type="text"
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC"
required="true"
hint="e.g. 192.168.1.100 or 10.0.0.0/24"
/>
<field
name="label"
type="text"
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC"
hint="e.g. Office network"
/>
<field
name="enabled"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</form>
@@ -7,180 +7,20 @@
; FILE INFORMATION
; Defgroup: Joomla Language
; Ingroup: MokoWaaS
; Version: 02.01.08
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} used in override templates
; File: plg_system_mokowaas.ini
; Path: /src/language/en-GB/plg_system_mokowaas.ini
; Brief: English language strings for MokoWaaS system plugin
; Notes: Contains translatable strings for plugin functionality
; Variables: (none)
; Brief: English language strings for MokoWaaS core system plugin
; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations."
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_LABEL="Enable Branding"
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_DESC="Enable or disable the branding overrides across the system."
; ===== Core fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_LABEL="Core"
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_DESC="Heartbeat token for health monitoring and Grafana integration."
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_LABEL="Brand Name"
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_DESC="The brand name that replaces 'Joomla' throughout the interface. Used in all language overrides."
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_LABEL="Company Name"
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_DESC="Your company name, used in support links and footer text."
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL="Support URL"
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC="URL for support and documentation links."
; ===== WaaS Access fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL="WaaS Access Control"
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC="Master user enforcement and emergency access settings for the WaaS operator."
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL="Enforce Master User"
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC="Ensure the master super admin account always exists. If deleted, it will be recreated on next admin page load."
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL="Master Username"
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC="Username for the persistent WaaS super admin account."
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL="Master Email"
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC="Email address for the master super admin account."
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL="Emergency Access"
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC="Allow login using database credentials as a two-factor emergency access method. Requires server file access to confirm."
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_SUCCESS="Emergency access LOGIN by {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_BLOCKED_IP="Emergency access BLOCKED (unauthorized IP) — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_WRONG_PASSWORD="Emergency access FAILED (wrong password) — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_VERIFY_FILE_CREATED="Emergency access verification file created — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_PENDING_FILE_DELETE="Emergency access pending file deletion — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_LABEL="IP Whitelist"
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_DESC="Emergency access requires an IP whitelist. Set <code>public $mokowaas_allowed_ips = '1.2.3.4,5.6.7.8';</code> in configuration.php. Emergency access is BLOCKED if no IPs are configured."
; ===== Maintenance fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_LABEL="Maintenance"
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_DESC="One-time maintenance actions. Set to Yes and save to execute. Resets to No automatically after execution."
PLG_SYSTEM_MOKOWAAS_DEV_MODE_LABEL="Development Mode"
PLG_SYSTEM_MOKOWAAS_DEV_MODE_DESC="Disables all Joomla caching at runtime. Useful during development and testing. Does not modify configuration.php."
PLG_SYSTEM_MOKOWAAS_RESET_HITS_LABEL="Reset All Hits"
PLG_SYSTEM_MOKOWAAS_RESET_HITS_DESC="Set all article hit counters to zero across the site. This action executes on save and resets to No."
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_LABEL="Delete All Versions"
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_DESC="Purge all content version history from the database. This action executes on save and resets to No."
; ===== Visual Branding fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_LABEL="Visual Branding"
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_DESC="Admin color scheme and CSS injection. Logos and favicon are shipped in the plugin media folder."
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_LABEL="Logos & Favicon"
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_DESC="Logos and favicon are automatically applied from the plugin media folder (<code>/media/plg_system_mokowaas/</code>). Replace <code>logo.png</code>, <code>favicon.ico</code>, and <code>favicon_256.png</code> to change them."
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_LABEL="Primary Color"
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_DESC="Main accent color used in the admin template header and buttons."
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_LABEL="Sidebar Color"
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_DESC="Background color for the admin sidebar navigation."
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_LABEL="Header Color"
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_DESC="Background color for the admin top header bar."
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_LABEL="Link Color"
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_DESC="Color for hyperlinks in the admin interface."
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_LABEL="Brand Icon (FontAwesome)"
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_DESC="FontAwesome unicode codepoint for the brand icon that replaces the Joomla logo icon. Enter the hex code only (e.g. f6d5 for fa-hat-cowboy). Find codes at fontawesome.com/icons."
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_LABEL="Custom CSS"
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_DESC="Additional CSS injected into admin pages. Use for fine-tuning visual presentation."
; ===== Tenant Restrictions fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_LABEL="Tenant Restrictions"
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC="Restrict admin features for non-master users. Master user always has full access."
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_LABEL="Restrict Extension Installer"
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_DESC="Block non-master users from installing or removing extensions."
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL="Allow Extension Updates"
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC="When the installer is restricted, still allow non-master users to update extensions."
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL="Hide System Information"
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC="Block non-master users from viewing PHP, database, and server information."
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_LABEL="Restrict Global Configuration"
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_DESC="Block non-master users from changing Global Configuration. Component config is still accessible."
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_LABEL="Restrict Template Code Editing"
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_DESC="Block non-master users from editing template source code. Template styles remain accessible."
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_LABEL="Disable Install from URL"
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_DESC="Block installing extensions from URL for ALL users (including master) as a safety measure."
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_LABEL="Hidden Menu Items"
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC="Components to hide from admin menu for non-master users. One per line (e.g., com_installer)."
; ===== Content Sync fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_LABEL="Content Sync"
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_DESC="One-way content push to remote MokoWaaS sites. Syncs articles, categories, menus, and modules by alias."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_LABEL="Sync Targets"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_DESC="Remote sites to push content to. Each target requires the site URL and that site's health API token."
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_LABEL="Push Content Now"
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_DESC="Set to Yes and save to immediately push all content to all configured targets. Resets to No automatically."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL="Site URL"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC="Full URL of the remote Joomla site (e.g. https://client.example.com)."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL="API Token"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC="The heartbeat token from the remote site's MokoWaaS plugin settings."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC="Friendly name for this target (for identification only)."
; ===== Diagnostics fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_LABEL="Diagnostics & Monitoring"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_DESC="Health check endpoint for external monitoring systems (e.g. Grafana). Exposes system status via a token-authenticated JSON API."
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_LABEL="Enable Health Endpoint"
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_DESC="Expose a JSON health check endpoint at <code>/?mokowaas=health</code>. Requires a valid API token. A random token is generated automatically when enabled."
; ===== Diagnostics =====
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Heartbeat Token"
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC="Auto-generated bearer token for the health endpoint. Use this token in your Grafana datasource configuration. Send as <code>Authorization: Bearer &lt;token&gt;</code> header or <code>&amp;token=&lt;value&gt;</code> query parameter."
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_LABEL="Grafana URL"
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_DESC="Base URL of your Grafana instance (e.g. <code>https://grafana.example.com</code>). When provided along with an API key, the plugin will auto-provision a datasource and dashboard in Grafana when the health endpoint is enabled."
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_LABEL="Grafana API Key"
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_DESC="Service account token or API key with Editor role in Grafana. Required for auto-provisioning the MokoWaaS datasource and dashboard."
; ===== Security fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL="Security Hardening"
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC="HTTPS enforcement, session timeouts, password policy, and upload restrictions."
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL="Force HTTPS"
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL="Trusted IPs (No Session Timeout)"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC="Sessions from these IP addresses or ranges will never time out. Supports exact IPs, CIDR notation (e.g. 10.0.0.0/24), and wildcards (e.g. 192.168.1.*)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL="IP / CIDR"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC="An IP address, CIDR range, or wildcard pattern."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC="A descriptive label for this entry (e.g. Office, VPN)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL="Enabled"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
PLG_SYSTEM_MOKOWAAS_PASSWORD_NUMBER_LABEL="Require Number"
PLG_SYSTEM_MOKOWAAS_PASSWORD_SPECIAL_LABEL="Require Special Character"
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_LABEL="Allowed Upload Types"
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file extensions for media uploads."
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)"
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
; ===== Demo Mode fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL="Demo Mode"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC="Configure demo site behavior with baseline snapshots and automatic periodic reset. When enabled, a warning banner is shown on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_LABEL="Enable Demo Mode"
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_DESC="When enabled, shows a warning banner on the frontend and enables snapshot/restore functionality."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_LABEL="Banner Message"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_DESC="Message displayed in the demo warning banner on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_LABEL="Banner Color"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_DESC="Background color for the demo warning banner."
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_LABEL="Show Reset Countdown"
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_DESC="Display a countdown timer in the banner showing time until the next scheduled reset."
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_LABEL="Reset Schedule"
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_DESC="How often the demo site resets. Select a preset or choose Custom to enter a crontab expression."
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_LABEL="Custom Crontab"
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_DESC="Crontab expression for the reset schedule. Format: minute hour day month weekday (e.g. 0 */6 * * * for every 6 hours)."
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_LABEL="Next Scheduled Reset"
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_DESC="Calculated automatically from the reset schedule. The banner countdown uses this timestamp."
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Directories"
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Select which directories to include in the snapshot. Images contains uploaded media, Media contains extension assets."
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_DESC="Save the current site state as a baseline snapshot. Uses the Active Baseline Name above. Resets to No after execution."
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_LABEL="Restore Baseline Now"
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_DESC="Immediately restore the site to the active baseline snapshot. WARNING: This will overwrite current content. Resets to No after execution."
; ===== Site Aliases fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
@@ -7,180 +7,20 @@
; FILE INFORMATION
; Defgroup: Joomla Language
; Ingroup: MokoWaaS
; Version: 02.01.08
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} used in override templates
; File: plg_system_mokowaas.ini
; Path: /src/language/en-GB/plg_system_mokowaas.ini
; Brief: English language strings for MokoWaaS system plugin
; Notes: Contains translatable strings for plugin functionality
; Variables: (none)
; Brief: English language strings for MokoWaaS core system plugin
; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations."
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_LABEL="Enable Branding"
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_DESC="Enable or disable the branding overrides across the system."
; ===== Core fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_LABEL="Core"
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_DESC="Heartbeat token for health monitoring and Grafana integration."
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_LABEL="Brand Name"
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_DESC="The brand name that replaces 'Joomla' throughout the interface. Used in all language overrides."
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_LABEL="Company Name"
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_DESC="Your company name, used in support links and footer text."
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL="Support URL"
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC="URL for support and documentation links."
; ===== WaaS Access fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL="WaaS Access Control"
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC="Master user enforcement and emergency access settings for the WaaS operator."
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL="Enforce Master User"
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC="Ensure the master super admin account always exists. If deleted, it will be recreated on next admin page load."
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL="Master Username"
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC="Username for the persistent WaaS super admin account."
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL="Master Email"
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC="Email address for the master super admin account."
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL="Emergency Access"
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC="Allow login using database credentials as a two-factor emergency access method. Requires server file access to confirm."
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_SUCCESS="Emergency access LOGIN by {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_BLOCKED_IP="Emergency access BLOCKED (unauthorized IP) — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_WRONG_PASSWORD="Emergency access FAILED (wrong password) — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_VERIFY_FILE_CREATED="Emergency access verification file created — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_PENDING_FILE_DELETE="Emergency access pending file deletion — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_LABEL="IP Whitelist"
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_DESC="Emergency access requires an IP whitelist. Set <code>public $mokowaas_allowed_ips = '1.2.3.4,5.6.7.8';</code> in configuration.php. Emergency access is BLOCKED if no IPs are configured."
; ===== Maintenance fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_LABEL="Maintenance"
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_DESC="One-time maintenance actions. Set to Yes and save to execute. Resets to No automatically after execution."
PLG_SYSTEM_MOKOWAAS_DEV_MODE_LABEL="Development Mode"
PLG_SYSTEM_MOKOWAAS_DEV_MODE_DESC="Disables all Joomla caching at runtime. Useful during development and testing. Does not modify configuration.php."
PLG_SYSTEM_MOKOWAAS_RESET_HITS_LABEL="Reset All Hits"
PLG_SYSTEM_MOKOWAAS_RESET_HITS_DESC="Set all article hit counters to zero across the site. This action executes on save and resets to No."
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_LABEL="Delete All Versions"
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_DESC="Purge all content version history from the database. This action executes on save and resets to No."
; ===== Visual Branding fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_LABEL="Visual Branding"
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_DESC="Admin color scheme and CSS injection. Logos and favicon are shipped in the plugin media folder."
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_LABEL="Logos & Favicon"
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_DESC="Logos and favicon are automatically applied from the plugin media folder (<code>/media/plg_system_mokowaas/</code>). Replace <code>logo.png</code>, <code>favicon.ico</code>, and <code>favicon_256.png</code> to change them."
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_LABEL="Primary Color"
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_DESC="Main accent color used in the admin template header and buttons."
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_LABEL="Sidebar Color"
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_DESC="Background color for the admin sidebar navigation."
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_LABEL="Header Color"
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_DESC="Background color for the admin top header bar."
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_LABEL="Link Color"
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_DESC="Color for hyperlinks in the admin interface."
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_LABEL="Brand Icon (FontAwesome)"
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_DESC="FontAwesome unicode codepoint for the brand icon that replaces the Joomla logo icon. Enter the hex code only (e.g. f6d5 for fa-hat-cowboy). Find codes at fontawesome.com/icons."
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_LABEL="Custom CSS"
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_DESC="Additional CSS injected into admin pages. Use for fine-tuning visual presentation."
; ===== Tenant Restrictions fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_LABEL="Tenant Restrictions"
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC="Restrict admin features for non-master users. Master user always has full access."
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_LABEL="Restrict Extension Installer"
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_DESC="Block non-master users from installing or removing extensions."
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL="Allow Extension Updates"
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC="When the installer is restricted, still allow non-master users to update extensions."
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL="Hide System Information"
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC="Block non-master users from viewing PHP, database, and server information."
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_LABEL="Restrict Global Configuration"
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_DESC="Block non-master users from changing Global Configuration. Component config is still accessible."
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_LABEL="Restrict Template Code Editing"
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_DESC="Block non-master users from editing template source code. Template styles remain accessible."
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_LABEL="Disable Install from URL"
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_DESC="Block installing extensions from URL for ALL users (including master) as a safety measure."
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_LABEL="Hidden Menu Items"
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC="Components to hide from admin menu for non-master users. One per line (e.g., com_installer)."
; ===== Content Sync fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_LABEL="Content Sync"
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_DESC="One-way content push to remote MokoWaaS sites. Syncs articles, categories, menus, and modules by alias."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_LABEL="Sync Targets"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_DESC="Remote sites to push content to. Each target requires the site URL and that site's health API token."
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_LABEL="Push Content Now"
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_DESC="Set to Yes and save to immediately push all content to all configured targets. Resets to No automatically."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL="Site URL"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC="Full URL of the remote Joomla site (e.g. https://client.example.com)."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL="API Token"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC="The heartbeat token from the remote site's MokoWaaS plugin settings."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC="Friendly name for this target (for identification only)."
; ===== Diagnostics fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_LABEL="Diagnostics & Monitoring"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_DESC="Health check endpoint for external monitoring systems (e.g. Grafana). Exposes system status via a token-authenticated JSON API."
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_LABEL="Enable Health Endpoint"
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_DESC="Expose a JSON health check endpoint at <code>/?mokowaas=health</code>. Requires a valid API token. A random token is generated automatically when enabled."
; ===== Diagnostics =====
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Heartbeat Token"
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC="Auto-generated bearer token for the health endpoint. Use this token in your Grafana datasource configuration. Send as <code>Authorization: Bearer &lt;token&gt;</code> header or <code>&amp;token=&lt;value&gt;</code> query parameter."
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_LABEL="Grafana URL"
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_DESC="Base URL of your Grafana instance (e.g. <code>https://grafana.example.com</code>). When provided along with an API key, the plugin will auto-provision a datasource and dashboard in Grafana when the health endpoint is enabled."
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_LABEL="Grafana API Key"
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_DESC="Service account token or API key with Editor role in Grafana. Required for auto-provisioning the MokoWaaS datasource and dashboard."
; ===== Security fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL="Security Hardening"
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC="HTTPS enforcement, session timeouts, password policy, and upload restrictions."
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL="Force HTTPS"
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL="Trusted IPs (No Session Timeout)"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC="Sessions from these IP addresses or ranges will never time out. Supports exact IPs, CIDR notation (e.g. 10.0.0.0/24), and wildcards (e.g. 192.168.1.*)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL="IP / CIDR"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC="An IP address, CIDR range, or wildcard pattern."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC="A descriptive label for this entry (e.g. Office, VPN)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL="Enabled"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
PLG_SYSTEM_MOKOWAAS_PASSWORD_NUMBER_LABEL="Require Number"
PLG_SYSTEM_MOKOWAAS_PASSWORD_SPECIAL_LABEL="Require Special Character"
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_LABEL="Allowed Upload Types"
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file extensions for media uploads."
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)"
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
; ===== Demo Mode fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL="Demo Mode"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC="Configure demo site behavior with baseline snapshots and automatic periodic reset. When enabled, a warning banner is shown on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_LABEL="Enable Demo Mode"
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_DESC="When enabled, shows a warning banner on the frontend and enables snapshot/restore functionality."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_LABEL="Banner Message"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_DESC="Message displayed in the demo warning banner on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_LABEL="Banner Color"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_DESC="Background color for the demo warning banner."
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_LABEL="Show Reset Countdown"
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_DESC="Display a countdown timer in the banner showing time until the next scheduled reset."
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_LABEL="Reset Schedule"
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_DESC="How often the demo site resets. Select a preset or choose Custom to enter a crontab expression."
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_LABEL="Custom Crontab"
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_DESC="Crontab expression for the reset schedule. Format: minute hour day month weekday (e.g. 0 */6 * * * for every 6 hours)."
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_LABEL="Next Scheduled Reset"
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_DESC="Calculated automatically from the reset schedule. The banner countdown uses this timestamp."
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Directories"
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Select which directories to include in the snapshot. Images contains uploaded media, Media contains extension assets."
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_DESC="Save the current site state as a baseline snapshot. Uses the Active Baseline Name above. Resets to No after execution."
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_LABEL="Restore Baseline Now"
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_DESC="Immediately restore the site to the active baseline snapshot. WARNING: This will overwrite current content. Resets to No after execution."
; ===== Site Aliases fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
@@ -10,38 +10,36 @@
; Version: 02.01.08
; File: en-GB.override.ini
; Path: language/overrides/en-GB.override.ini
; Brief: Site/frontend override TEMPLATE — placeholders resolved at runtime/install.
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
; Brief: Site/frontend language overrides — values are hardcoded.
; -----------------------------------------------------------------------------
; ===== Footer & template branding =====
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Generic replacements =====
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
LIB_JOOMLA="{{BRAND_NAME}} Library"
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
LIB_JOOMLA="MokoWaaS Library"
; ===== System messages =====
JERROR_JOOMLA="{{BRAND_NAME}} Error"
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
JERROR_JOOMLA="MokoWaaS Error"
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
; ===== Error messages =====
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
; ===== Installer / Sample data =====
INSTL_SITE_NAME_LABEL="{{BRAND_NAME}} Site Name"
INSTL_SAMPLE_BLOG_SET="{{BRAND_NAME}} Sample Data - Blog"
INSTL_SAMPLE_BROCHURE_SET="{{BRAND_NAME}} Sample Data - Brochure Site"
INSTL_SAMPLE_DATA_SET="{{BRAND_NAME}} Sample Data - Default"
INSTL_SAMPLE_LEARN_SET="{{BRAND_NAME}} Sample Data - Learn"
INSTL_SAMPLE_TESTING_SET="{{BRAND_NAME}} Sample Data - Testing"
INSTL_SITE_NAME_LABEL="MokoWaaS Site Name"
INSTL_SAMPLE_BLOG_SET="MokoWaaS Sample Data - Blog"
INSTL_SAMPLE_BROCHURE_SET="MokoWaaS Sample Data - Brochure Site"
INSTL_SAMPLE_DATA_SET="MokoWaaS Sample Data - Default"
INSTL_SAMPLE_LEARN_SET="MokoWaaS Sample Data - Learn"
INSTL_SAMPLE_TESTING_SET="MokoWaaS Sample Data - Testing"
; ===== Login support =====
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
; ===== Site offline =====
JOFFLINE_MESSAGE="This site is down for maintenance.<br>Please check back again soon."
@@ -52,15 +50,15 @@ JERROR_AN_ERROR_HAS_OCCURRED="An error has occurred."
JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND="Component not found."
; ===== Version and About =====
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Akeeba Ticket System (ATS) =====
COM_ATS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
COM_ATS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
@@ -10,38 +10,36 @@
; Version: 02.01.08
; File: en-US.override.ini
; Path: language/overrides/en-US.override.ini
; Brief: Site/frontend override TEMPLATE — placeholders resolved at runtime/install.
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
; Brief: Site/frontend language overrides — values are hardcoded.
; -----------------------------------------------------------------------------
; ===== Footer & template branding =====
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Generic replacements =====
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
LIB_JOOMLA="{{BRAND_NAME}} Library"
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
LIB_JOOMLA="MokoWaaS Library"
; ===== System messages =====
JERROR_JOOMLA="{{BRAND_NAME}} Error"
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
JERROR_JOOMLA="MokoWaaS Error"
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
; ===== Error messages =====
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
; ===== Installer / Sample data =====
INSTL_SITE_NAME_LABEL="{{BRAND_NAME}} Site Name"
INSTL_SAMPLE_BLOG_SET="{{BRAND_NAME}} Sample Data - Blog"
INSTL_SAMPLE_BROCHURE_SET="{{BRAND_NAME}} Sample Data - Brochure Site"
INSTL_SAMPLE_DATA_SET="{{BRAND_NAME}} Sample Data - Default"
INSTL_SAMPLE_LEARN_SET="{{BRAND_NAME}} Sample Data - Learn"
INSTL_SAMPLE_TESTING_SET="{{BRAND_NAME}} Sample Data - Testing"
INSTL_SITE_NAME_LABEL="MokoWaaS Site Name"
INSTL_SAMPLE_BLOG_SET="MokoWaaS Sample Data - Blog"
INSTL_SAMPLE_BROCHURE_SET="MokoWaaS Sample Data - Brochure Site"
INSTL_SAMPLE_DATA_SET="MokoWaaS Sample Data - Default"
INSTL_SAMPLE_LEARN_SET="MokoWaaS Sample Data - Learn"
INSTL_SAMPLE_TESTING_SET="MokoWaaS Sample Data - Testing"
; ===== Login support =====
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
; ===== Site offline =====
JOFFLINE_MESSAGE="This site is down for maintenance.<br>Please check back again soon."
@@ -52,15 +50,15 @@ JERROR_AN_ERROR_HAS_OCCURRED="An error has occurred."
JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND="Component not found."
; ===== Version and About =====
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Akeeba Ticket System (ATS) =====
COM_ATS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
COM_ATS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

+3 -48
View File
@@ -16,7 +16,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.00
VERSION: 02.34.11
PATH: /src/mokowaas.xml
BRIEF: Plugin manifest for MokoWaaS system plugin
NOTE: Defines installation metadata, files, and configuration for Joomla
@@ -30,8 +30,8 @@
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<description>MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations.</description>
<version>02.34.11</version>
<description>MokoWaaS core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description>
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
<scriptfile>script.php</scriptfile>
@@ -40,7 +40,6 @@
<folder>Extension</folder>
<folder>Field</folder>
<folder>Helper</folder>
<folder>Service</folder>
<folder>forms</folder>
<folder>payload</folder>
<folder>services</folder>
@@ -48,14 +47,6 @@
<folder>administrator</folder>
</files>
<media destination="plg_system_mokowaas" folder="media">
<filename>index.html</filename>
<filename>favicon.ico</filename>
<filename>favicon.svg</filename>
<filename>favicon_256.png</filename>
<filename>logo.png</filename>
</media>
<languages folder="language">
<language tag="en-GB">en-GB/plg_system_mokowaas.ini</language>
<language tag="en-US">en-US/plg_system_mokowaas.ini</language>
@@ -89,42 +80,6 @@
readonly="true"
/>
</fieldset>
<fieldset name="demo_mode"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
>
<field name="demo_scheduled_task" type="DemoTaskInfo"
label="PLG_SYSTEM_MOKOWAAS_DEMO_TASK_INFO_LABEL"
/>
</fieldset>
<fieldset name="security"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
>
<field
name="emergency_access"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="allowed_ips_display"
type="AllowedIps"
label=""
/>
<field
name="current_ip_display"
type="CurrentIp"
label=""
/>
</fieldset>
</fields>
</config>
</extension>
+1 -1
View File
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
* VERSION: 02.34.08
* VERSION: 02.34.11
* PATH: /src/script.php
* BRIEF: Installation script for MokoWaaS plugin
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
* VERSION: 02.34.08
* VERSION: 02.34.11
* PATH: /src/services/provider.php
* BRIEF: Service provider for dependency injection in Joomla 5.x
* NOTE: Registers the plugin with Joomla's DI container
@@ -13,3 +13,5 @@ PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_HITS_LABEL="Reset All Hits"
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_HITS_DESC="One-shot: reset article hit counters on save. Automatically turns off after execution."
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DELETE_VERSIONS_LABEL="Delete All Versions"
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DELETE_VERSIONS_DESC="One-shot: delete all content version history on save. Automatically turns off after execution."
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_LABEL="Reset Download Keys"
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_DESC="One-shot: clear all download keys (dlid) from update sites on save. Automatically turns off after execution."
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSDevTools</namespace>
@@ -52,6 +52,14 @@
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="reset_download_keys" type="radio" default="0"
label="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
</fields>
</config>
@@ -111,6 +111,13 @@ class DevTools extends CMSPlugin implements SubscriberInterface
$params->set('delete_versions', 0);
}
// Reset download keys on save if toggled on
if ($params->get('reset_download_keys', 0))
{
$this->resetDownloadKeys();
$params->set('reset_download_keys', 0);
}
// Reset the one-shot toggles
if ($table->params !== $params->toString())
{
@@ -152,4 +159,41 @@ class DevTools extends CMSPlugin implements SubscriberInterface
return $count;
}
private function resetDownloadKeys(): int
{
$db = Factory::getDbo();
// Find update sites that have a dlid in extra_query
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')])
->from($db->quoteName('#__update_sites'))
->where($db->quoteName('extra_query') . ' LIKE ' . $db->quote('%dlid=%'))
);
$sites = $db->loadObjectList();
$count = 0;
foreach ($sites as $site)
{
// Parse the query string, remove dlid, rebuild
parse_str($site->extra_query, $parsed);
unset($parsed['dlid']);
$newQuery = http_build_query($parsed);
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('extra_query') . ' = ' . $db->quote($newQuery))
->where($db->quoteName('update_site_id') . ' = ' . (int) $site->update_site_id)
)->execute();
$count++;
}
$this->getApplication()->enqueueMessage(\sprintf('Cleared download keys from %d update sites.', $count), 'message');
return $count;
}
}
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSFirewall</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>PLG_SYSTEM_MOKOWAAS_MONITOR_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSMonitor</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>PLG_SYSTEM_MOKOWAAS_OFFLINE_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSOffline</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>PLG_SYSTEM_MOKOWAAS_TENANT_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSTenant</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions.</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSTickets</namespace>
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>PLG_TASK_MOKOWAASDEMO_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSDemo</namespace>
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>PLG_TASK_MOKOWAASSYNC_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSSync</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.</description>
<namespace path="src">Moko\Plugin\WebServices\MokoWaaS</namespace>
<files>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<description>Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds.</description>
<namespace path="src">Moko\Plugin\WebServices\PerfectPublisher</namespace>
<files>
@@ -8,7 +8,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_webservices_perfectpublisher/services/provider.php
* VERSION: 02.34.08
* VERSION: 02.34.11
* BRIEF: DI service provider for Perfect Publisher Web Services plugin
*/
@@ -8,7 +8,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
* VERSION: 02.34.08
* VERSION: 02.34.11
* BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet)
*/
+2 -1
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoWaaS</name>
<packagename>mokowaas</packagename>
<version>02.34.08-dev</version>
<version>02.34.11</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -24,6 +24,7 @@
<file type="module" id="mod_mokowaas_cpanel" client="administrator">mod_mokowaas_cpanel.zip</file>
<file type="module" id="mod_mokowaas_menu" client="administrator">mod_mokowaas_menu.zip</file>
<file type="module" id="mod_mokowaas_cache" client="administrator">mod_mokowaas_cache.zip</file>
<file type="module" id="mod_mokowaas_categories" client="administrator">mod_mokowaas_categories.zip</file>
<file type="plugin" id="plg_webservices_mokowaas" group="webservices">plg_webservices_mokowaas.zip</file>
<file type="plugin" id="plg_webservices_perfectpublisher" group="webservices">plg_webservices_perfectpublisher.zip</file>
<file type="plugin" id="plg_task_mokowaasdemo" group="task">plg_task_mokowaasdemo.zip</file>
+4 -5
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: 02.34.08-dev
VERSION: 02.34.11-dev
-->
<updates>
@@ -11,13 +11,12 @@
<element>pkg_mokowaas</element>
<type>package</type>
<client>site</client>
<version>02.34.02-dev</version>
<creationDate>2026-06-04</creationDate>
<version>02.34.11-dev</version>
<creationDate>2026-06-06</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.34.02-dev.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.34.11-dev.zip</downloadurl>
</downloads>
<sha256>16cd0c7cef22b6f260fb921767a841839d7060cd39fb11b402e9a96f217e7810</sha256>
<tags><tag>dev</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>