Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b170894228 | |||
| 082fa0798c | |||
| d1ee2ef3f4 | |||
| 7f9b59a36d | |||
| 79047e37b5 | |||
| 3d5f9346c6 | |||
| 93c82a9cee | |||
| 384b8824c6 | |||
| e01791ae68 | |||
| e42d6e7596 |
@@ -1,4 +0,0 @@
|
||||
[submodule "src/packages/tpl_mokoonyx"]
|
||||
path = src/packages/tpl_mokoonyx
|
||||
url = https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
|
||||
branch = main
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
-->
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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">— 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>
|
||||
+58
-60
@@ -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>"
|
||||
|
||||
+58
-60
@@ -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 <token></code> header or <code>&token=<value></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 <token></code> header or <code>&token=<value></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 |
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
@@ -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)
|
||||
*/
|
||||
|
||||
|
||||
Submodule src/packages/tpl_mokoonyx deleted from f3897495ad
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user