diff --git a/README.md b/README.md
index f400e11..fadf282 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
- VERSION: 02.01.43
+ VERSION: 02.03.02
PATH: /README.md
BRIEF: Rebranding plugin for MokoWaaS platform
NOTE: Internal WaaS identity abstraction layer
@@ -23,7 +23,7 @@
# MokoWaaS Plugin
-[](https://github.com/mokoconsulting-tech/MokoWaaS/releases/tag/v02)
+[](https://github.com/mokoconsulting-tech/MokoWaaS/releases/tag/v02)
[](LICENSE)
[](https://www.joomla.org)
[](https://www.php.net)
diff --git a/src/packages/com_mokowaas/admin/services/provider.php b/src/packages/com_mokowaas/admin/services/provider.php
new file mode 100644
index 0000000..d89877d
--- /dev/null
+++ b/src/packages/com_mokowaas/admin/services/provider.php
@@ -0,0 +1,38 @@
+registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoWaaS'));
+ $container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoWaaS'));
+
+ $container->set(
+ ComponentInterface::class,
+ function (Container $container) {
+ $component = new \Joomla\CMS\Extension\MVCComponent(
+ $container->get(ComponentDispatcherFactoryInterface::class)
+ );
+ $component->setMVCFactory($container->get(MVCFactoryInterface::class));
+
+ return $component;
+ }
+ );
+ }
+};
diff --git a/src/packages/com_mokowaas/api/src/Controller/CacheController.php b/src/packages/com_mokowaas/api/src/Controller/CacheController.php
new file mode 100644
index 0000000..8b4a4f4
--- /dev/null
+++ b/src/packages/com_mokowaas/api/src/Controller/CacheController.php
@@ -0,0 +1,89 @@
+input->getMethod() !== 'POST')
+ {
+ $this->sendJson(405, ['error' => 'POST required']);
+ return;
+ }
+
+ $user = $app->getIdentity();
+ if (!$user->authorise('core.manage', 'com_plugins'))
+ {
+ $this->sendJson(403, ['error' => 'Not authorized']);
+ return;
+ }
+
+ try
+ {
+ $cache = Factory::getCache('');
+ $cache->clean('');
+
+ $adminCache = Factory::getCache('', 'callback', 'administrator');
+ $adminCache->clean('');
+
+ if (function_exists('opcache_reset'))
+ {
+ opcache_reset();
+ }
+
+ $this->sendJson(200, [
+ 'status' => 'ok',
+ 'message' => 'Cache cleared',
+ ]);
+ }
+ catch (\Throwable $e)
+ {
+ $this->sendJson(500, [
+ 'error' => 'Cache clear failed',
+ 'message' => $e->getMessage(),
+ ]);
+ }
+ }
+
+ /**
+ * @param int $code HTTP status code
+ * @param array $payload Response data
+ * @return void
+ */
+ private function sendJson(int $code, array $payload): void
+ {
+ $app = Factory::getApplication();
+ $app->setHeader('Content-Type', 'application/json', true);
+ $app->setHeader('Status', (string) $code, true);
+ echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+ $app->close();
+ }
+}
diff --git a/src/packages/com_mokowaas/api/src/Controller/HealthController.php b/src/packages/com_mokowaas/api/src/Controller/HealthController.php
new file mode 100644
index 0000000..5394bcb
--- /dev/null
+++ b/src/packages/com_mokowaas/api/src/Controller/HealthController.php
@@ -0,0 +1,139 @@
+getIdentity();
+
+ if (!$user->authorise('core.manage', 'com_plugins'))
+ {
+ $this->sendJson(403, ['error' => 'Not authorized']);
+ return;
+ }
+
+ $plugin = PluginHelper::getPlugin('system', 'mokowaas');
+
+ if (!$plugin)
+ {
+ $this->sendJson(503, ['error' => 'MokoWaaS system plugin not enabled']);
+ return;
+ }
+
+ $params = new Registry($plugin->params);
+ $config = Factory::getConfig();
+ $db = Factory::getDbo();
+
+ // Collect basic health data
+ $payload = [
+ 'status' => 'ok',
+ 'timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
+ 'site' => [
+ 'name' => $config->get('sitename', ''),
+ 'url' => rtrim(Uri::root(), '/'),
+ 'joomla_version' => JVERSION,
+ 'php_version' => PHP_VERSION,
+ 'db_type' => $db->getName(),
+ 'offline' => (bool) $config->get('offline', 0),
+ 'debug' => (bool) $config->get('debug', 0),
+ 'sef' => (bool) $config->get('sef', 0),
+ 'caching' => (bool) $config->get('caching', 0),
+ ],
+ 'plugin' => [
+ 'brand' => $params->get('brand_name', 'MokoWaaS'),
+ 'company' => $params->get('company_name', 'Moko Consulting'),
+ ],
+ ];
+
+ // Database check
+ try
+ {
+ $db->setQuery('SELECT 1');
+ $db->loadResult();
+ $payload['checks']['database'] = ['status' => 'ok'];
+ }
+ catch (\Throwable $e)
+ {
+ $payload['status'] = 'error';
+ $payload['checks']['database'] = ['status' => 'error', 'message' => $e->getMessage()];
+ }
+
+ // Disk space
+ $free = @disk_free_space(JPATH_ROOT);
+ $total = @disk_total_space(JPATH_ROOT);
+ if ($free !== false && $total !== false)
+ {
+ $freeMb = round($free / 1048576);
+ $payload['checks']['filesystem'] = [
+ 'status' => $freeMb < 100 ? 'degraded' : 'ok',
+ 'free_disk_mb' => $freeMb,
+ 'total_disk_mb' => round($total / 1048576),
+ ];
+ }
+
+ // Content counts
+ $db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__content')));
+ $payload['counts']['articles'] = (int) $db->loadResult();
+
+ $db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__users')));
+ $payload['counts']['users'] = (int) $db->loadResult();
+
+ $db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__extensions'))->where($db->quoteName('enabled') . ' = 1'));
+ $payload['counts']['extensions'] = (int) $db->loadResult();
+
+ $this->sendJson(200, $payload);
+ }
+
+ /**
+ * Send a JSON response and close.
+ *
+ * @param int $code HTTP status code
+ * @param array $payload Response data
+ *
+ * @return void
+ *
+ * @since 1.0.0
+ */
+ private function sendJson(int $code, array $payload): void
+ {
+ $app = Factory::getApplication();
+ $app->setHeader('Content-Type', 'application/json', true);
+ $app->setHeader('Status', (string) $code, true);
+ echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
+ $app->close();
+ }
+}
diff --git a/src/packages/com_mokowaas/api/src/Controller/UpdateController.php b/src/packages/com_mokowaas/api/src/Controller/UpdateController.php
new file mode 100644
index 0000000..d74e9c2
--- /dev/null
+++ b/src/packages/com_mokowaas/api/src/Controller/UpdateController.php
@@ -0,0 +1,94 @@
+input->getMethod() !== 'POST')
+ {
+ $this->sendJson(405, ['error' => 'POST required']);
+ return;
+ }
+
+ $user = $app->getIdentity();
+ if (!$user->authorise('core.manage', 'com_installer'))
+ {
+ $this->sendJson(403, ['error' => 'Not authorized']);
+ return;
+ }
+
+ try
+ {
+ $db = Factory::getDbo();
+
+ $db->setQuery($db->getQuery(true)->delete($db->quoteName('#__updates')));
+ $db->execute();
+
+ \Joomla\CMS\Updater\Updater::getInstance()->findUpdates();
+
+ $db->setQuery(
+ $db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from($db->quoteName('#__updates'))
+ ->where($db->quoteName('extension_id') . ' != 0')
+ );
+ $count = (int) $db->loadResult();
+
+ $this->sendJson(200, [
+ 'status' => 'ok',
+ 'updates_found' => $count,
+ 'message' => $count . ' update(s) available',
+ ]);
+ }
+ catch (\Throwable $e)
+ {
+ $this->sendJson(500, [
+ 'error' => 'Update check failed',
+ 'message' => $e->getMessage(),
+ ]);
+ }
+ }
+
+ /**
+ * @param int $code HTTP status code
+ * @param array $payload Response data
+ * @return void
+ */
+ private function sendJson(int $code, array $payload): void
+ {
+ $app = Factory::getApplication();
+ $app->setHeader('Content-Type', 'application/json', true);
+ $app->setHeader('Status', (string) $code, true);
+ echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+ $app->close();
+ }
+}
diff --git a/src/packages/com_mokowaas/mokowaas.xml b/src/packages/com_mokowaas/mokowaas.xml
new file mode 100644
index 0000000..5481612
--- /dev/null
+++ b/src/packages/com_mokowaas/mokowaas.xml
@@ -0,0 +1,23 @@
+
+
+ MokoWaaS API
+ Moko Consulting
+ 2026-05-23
+ Copyright (C) 2026 Moko Consulting. All rights reserved.
+ GPL-3.0-or-later
+ hello@mokoconsulting.tech
+ https://mokoconsulting.tech
+ 01.00.00
+ Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups.
+ Moko\Component\MokoWaaS\Api
+
+
+ services
+
+
+
+
+ src
+
+
+
diff --git a/src/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
similarity index 100%
rename from src/Extension/MokoWaaS.php
rename to src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
diff --git a/src/Field/AllowedIpsField.php b/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php
similarity index 100%
rename from src/Field/AllowedIpsField.php
rename to src/packages/plg_system_mokowaas/Field/AllowedIpsField.php
diff --git a/src/administrator/language/en-GB/index.html b/src/packages/plg_system_mokowaas/administrator/language/en-GB/index.html
similarity index 100%
rename from src/administrator/language/en-GB/index.html
rename to src/packages/plg_system_mokowaas/administrator/language/en-GB/index.html
diff --git a/src/administrator/language/en-GB/plg_system_mokowaas.sys.ini b/src/packages/plg_system_mokowaas/administrator/language/en-GB/plg_system_mokowaas.sys.ini
similarity index 100%
rename from src/administrator/language/en-GB/plg_system_mokowaas.sys.ini
rename to src/packages/plg_system_mokowaas/administrator/language/en-GB/plg_system_mokowaas.sys.ini
diff --git a/src/administrator/language/en-US/index.html b/src/packages/plg_system_mokowaas/administrator/language/en-US/index.html
similarity index 100%
rename from src/administrator/language/en-US/index.html
rename to src/packages/plg_system_mokowaas/administrator/language/en-US/index.html
diff --git a/src/administrator/language/en-US/plg_system_mokowaas.sys.ini b/src/packages/plg_system_mokowaas/administrator/language/en-US/plg_system_mokowaas.sys.ini
similarity index 100%
rename from src/administrator/language/en-US/plg_system_mokowaas.sys.ini
rename to src/packages/plg_system_mokowaas/administrator/language/en-US/plg_system_mokowaas.sys.ini
diff --git a/src/administrator/language/overrides/en-GB.override.ini b/src/packages/plg_system_mokowaas/administrator/language/overrides/en-GB.override.ini
similarity index 100%
rename from src/administrator/language/overrides/en-GB.override.ini
rename to src/packages/plg_system_mokowaas/administrator/language/overrides/en-GB.override.ini
diff --git a/src/administrator/language/overrides/en-US.override.ini b/src/packages/plg_system_mokowaas/administrator/language/overrides/en-US.override.ini
similarity index 100%
rename from src/administrator/language/overrides/en-US.override.ini
rename to src/packages/plg_system_mokowaas/administrator/language/overrides/en-US.override.ini
diff --git a/src/administrator/language/overrides/index.html b/src/packages/plg_system_mokowaas/administrator/language/overrides/index.html
similarity index 100%
rename from src/administrator/language/overrides/index.html
rename to src/packages/plg_system_mokowaas/administrator/language/overrides/index.html
diff --git a/src/forms/alias_entry.xml b/src/packages/plg_system_mokowaas/forms/alias_entry.xml
similarity index 100%
rename from src/forms/alias_entry.xml
rename to src/packages/plg_system_mokowaas/forms/alias_entry.xml
diff --git a/src/index.html b/src/packages/plg_system_mokowaas/index.html
similarity index 100%
rename from src/index.html
rename to src/packages/plg_system_mokowaas/index.html
diff --git a/src/language/en-GB/index.html b/src/packages/plg_system_mokowaas/language/en-GB/index.html
similarity index 100%
rename from src/language/en-GB/index.html
rename to src/packages/plg_system_mokowaas/language/en-GB/index.html
diff --git a/src/language/en-GB/plg_system_mokowaas.ini b/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini
similarity index 100%
rename from src/language/en-GB/plg_system_mokowaas.ini
rename to src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini
diff --git a/src/language/en-GB/plg_system_mokowaas.sys.ini b/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.sys.ini
similarity index 100%
rename from src/language/en-GB/plg_system_mokowaas.sys.ini
rename to src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.sys.ini
diff --git a/src/language/en-US/index.html b/src/packages/plg_system_mokowaas/language/en-US/index.html
similarity index 100%
rename from src/language/en-US/index.html
rename to src/packages/plg_system_mokowaas/language/en-US/index.html
diff --git a/src/language/en-US/plg_system_mokowaas.ini b/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini
similarity index 100%
rename from src/language/en-US/plg_system_mokowaas.ini
rename to src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini
diff --git a/src/language/en-US/plg_system_mokowaas.sys.ini b/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.sys.ini
similarity index 100%
rename from src/language/en-US/plg_system_mokowaas.sys.ini
rename to src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.sys.ini
diff --git a/src/language/overrides/en-GB.override.ini b/src/packages/plg_system_mokowaas/language/overrides/en-GB.override.ini
similarity index 100%
rename from src/language/overrides/en-GB.override.ini
rename to src/packages/plg_system_mokowaas/language/overrides/en-GB.override.ini
diff --git a/src/language/overrides/en-US.override.ini b/src/packages/plg_system_mokowaas/language/overrides/en-US.override.ini
similarity index 100%
rename from src/language/overrides/en-US.override.ini
rename to src/packages/plg_system_mokowaas/language/overrides/en-US.override.ini
diff --git a/src/language/overrides/index.html b/src/packages/plg_system_mokowaas/language/overrides/index.html
similarity index 100%
rename from src/language/overrides/index.html
rename to src/packages/plg_system_mokowaas/language/overrides/index.html
diff --git a/src/media/favicon.ico b/src/packages/plg_system_mokowaas/media/favicon.ico
similarity index 100%
rename from src/media/favicon.ico
rename to src/packages/plg_system_mokowaas/media/favicon.ico
diff --git a/src/media/favicon.svg b/src/packages/plg_system_mokowaas/media/favicon.svg
similarity index 100%
rename from src/media/favicon.svg
rename to src/packages/plg_system_mokowaas/media/favicon.svg
diff --git a/src/media/favicon_256.png b/src/packages/plg_system_mokowaas/media/favicon_256.png
similarity index 100%
rename from src/media/favicon_256.png
rename to src/packages/plg_system_mokowaas/media/favicon_256.png
diff --git a/src/media/index.html b/src/packages/plg_system_mokowaas/media/index.html
similarity index 100%
rename from src/media/index.html
rename to src/packages/plg_system_mokowaas/media/index.html
diff --git a/src/media/logo.png b/src/packages/plg_system_mokowaas/media/logo.png
similarity index 100%
rename from src/media/logo.png
rename to src/packages/plg_system_mokowaas/media/logo.png
diff --git a/src/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml
similarity index 99%
rename from src/mokowaas.xml
rename to src/packages/plg_system_mokowaas/mokowaas.xml
index fe19a47..6aa8e25 100644
--- a/src/mokowaas.xml
+++ b/src/packages/plg_system_mokowaas/mokowaas.xml
@@ -30,7 +30,7 @@
GNU General Public License version 3 or later; see LICENSE.md
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.01.43
+ 02.03.00
This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.
Moko\Plugin\System\MokoWaaS
script.php
diff --git a/src/payload/index.html b/src/packages/plg_system_mokowaas/payload/index.html
similarity index 100%
rename from src/payload/index.html
rename to src/packages/plg_system_mokowaas/payload/index.html
diff --git a/src/packages/plg_system_mokowaas/script.php b/src/packages/plg_system_mokowaas/script.php
new file mode 100644
index 0000000..bd4cd4a
--- /dev/null
+++ b/src/packages/plg_system_mokowaas/script.php
@@ -0,0 +1,1191 @@
+
+ *
+ * This file is part of a Moko Consulting project.
+ *
+ * SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License (./LICENSE.md).
+ *
+ * FILE INFORMATION
+ * DEFGROUP: Joomla.Plugin
+ * INGROUP: MokoWaaS
+ * REPO: https://github.com/mokoconsulting-tech/mokowaas
+ * VERSION: 02.01.08
+ * PATH: /src/script.php
+ * BRIEF: Installation script for MokoWaaS plugin
+ * NOTE: Handles installation, update, and uninstallation tasks including language override deployment
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\CMS\Installer\InstallerAdapter;
+use Joomla\CMS\Installer\InstallerScriptInterface;
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\Log\Log;
+use Joomla\Filesystem\File;
+use Joomla\Filesystem\Folder;
+
+/**
+ * Installation script for MokoWaaS plugin
+ *
+ * This script handles the installation and uninstallation of language override files
+ * to Joomla's global language override directories.
+ *
+ * @since 02.01.08
+ */
+class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
+{
+ /**
+ * Minimum Joomla version required to install the extension.
+ *
+ * @var string
+ * @since 02.01.08
+ */
+ private $minimumJoomla = '5.0.0';
+
+ /**
+ * Minimum PHP version required to install the extension.
+ *
+ * @var string
+ * @since 02.01.08
+ */
+ private $minimumPhp = '8.1.0';
+
+ /**
+ * Language tags supported by this plugin.
+ *
+ * @var array
+ * @since 02.01.08
+ */
+ private $languageTags = ['en-GB', 'en-US'];
+
+ /**
+ * Called before any type of action.
+ *
+ * @param string $type Which action is happening (install|uninstall|discover_install|update)
+ * @param InstallerAdapter $adapter The object responsible for running this script
+ *
+ * @return boolean True on success
+ *
+ * @since 02.01.08
+ */
+ public function preflight($type, $adapter): bool
+ {
+ // Check minimum Joomla version
+ if (version_compare(JVERSION, $this->minimumJoomla, '<'))
+ {
+ Factory::getApplication()->enqueueMessage(
+ sprintf('This extension requires Joomla %s or later.', $this->minimumJoomla),
+ 'error'
+ );
+ return false;
+ }
+
+ // Check minimum PHP version
+ if (version_compare(PHP_VERSION, $this->minimumPhp, '<'))
+ {
+ Factory::getApplication()->enqueueMessage(
+ sprintf('This extension requires PHP %s or later.', $this->minimumPhp),
+ 'error'
+ );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Called after any type of action.
+ *
+ * @param string $type Which action is happening (install|uninstall|discover_install|update)
+ * @param InstallerAdapter $adapter The object responsible for running this script
+ *
+ * @return boolean True on success
+ *
+ * @since 02.01.08
+ */
+ public function postflight($type, $adapter): bool
+ {
+ // Only install overrides on install or update
+ if ($type === 'install' || $type === 'update')
+ {
+ $this->enableAndLockPlugin();
+ $this->ensureMokoCassiopeia();
+ $this->installLanguageOverrides();
+ $this->updateLoginSupportUrls();
+ $this->updateAtumBranding();
+ $this->registerActionLogExtension();
+ $this->provisionHealthEndpoint();
+ $this->sendInstallNotification($type);
+ }
+
+ return true;
+ }
+
+ /**
+ * Called on installation.
+ *
+ * @param InstallerAdapter $adapter The object responsible for running this script
+ *
+ * @return boolean True on success
+ *
+ * @since 02.01.08
+ */
+ public function install(InstallerAdapter $adapter): bool
+ {
+ return true;
+ }
+
+ /**
+ * Called on update.
+ *
+ * @param InstallerAdapter $adapter The object responsible for running this script
+ *
+ * @return boolean True on success
+ *
+ * @since 02.01.08
+ */
+ public function update(InstallerAdapter $adapter): bool
+ {
+ return true;
+ }
+
+ /**
+ * Called on uninstallation.
+ *
+ * @param InstallerAdapter $adapter The object responsible for running this script
+ *
+ * @return boolean True on success
+ *
+ * @since 02.01.08
+ */
+ public function uninstall(InstallerAdapter $adapter): bool
+ {
+ $this->sendInstallNotification('uninstall');
+ $this->uninstallLanguageOverrides();
+ $this->unregisterActionLogExtension();
+
+ return true;
+ }
+
+ /** Sentinel comment that marks the start of MokoWaaS overrides inside a Joomla override file. */
+ /**
+ * Enable, lock, and protect the plugin in #__extensions.
+ *
+ * Runs on both install and update so existing installs get locked.
+ *
+ * @return void
+ *
+ * @since 02.01.08
+ */
+ private function enableAndLockPlugin()
+ {
+ $db = Factory::getDbo();
+ $db->setQuery(
+ $db->getQuery(true)
+ ->update($db->quoteName('#__extensions'))
+ ->set($db->quoteName('enabled') . ' = 1')
+ ->set($db->quoteName('locked') . ' = 1')
+ ->set($db->quoteName('protected') . ' = 1')
+ ->where($db->quoteName('element') . ' = '
+ . $db->quote('mokowaas'))
+ ->where($db->quoteName('folder') . ' = '
+ . $db->quote('system'))
+ ->where($db->quoteName('type') . ' = '
+ . $db->quote('plugin'))
+ );
+ $db->execute();
+ }
+
+ /**
+ * Ensure MokoOnyx is installed, locked, and set as default.
+ *
+ * Always installs MokoOnyx from bundled payload if not present,
+ * locks it, sets it as default site template, and unlocks
+ * MokoCassiopeia so it can be uninstalled.
+ *
+ * @return void
+ *
+ * @since 02.00.03
+ */
+ private function ensureMokoCassiopeia()
+ {
+ $db = Factory::getDbo();
+
+ // Check whether MokoOnyx is already installed
+ $query = $db->getQuery(true)
+ ->select($db->quoteName('extension_id'))
+ ->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('mokoonyx'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('template'));
+ $onyx = $db->setQuery($query)->loadObject();
+
+ // Install from payload if missing
+ if (!$onyx)
+ {
+ $this->installMokoOnyxFromPayload();
+
+ // Re-check after install
+ $onyx = $db->setQuery($query)->loadObject();
+ }
+
+ if ($onyx)
+ {
+ // Lock and protect MokoOnyx
+ $db->setQuery(
+ $db->getQuery(true)
+ ->update($db->quoteName('#__extensions'))
+ ->set($db->quoteName('enabled') . ' = 1')
+ ->set($db->quoteName('locked') . ' = 1')
+ ->set($db->quoteName('protected') . ' = 1')
+ ->where($db->quoteName('extension_id') . ' = ' . (int) $onyx->extension_id)
+ )->execute();
+
+ $this->setDefaultTemplate('mokoonyx', 0);
+ }
+
+ // Always unlock MokoCassiopeia (allow uninstall)
+ $db->setQuery(
+ $db->getQuery(true)
+ ->update($db->quoteName('#__extensions'))
+ ->set($db->quoteName('locked') . ' = 0')
+ ->set($db->quoteName('protected') . ' = 0')
+ ->where($db->quoteName('element') . ' = ' . $db->quote('mokocassiopeia'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('template'))
+ )->execute();
+ }
+
+ /**
+ * Install MokoOnyx from the bundled payload zip.
+ *
+ * @return void
+ *
+ * @since 02.01.17
+ */
+ private function installMokoOnyxFromPayload()
+ {
+ $pluginPath = JPATH_PLUGINS . '/system/mokowaas';
+ $payloadZip = $pluginPath . '/payload/mokoonyx.zip';
+
+ if (!file_exists($payloadZip))
+ {
+ Factory::getApplication()->enqueueMessage(
+ 'MokoOnyx payload not found. Please install MokoOnyx manually from '
+ . 'Gitea Releases.',
+ 'warning'
+ );
+
+ return;
+ }
+
+ $tmpDir = JPATH_ROOT . '/tmp/mokoonyx';
+
+ try
+ {
+ $archive = new \Joomla\Archive\Archive();
+ $archive->extract($payloadZip, $tmpDir);
+
+ $installDir = $tmpDir;
+
+ if (!file_exists($tmpDir . '/templateDetails.xml'))
+ {
+ $xmlFiles = glob($tmpDir . '/*/templateDetails.xml');
+
+ if (!empty($xmlFiles))
+ {
+ $installDir = dirname($xmlFiles[0]);
+ }
+ else
+ {
+ Factory::getApplication()->enqueueMessage(
+ 'MokoOnyx: templateDetails.xml not found in archive.',
+ 'warning'
+ );
+
+ return;
+ }
+ }
+
+ $installer = \Joomla\CMS\Installer\Installer::getInstance();
+
+ if ($installer->install($installDir))
+ {
+ Factory::getApplication()->enqueueMessage(
+ 'MokoOnyx installed successfully.',
+ 'message'
+ );
+ }
+ else
+ {
+ Factory::getApplication()->enqueueMessage(
+ 'MokoOnyx installation from payload failed.',
+ 'warning'
+ );
+ }
+ }
+ catch (\Exception $e)
+ {
+ Factory::getApplication()->enqueueMessage(
+ 'MokoOnyx error: ' . $e->getMessage(),
+ 'warning'
+ );
+ }
+ finally
+ {
+ if (is_dir($tmpDir))
+ {
+ Folder::delete($tmpDir);
+ }
+ }
+ }
+
+ /**
+ * Set a template as the default for a given client.
+ *
+ * @param string $template Template element name
+ * @param int $clientId 0 = site, 1 = admin
+ *
+ * @return void
+ *
+ * @since 02.01.01
+ */
+ private function setDefaultTemplate($template, $clientId)
+ {
+ $db = Factory::getDbo();
+
+ // Unset all other defaults for this client
+ $db->setQuery(
+ $db->getQuery(true)
+ ->update($db->quoteName('#__template_styles'))
+ ->set($db->quoteName('home') . ' = 0')
+ ->where($db->quoteName('client_id') . ' = ' . $clientId)
+ ->where($db->quoteName('home') . ' = 1')
+ );
+ $db->execute();
+
+ // Set our template as default
+ $db->setQuery(
+ $db->getQuery(true)
+ ->update($db->quoteName('#__template_styles'))
+ ->set($db->quoteName('home') . ' = 1')
+ ->where($db->quoteName('template') . ' = '
+ . $db->quote($template))
+ ->where($db->quoteName('client_id') . ' = ' . $clientId)
+ );
+ $db->execute();
+ }
+
+ /**
+ * Parse an updates.xml and return the download URL.
+ *
+ * @param string $updatesUrl URL to the updates.xml file
+ *
+ * @return string|null Download URL or null on failure
+ *
+ * @since 02.01.01
+ */
+ /**
+ * Send email notification on install or update.
+ *
+ * @param string $type install or update
+ *
+ * @return void
+ *
+ * @since 02.01.02
+ */
+ private function sendInstallNotification($type)
+ {
+ try
+ {
+ $config = Factory::getApplication()->getConfig();
+ $siteName = $config->get('sitename', 'Joomla Site');
+ $siteUrl = \Joomla\CMS\Uri\Uri::root();
+ // Read version from manifest XML
+ $manifestFile = JPATH_PLUGINS
+ . '/system/mokowaas/mokowaas.xml';
+ $version = '?.?.?';
+
+ if (file_exists($manifestFile))
+ {
+ $xml = simplexml_load_file($manifestFile);
+
+ if ($xml && isset($xml->version))
+ {
+ $version = (string) $xml->version;
+ }
+ }
+
+ $mailer = Factory::getMailer();
+ $mailer->addRecipient('webmaster@mokoconsulting.tech');
+ $mailer->setSubject(
+ sprintf('[%s] MokoWaaS %s — %s',
+ $siteName, $type, $version)
+ );
+ $mailer->setBody(
+ sprintf(
+ "MokoWaaS plugin was %sd on %s\n\n"
+ . "Version: %s\n"
+ . "Site: %s\n"
+ . "Time: %s\n"
+ . "PHP: %s\n"
+ . "Joomla: %s\n",
+ $type,
+ $siteName,
+ $version,
+ $siteUrl,
+ date('Y-m-d H:i:s T'),
+ PHP_VERSION,
+ JVERSION
+ )
+ );
+ $mailer->isHtml(false);
+ $mailer->Send();
+ }
+ catch (\Exception $e)
+ {
+ // Don't break install if email fails
+ }
+ }
+
+ private const BLOCK_START = '; ===== BEGIN MokoWaaS Overrides (do not edit this block) =====';
+
+ /** Sentinel comment that marks the end of MokoWaaS overrides inside a Joomla override file. */
+ private const BLOCK_END = '; ===== END MokoWaaS Overrides =====';
+
+ /**
+ * Build the placeholder → value map from the plugin's saved params.
+ *
+ * On first install the params row may not exist yet, so every value
+ * falls back to a sensible default.
+ *
+ * @return array Associative array of placeholder => replacement value
+ *
+ * @since 02.01.08
+ */
+ private function getPlaceholders()
+ {
+ $params = $this->getPluginParams();
+
+ return [
+ '{{BRAND_NAME}}' => $params->get('brand_name', 'MokoWaaS'),
+ '{{COMPANY_NAME}}' => $params->get('company_name', 'Moko Consulting'),
+ '{{SUPPORT_URL}}' => $params->get('support_url', 'https://mokoconsulting.tech'),
+ ];
+ }
+
+ /**
+ * Load the plugin's saved params from the database.
+ *
+ * @return \Joomla\Registry\Registry
+ *
+ * @since 02.01.08
+ */
+ private function getPluginParams()
+ {
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select($db->quoteName('params'))
+ ->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
+ ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'));
+
+ $db->setQuery($query);
+ $json = $db->loadResult();
+
+ return new \Joomla\Registry\Registry($json ?: '{}');
+ }
+
+ /**
+ * Resolve placeholders in an array of language strings.
+ *
+ * @param array $strings Key/value pairs (values may contain {{…}} tokens)
+ *
+ * @return array The same array with placeholders replaced
+ *
+ * @since 02.01.08
+ */
+ private function resolvePlaceholders(array $strings)
+ {
+ $placeholders = $this->getPlaceholders();
+ $search = array_keys($placeholders);
+ $replace = array_values($placeholders);
+
+ foreach ($strings as $key => $value)
+ {
+ $strings[$key] = str_replace($search, $replace, $value);
+ }
+
+ return $strings;
+ }
+
+ /**
+ * Install language override files to Joomla's global override directories.
+ *
+ * Reads each source override template shipped with the plugin, resolves
+ * {{BRAND_NAME}} etc. from plugin params, then merges the resolved keys
+ * into the destination file inside a clearly delimited block. Existing
+ * overrides outside the block are never touched.
+ *
+ * @return void
+ *
+ * @since 02.01.08
+ */
+ private function installLanguageOverrides()
+ {
+ $app = Factory::getApplication();
+ $pluginPath = JPATH_PLUGINS . '/system/mokowaas';
+
+ $overrideSets = [
+ // [source folder relative to plugin, Joomla destination base]
+ ['language/overrides', JPATH_ROOT . '/language/overrides', 'frontend'],
+ ['administrator/language/overrides', JPATH_ADMINISTRATOR . '/language/overrides', 'administrator'],
+ ];
+
+ foreach ($overrideSets as [$sourceDir, $destDir, $label])
+ {
+ foreach ($this->languageTags as $tag)
+ {
+ $source = $pluginPath . '/' . $sourceDir . '/' . $tag . '.override.ini';
+ $dest = $destDir . '/' . $tag . '.override.ini';
+
+ if (!file_exists($source))
+ {
+ continue;
+ }
+
+ if (!is_dir($destDir))
+ {
+ Folder::create($destDir);
+ }
+
+ $pluginOverrides = $this->resolvePlaceholders($this->parseLanguageFile($source));
+
+ if (empty($pluginOverrides))
+ {
+ continue;
+ }
+
+ if ($this->mergeOverridesIntoFile($dest, $pluginOverrides))
+ {
+ $app->enqueueMessage(
+ sprintf('Installed %s language overrides for %s', $label, $tag),
+ 'message'
+ );
+ }
+ else
+ {
+ $app->enqueueMessage(
+ sprintf('Failed to install %s language overrides for %s', $label, $tag),
+ 'warning'
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Update the mod_loginsupport module params to point to
+ * Moko Consulting URLs at install time.
+ *
+ * @return void
+ *
+ * @since 02.01.08
+ */
+ private function updateLoginSupportUrls()
+ {
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select([$db->quoteName('id'), $db->quoteName('params')])
+ ->from($db->quoteName('#__modules'))
+ ->where($db->quoteName('module') . ' = '
+ . $db->quote('mod_loginsupport'));
+
+ $db->setQuery($query);
+ $modules = $db->loadObjectList();
+
+ if (empty($modules))
+ {
+ return;
+ }
+
+ $supportUrls = [
+ 'forum_url' => 'https://mokoconsulting.tech/support',
+ 'documentation_url' => 'https://mokoconsulting.tech/kb',
+ 'news_url' => 'https://mokoconsulting.tech/news',
+ ];
+
+ foreach ($modules as $module)
+ {
+ $params = new \Joomla\Registry\Registry(
+ $module->params ?: '{}'
+ );
+
+ foreach ($supportUrls as $key => $url)
+ {
+ $params->set($key, $url);
+ }
+
+ $update = $db->getQuery(true)
+ ->update($db->quoteName('#__modules'))
+ ->set($db->quoteName('params') . ' = '
+ . $db->quote($params->toString()))
+ ->where($db->quoteName('id') . ' = '
+ . (int) $module->id);
+
+ $db->setQuery($update);
+ $db->execute();
+ }
+
+ Factory::getApplication()->enqueueMessage(
+ 'Updated login support URLs.', 'message'
+ );
+ }
+
+ /**
+ * Set Atum admin template branding params at install time.
+ *
+ * @return void
+ *
+ * @since 02.01.08
+ */
+ private function updateAtumBranding()
+ {
+ $mediaBase = 'media/plg_system_mokowaas/';
+
+ $expected = [
+ 'logoBrandLarge' => $mediaBase . 'logo.png',
+ 'logoBrandSmall' => $mediaBase . 'favicon_256.png',
+ 'loginLogo' => $mediaBase . 'logo.png',
+ 'logoBrandLargeAlt' => '',
+ 'logoBrandSmallAlt' => '',
+ 'loginLogoAlt' => '',
+ 'emptyLogoBrandLargeAlt' => '1',
+ 'emptyLogoBrandSmallAlt' => '1',
+ 'emptyLoginLogoAlt' => '1',
+ 'hue' => 'hsl(219, 44%, 18%)',
+ 'special-color' => '#1a2744',
+ 'link-color' => '#0051ad',
+ ];
+
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select([$db->quoteName('id'), $db->quoteName('params')])
+ ->from($db->quoteName('#__template_styles'))
+ ->where($db->quoteName('template') . ' = '
+ . $db->quote('atum'))
+ ->where($db->quoteName('client_id') . ' = 1');
+
+ $db->setQuery($query);
+ $styles = $db->loadObjectList();
+
+ if (empty($styles))
+ {
+ return;
+ }
+
+ foreach ($styles as $style)
+ {
+ $params = new \Joomla\Registry\Registry(
+ $style->params ?: '{}'
+ );
+
+ foreach ($expected as $key => $value)
+ {
+ $params->set($key, $value);
+ }
+
+ $update = $db->getQuery(true)
+ ->update($db->quoteName('#__template_styles'))
+ ->set($db->quoteName('params') . ' = '
+ . $db->quote($params->toString()))
+ ->where($db->quoteName('id') . ' = '
+ . (int) $style->id);
+
+ $db->setQuery($update);
+ $db->execute();
+ }
+
+ Factory::getApplication()->enqueueMessage(
+ 'Updated Atum template branding.', 'message'
+ );
+ }
+
+ /**
+ * Register the plugin in #__action_logs_extensions so it appears
+ * as a filterable extension in System > Action Logs.
+ *
+ * @return void
+ *
+ * @since 02.01.08
+ */
+ /**
+ * Provision health endpoint with Grafana if configured.
+ *
+ * On install/update, if the health endpoint is enabled and Grafana
+ * credentials are set, generates a token (if missing) and triggers
+ * Grafana datasource provisioning via cURL. All data is passed as
+ * structured JSON — no shell commands are invoked.
+ *
+ * @return void
+ *
+ * @since 02.01.22
+ */
+ private function provisionHealthEndpoint()
+ {
+ $db = Factory::getDbo();
+
+ $query = $db->getQuery(true)
+ ->select($db->quoteName('params'))
+ ->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('element') . ' = '
+ . $db->quote('mokowaas'))
+ ->where($db->quoteName('folder') . ' = '
+ . $db->quote('system'));
+
+ $db->setQuery($query);
+ $rawParams = $db->loadResult();
+
+ if (empty($rawParams))
+ {
+ return;
+ }
+
+ $params = new \Joomla\Registry\Registry($rawParams);
+ $changed = false;
+
+ // Auto-generate token if missing
+ if (empty($params->get('health_api_token', '')))
+ {
+ $params->set(
+ 'health_api_token',
+ bin2hex(random_bytes(32))
+ );
+ $changed = true;
+ }
+
+ if ($changed)
+ {
+ $db->setQuery(
+ $db->getQuery(true)
+ ->update($db->quoteName('#__extensions'))
+ ->set($db->quoteName('params') . ' = '
+ . $db->quote($params->toString()))
+ ->where($db->quoteName('element') . ' = '
+ . $db->quote('mokowaas'))
+ ->where($db->quoteName('folder') . ' = '
+ . $db->quote('system'))
+ );
+ $db->execute();
+ }
+
+ // Heartbeat receiver — register with Grafana provisioning
+ $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
+ $siteName = Factory::getConfig()->get('sitename', 'Joomla');
+ $token = $params->get('health_api_token', '');
+
+ $payload = json_encode([
+ 'site_url' => $siteUrl,
+ 'site_name' => $siteName,
+ 'health_token' => $token,
+ 'action' => 'register',
+ ], JSON_UNESCAPED_SLASHES);
+
+ $ch = curl_init('https://bench.mokoconsulting.tech/api/waas-heartbeat/register');
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Content-Type: application/json',
+ 'X-MokoWaaS-Key: moko-waas-hb-2026-x9k4m',
+ ]);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+
+ $response = curl_exec($ch);
+ $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $error = curl_error($ch);
+ curl_close($ch);
+
+ $app = Factory::getApplication();
+ $body = json_decode($response, true);
+
+ if ($error)
+ {
+ $app->enqueueMessage('Grafana heartbeat failed: ' . $error, 'warning');
+ Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas');
+ }
+ elseif ($code === 200)
+ {
+ $status = $body['status'] ?? 'ok';
+ $app->enqueueMessage(
+ 'Grafana heartbeat: ' . $status . ' (' . ($body['ds_uid'] ?? '') . ')',
+ 'message'
+ );
+ }
+ else
+ {
+ $msg = sprintf('Grafana heartbeat failed: HTTP %d — %s',
+ $code, $body['error'] ?? $body['message'] ?? 'Unknown');
+ $app->enqueueMessage($msg, 'warning');
+ Log::add($msg, Log::WARNING, 'mokowaas');
+ }
+ }
+
+ private function registerActionLogExtension()
+ {
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from($db->quoteName('#__action_logs_extensions'))
+ ->where($db->quoteName('extension') . ' = '
+ . $db->quote('plg_system_mokowaas'));
+
+ $db->setQuery($query);
+
+ if ((int) $db->loadResult() > 0)
+ {
+ return;
+ }
+
+ $row = (object) ['extension' => 'plg_system_mokowaas'];
+ $db->insertObject('#__action_logs_extensions', $row);
+
+ Factory::getApplication()->enqueueMessage(
+ 'Registered MokoWaaS in Action Logs.', 'message'
+ );
+
+ // Register content type config for display formatting
+ $configQuery = $db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from($db->quoteName('#__action_log_config'))
+ ->where($db->quoteName('type_alias') . ' = '
+ . $db->quote('plg_system_mokowaas'));
+
+ $db->setQuery($configQuery);
+
+ if ((int) $db->loadResult() === 0)
+ {
+ $config = (object) [
+ 'type_title' => 'MokoWaaS',
+ 'type_alias' => 'plg_system_mokowaas',
+ 'id_holder' => '',
+ 'title_holder' => '',
+ 'table_name' => '',
+ 'text_prefix' => 'PLG_SYSTEM_MOKOWAAS',
+ ];
+
+ $db->insertObject('#__action_log_config', $config);
+ }
+ }
+
+ /**
+ * Remove the plugin from #__action_logs_extensions on uninstall.
+ *
+ * @return void
+ *
+ * @since 02.01.08
+ */
+ private function unregisterActionLogExtension()
+ {
+ $db = Factory::getDbo();
+
+ $db->setQuery(
+ $db->getQuery(true)
+ ->delete($db->quoteName('#__action_logs_extensions'))
+ ->where($db->quoteName('extension') . ' = '
+ . $db->quote('plg_system_mokowaas'))
+ );
+ $db->execute();
+
+ $db->setQuery(
+ $db->getQuery(true)
+ ->delete($db->quoteName('#__action_log_config'))
+ ->where($db->quoteName('type_alias') . ' = '
+ . $db->quote('plg_system_mokowaas'))
+ );
+ $db->execute();
+ }
+
+ /**
+ * Remove only MokoWaaS overrides from Joomla's global override files.
+ *
+ * Strips the delimited MokoWaaS block and any duplicate keys that appear
+ * outside the block (safety net for upgrades from older versions that wrote
+ * keys inline). All other content is preserved verbatim.
+ *
+ * @return void
+ *
+ * @since 02.01.08
+ */
+ private function uninstallLanguageOverrides()
+ {
+ $app = Factory::getApplication();
+ $pluginPath = JPATH_PLUGINS . '/system/mokowaas';
+
+ $overrideSets = [
+ ['language/overrides', JPATH_ROOT . '/language/overrides', 'frontend'],
+ ['administrator/language/overrides', JPATH_ADMINISTRATOR . '/language/overrides', 'administrator'],
+ ];
+
+ foreach ($overrideSets as [$sourceDir, $destDir, $label])
+ {
+ foreach ($this->languageTags as $tag)
+ {
+ $source = $pluginPath . '/' . $sourceDir . '/' . $tag . '.override.ini';
+ $dest = $destDir . '/' . $tag . '.override.ini';
+
+ if (!file_exists($dest))
+ {
+ continue;
+ }
+
+ $pluginKeys = array_keys($this->parseLanguageFile($source));
+
+ if ($this->removeOverridesFromFile($dest, $pluginKeys))
+ {
+ $app->enqueueMessage(
+ sprintf('Removed %s language overrides for %s', $label, $tag),
+ 'message'
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Merge plugin overrides into an existing Joomla override file.
+ *
+ * The method:
+ * 1. Reads the destination file (if it exists) and preserves every line.
+ * 2. Strips any previous MokoWaaS block so it can be rewritten cleanly.
+ * 3. Collects keys that exist outside the block (user-set overrides).
+ * 4. Appends a MokoWaaS block containing only keys NOT already
+ * defined by the user — existing customisations are never touched.
+ *
+ * @param string $dest Absolute path to the Joomla override file
+ * @param array $overrides Key/value pairs to inject
+ *
+ * @return boolean True on success
+ *
+ * @since 02.01.08
+ */
+ private function mergeOverridesIntoFile($dest, array $overrides)
+ {
+ $existingLines = [];
+
+ if (file_exists($dest))
+ {
+ $existingLines = file($dest, FILE_IGNORE_NEW_LINES);
+ }
+
+ // Strip any previous MokoWaaS block
+ $existingLines = $this->stripMokoWaaSBlock($existingLines);
+
+ // Collect keys already defined outside the block (user overrides)
+ $userKeys = [];
+
+ foreach ($existingLines as $line)
+ {
+ $trimmed = trim($line);
+
+ if ($trimmed !== '' && $trimmed[0] !== ';')
+ {
+ if (preg_match('/^([A-Z0-9_]+)\s*=/i', $trimmed, $m))
+ {
+ $userKeys[] = strtoupper($m[1]);
+ }
+ }
+ }
+
+ // Remove trailing blank lines so the block starts cleanly
+ while (!empty($existingLines)
+ && trim(end($existingLines)) === '')
+ {
+ array_pop($existingLines);
+ }
+
+ // Build the MokoWaaS block — skip keys the user already set
+ $block = [];
+ $block[] = '';
+ $block[] = self::BLOCK_START;
+ $block[] = '; Auto-generated on '
+ . date('Y-m-d H:i:s') . ' — do not edit manually.';
+
+ foreach ($overrides as $key => $value)
+ {
+ if (!in_array(strtoupper($key), $userKeys, true))
+ {
+ $block[] = strtoupper($key) . '="' . $value . '"';
+ }
+ }
+
+ $block[] = self::BLOCK_END;
+ $block[] = '';
+
+ $content = implode("\n", array_merge($existingLines, $block));
+
+ return File::write($dest, $content);
+ }
+
+ /**
+ * Remove MokoWaaS overrides from an existing Joomla override file.
+ *
+ * Strips the delimited block and any stray keys that match, then rewrites
+ * the file. If the file would be empty (or comments-only) it is deleted.
+ *
+ * @param string $dest Absolute path to the override file
+ * @param array $keys The override keys to remove (uppercase)
+ *
+ * @return boolean True on success
+ *
+ * @since 02.01.08
+ */
+ private function removeOverridesFromFile($dest, array $keys)
+ {
+ if (!file_exists($dest))
+ {
+ return true;
+ }
+
+ $lines = file($dest, FILE_IGNORE_NEW_LINES);
+
+ // Strip the MokoWaaS block
+ $lines = $this->stripMokoWaaSBlock($lines);
+
+ // Also strip any stray keys that match (legacy installs)
+ $upperKeys = array_map('strtoupper', $keys);
+ $cleaned = [];
+
+ foreach ($lines as $line)
+ {
+ $trimmed = trim($line);
+
+ if ($trimmed !== '' && $trimmed[0] !== ';')
+ {
+ if (preg_match('/^([A-Z0-9_]+)\s*=/i', $trimmed, $m))
+ {
+ if (in_array(strtoupper($m[1]), $upperKeys, true))
+ {
+ continue;
+ }
+ }
+ }
+
+ $cleaned[] = $line;
+ }
+
+ // Check whether any real keys remain
+ $hasKeys = false;
+
+ foreach ($cleaned as $line)
+ {
+ $trimmed = trim($line);
+
+ if ($trimmed !== '' && $trimmed[0] !== ';')
+ {
+ $hasKeys = true;
+ break;
+ }
+ }
+
+ if (!$hasKeys)
+ {
+ return File::delete($dest);
+ }
+
+ return File::write($dest, implode("\n", $cleaned) . "\n");
+ }
+
+ /**
+ * Remove the MokoWaaS sentinel block from an array of file lines.
+ *
+ * @param array $lines Lines of the file (no trailing newlines)
+ *
+ * @return array Lines with the block removed
+ *
+ * @since 02.01.08
+ */
+ private function stripMokoWaaSBlock(array $lines)
+ {
+ $out = [];
+ $inBlock = false;
+
+ foreach ($lines as $line)
+ {
+ if (trim($line) === self::BLOCK_START)
+ {
+ $inBlock = true;
+ continue;
+ }
+
+ if (trim($line) === self::BLOCK_END)
+ {
+ $inBlock = false;
+ continue;
+ }
+
+ if (!$inBlock)
+ {
+ $out[] = $line;
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Parse a language INI file and return the strings as an associative array.
+ *
+ * @param string $filePath The path to the language file
+ *
+ * @return array Array of language strings (key => value)
+ *
+ * @since 02.01.08
+ */
+ private function parseLanguageFile($filePath)
+ {
+ $strings = [];
+
+ if (!file_exists($filePath))
+ {
+ return $strings;
+ }
+
+ $content = file_get_contents($filePath);
+ $lines = explode("\n", $content);
+
+ foreach ($lines as $line)
+ {
+ $line = trim($line);
+
+ // Skip empty lines and comments
+ if ($line === '' || $line[0] === ';')
+ {
+ continue;
+ }
+
+ // Parse KEY="VALUE" format
+ if (preg_match('/^([A-Z0-9_]+)="(.+)"$/i', $line, $matches))
+ {
+ $strings[strtoupper($matches[1])] = $matches[2];
+ }
+ }
+
+ return $strings;
+ }
+}
diff --git a/src/services/index.html b/src/packages/plg_system_mokowaas/services/index.html
similarity index 100%
rename from src/services/index.html
rename to src/packages/plg_system_mokowaas/services/index.html
diff --git a/src/services/provider.php b/src/packages/plg_system_mokowaas/services/provider.php
similarity index 100%
rename from src/services/provider.php
rename to src/packages/plg_system_mokowaas/services/provider.php
diff --git a/src/packages/plg_webservices_mokowaas/mokowaas.xml b/src/packages/plg_webservices_mokowaas/mokowaas.xml
new file mode 100644
index 0000000..48c1cf4
--- /dev/null
+++ b/src/packages/plg_webservices_mokowaas/mokowaas.xml
@@ -0,0 +1,17 @@
+
+
+ Web Services - MokoWaaS
+ Moko Consulting
+ 2026-05-23
+ Copyright (C) 2026 Moko Consulting. All rights reserved.
+ GPL-3.0-or-later
+ hello@mokoconsulting.tech
+ https://mokoconsulting.tech
+ 01.00.00
+ Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.
+ Moko\Plugin\WebServices\MokoWaaS
+
+ services
+ src
+
+
diff --git a/src/packages/plg_webservices_mokowaas/services/provider.php b/src/packages/plg_webservices_mokowaas/services/provider.php
new file mode 100644
index 0000000..75919da
--- /dev/null
+++ b/src/packages/plg_webservices_mokowaas/services/provider.php
@@ -0,0 +1,36 @@
+set(
+ PluginInterface::class,
+ function (Container $container) {
+ $dispatcher = $container->get(DispatcherInterface::class);
+ $plugin = new MokoWaaSApi(
+ $dispatcher,
+ (array) PluginHelper::getPlugin('webservices', 'mokowaas')
+ );
+ $plugin->setApplication(Factory::getApplication());
+ return $plugin;
+ }
+ );
+ }
+};
diff --git a/src/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php b/src/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php
new file mode 100644
index 0000000..0e1860f
--- /dev/null
+++ b/src/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php
@@ -0,0 +1,65 @@
+ 'onBeforeApiRoute',
+ ];
+ }
+
+ /**
+ * Register API routes for MokoWaaS.
+ *
+ * @param ApiRouter $router The API router
+ *
+ * @return void
+ *
+ * @since 1.0.0
+ */
+ public function onBeforeApiRoute(&$router): void
+ {
+ $router->createCRUDRoutes(
+ 'v1/mokowaas/health',
+ 'health',
+ ['component' => 'com_mokowaas']
+ );
+
+ $router->createCRUDRoutes(
+ 'v1/mokowaas/cache',
+ 'cache',
+ ['component' => 'com_mokowaas']
+ );
+
+ $router->createCRUDRoutes(
+ 'v1/mokowaas/update',
+ 'update',
+ ['component' => 'com_mokowaas']
+ );
+ }
+}
diff --git a/src/payload/mokoonyx.zip b/src/payload/mokoonyx.zip
deleted file mode 100644
index 2f9c696..0000000
Binary files a/src/payload/mokoonyx.zip and /dev/null differ
diff --git a/src/pkg_mokowaas.xml b/src/pkg_mokowaas.xml
new file mode 100644
index 0000000..108d625
--- /dev/null
+++ b/src/pkg_mokowaas.xml
@@ -0,0 +1,24 @@
+
+
+ MokoWaaS
+ mokowaas
+ 02.03.02
+ 2026-05-23
+ Moko Consulting
+ hello@mokoconsulting.tech
+ https://mokoconsulting.tech
+ Copyright (C) 2026 Moko Consulting. All rights reserved.
+ GNU General Public License version 3 or later; see LICENSE
+ MokoWaaS site management suite — branding, health monitoring, tenant restrictions, and REST API.
+ script.php
+
+
+ plg_system_mokowaas.zip
+ com_mokowaas.zip
+ plg_webservices_mokowaas.zip
+
+
+
+ https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml
+
+
diff --git a/src/script.php b/src/script.php
index bd4cd4a..bc35c26 100644
--- a/src/script.php
+++ b/src/script.php
@@ -1,1191 +1,72 @@
- *
- * This file is part of a Moko Consulting project.
- *
- * SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License (./LICENSE.md).
- *
- * FILE INFORMATION
- * DEFGROUP: Joomla.Plugin
- * INGROUP: MokoWaaS
- * REPO: https://github.com/mokoconsulting-tech/mokowaas
- * VERSION: 02.01.08
- * PATH: /src/script.php
- * BRIEF: Installation script for MokoWaaS plugin
- * NOTE: Handles installation, update, and uninstallation tasks including language override deployment
+ * @package MokoWaaS
+ * @subpackage pkg_mokowaas
+ * @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\Factory;
use Joomla\CMS\Installer\InstallerAdapter;
-use Joomla\CMS\Installer\InstallerScriptInterface;
-use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
-use Joomla\Filesystem\File;
-use Joomla\Filesystem\Folder;
/**
- * Installation script for MokoWaaS plugin
+ * Package installation script for MokoWaaS.
*
- * This script handles the installation and uninstallation of language override files
- * to Joomla's global language override directories.
+ * Auto-enables the system plugin and webservices plugin after install.
*
- * @since 02.01.08
+ * @since 2.2.0
*/
-class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
+class Pkg_MokowaasInstallerScript
{
/**
- * Minimum Joomla version required to install the extension.
+ * Runs after package installation/update.
*
- * @var string
- * @since 02.01.08
- */
- private $minimumJoomla = '5.0.0';
-
- /**
- * Minimum PHP version required to install the extension.
- *
- * @var string
- * @since 02.01.08
- */
- private $minimumPhp = '8.1.0';
-
- /**
- * Language tags supported by this plugin.
- *
- * @var array
- * @since 02.01.08
- */
- private $languageTags = ['en-GB', 'en-US'];
-
- /**
- * Called before any type of action.
- *
- * @param string $type Which action is happening (install|uninstall|discover_install|update)
- * @param InstallerAdapter $adapter The object responsible for running this script
- *
- * @return boolean True on success
- *
- * @since 02.01.08
- */
- public function preflight($type, $adapter): bool
- {
- // Check minimum Joomla version
- if (version_compare(JVERSION, $this->minimumJoomla, '<'))
- {
- Factory::getApplication()->enqueueMessage(
- sprintf('This extension requires Joomla %s or later.', $this->minimumJoomla),
- 'error'
- );
- return false;
- }
-
- // Check minimum PHP version
- if (version_compare(PHP_VERSION, $this->minimumPhp, '<'))
- {
- Factory::getApplication()->enqueueMessage(
- sprintf('This extension requires PHP %s or later.', $this->minimumPhp),
- 'error'
- );
- return false;
- }
-
- return true;
- }
-
- /**
- * Called after any type of action.
- *
- * @param string $type Which action is happening (install|uninstall|discover_install|update)
- * @param InstallerAdapter $adapter The object responsible for running this script
- *
- * @return boolean True on success
- *
- * @since 02.01.08
- */
- public function postflight($type, $adapter): bool
- {
- // Only install overrides on install or update
- if ($type === 'install' || $type === 'update')
- {
- $this->enableAndLockPlugin();
- $this->ensureMokoCassiopeia();
- $this->installLanguageOverrides();
- $this->updateLoginSupportUrls();
- $this->updateAtumBranding();
- $this->registerActionLogExtension();
- $this->provisionHealthEndpoint();
- $this->sendInstallNotification($type);
- }
-
- return true;
- }
-
- /**
- * Called on installation.
- *
- * @param InstallerAdapter $adapter The object responsible for running this script
- *
- * @return boolean True on success
- *
- * @since 02.01.08
- */
- public function install(InstallerAdapter $adapter): bool
- {
- return true;
- }
-
- /**
- * Called on update.
- *
- * @param InstallerAdapter $adapter The object responsible for running this script
- *
- * @return boolean True on success
- *
- * @since 02.01.08
- */
- public function update(InstallerAdapter $adapter): bool
- {
- return true;
- }
-
- /**
- * Called on uninstallation.
- *
- * @param InstallerAdapter $adapter The object responsible for running this script
- *
- * @return boolean True on success
- *
- * @since 02.01.08
- */
- public function uninstall(InstallerAdapter $adapter): bool
- {
- $this->sendInstallNotification('uninstall');
- $this->uninstallLanguageOverrides();
- $this->unregisterActionLogExtension();
-
- return true;
- }
-
- /** Sentinel comment that marks the start of MokoWaaS overrides inside a Joomla override file. */
- /**
- * Enable, lock, and protect the plugin in #__extensions.
- *
- * Runs on both install and update so existing installs get locked.
+ * @param string $type Installation type
+ * @param InstallerAdapter $parent Parent installer
*
* @return void
*
- * @since 02.01.08
+ * @since 2.2.0
*/
- private function enableAndLockPlugin()
+ public function postflight($type, $parent)
{
- $db = Factory::getDbo();
- $db->setQuery(
- $db->getQuery(true)
+ if ($type === 'install' || $type === 'discover_install')
+ {
+ $this->enablePlugin('system', 'mokowaas');
+ $this->enablePlugin('webservices', 'mokowaas');
+ }
+ }
+
+ /**
+ * Enable a plugin by group and element.
+ *
+ * @param string $group Plugin group
+ * @param string $element Plugin element name
+ *
+ * @return void
+ *
+ * @since 2.2.0
+ */
+ private function enablePlugin(string $group, string $element): void
+ {
+ try
+ {
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
- ->set($db->quoteName('locked') . ' = 1')
- ->set($db->quoteName('protected') . ' = 1')
- ->where($db->quoteName('element') . ' = '
- . $db->quote('mokowaas'))
- ->where($db->quoteName('folder') . ' = '
- . $db->quote('system'))
- ->where($db->quoteName('type') . ' = '
- . $db->quote('plugin'))
- );
- $db->execute();
- }
-
- /**
- * Ensure MokoOnyx is installed, locked, and set as default.
- *
- * Always installs MokoOnyx from bundled payload if not present,
- * locks it, sets it as default site template, and unlocks
- * MokoCassiopeia so it can be uninstalled.
- *
- * @return void
- *
- * @since 02.00.03
- */
- private function ensureMokoCassiopeia()
- {
- $db = Factory::getDbo();
-
- // Check whether MokoOnyx is already installed
- $query = $db->getQuery(true)
- ->select($db->quoteName('extension_id'))
- ->from($db->quoteName('#__extensions'))
- ->where($db->quoteName('element') . ' = ' . $db->quote('mokoonyx'))
- ->where($db->quoteName('type') . ' = ' . $db->quote('template'));
- $onyx = $db->setQuery($query)->loadObject();
-
- // Install from payload if missing
- if (!$onyx)
- {
- $this->installMokoOnyxFromPayload();
-
- // Re-check after install
- $onyx = $db->setQuery($query)->loadObject();
- }
-
- if ($onyx)
- {
- // Lock and protect MokoOnyx
- $db->setQuery(
- $db->getQuery(true)
- ->update($db->quoteName('#__extensions'))
- ->set($db->quoteName('enabled') . ' = 1')
- ->set($db->quoteName('locked') . ' = 1')
- ->set($db->quoteName('protected') . ' = 1')
- ->where($db->quoteName('extension_id') . ' = ' . (int) $onyx->extension_id)
- )->execute();
-
- $this->setDefaultTemplate('mokoonyx', 0);
- }
-
- // Always unlock MokoCassiopeia (allow uninstall)
- $db->setQuery(
- $db->getQuery(true)
- ->update($db->quoteName('#__extensions'))
- ->set($db->quoteName('locked') . ' = 0')
- ->set($db->quoteName('protected') . ' = 0')
- ->where($db->quoteName('element') . ' = ' . $db->quote('mokocassiopeia'))
- ->where($db->quoteName('type') . ' = ' . $db->quote('template'))
- )->execute();
- }
-
- /**
- * Install MokoOnyx from the bundled payload zip.
- *
- * @return void
- *
- * @since 02.01.17
- */
- private function installMokoOnyxFromPayload()
- {
- $pluginPath = JPATH_PLUGINS . '/system/mokowaas';
- $payloadZip = $pluginPath . '/payload/mokoonyx.zip';
-
- if (!file_exists($payloadZip))
- {
- Factory::getApplication()->enqueueMessage(
- 'MokoOnyx payload not found. Please install MokoOnyx manually from '
- . 'Gitea Releases.',
- 'warning'
- );
-
- return;
- }
-
- $tmpDir = JPATH_ROOT . '/tmp/mokoonyx';
-
- try
- {
- $archive = new \Joomla\Archive\Archive();
- $archive->extract($payloadZip, $tmpDir);
-
- $installDir = $tmpDir;
-
- if (!file_exists($tmpDir . '/templateDetails.xml'))
- {
- $xmlFiles = glob($tmpDir . '/*/templateDetails.xml');
-
- if (!empty($xmlFiles))
- {
- $installDir = dirname($xmlFiles[0]);
- }
- else
- {
- Factory::getApplication()->enqueueMessage(
- 'MokoOnyx: templateDetails.xml not found in archive.',
- 'warning'
- );
-
- return;
- }
- }
-
- $installer = \Joomla\CMS\Installer\Installer::getInstance();
-
- if ($installer->install($installDir))
- {
- Factory::getApplication()->enqueueMessage(
- 'MokoOnyx installed successfully.',
- 'message'
- );
- }
- else
- {
- Factory::getApplication()->enqueueMessage(
- 'MokoOnyx installation from payload failed.',
- 'warning'
- );
- }
- }
- catch (\Exception $e)
- {
- Factory::getApplication()->enqueueMessage(
- 'MokoOnyx error: ' . $e->getMessage(),
- 'warning'
- );
- }
- finally
- {
- if (is_dir($tmpDir))
- {
- Folder::delete($tmpDir);
- }
- }
- }
-
- /**
- * Set a template as the default for a given client.
- *
- * @param string $template Template element name
- * @param int $clientId 0 = site, 1 = admin
- *
- * @return void
- *
- * @since 02.01.01
- */
- private function setDefaultTemplate($template, $clientId)
- {
- $db = Factory::getDbo();
-
- // Unset all other defaults for this client
- $db->setQuery(
- $db->getQuery(true)
- ->update($db->quoteName('#__template_styles'))
- ->set($db->quoteName('home') . ' = 0')
- ->where($db->quoteName('client_id') . ' = ' . $clientId)
- ->where($db->quoteName('home') . ' = 1')
- );
- $db->execute();
-
- // Set our template as default
- $db->setQuery(
- $db->getQuery(true)
- ->update($db->quoteName('#__template_styles'))
- ->set($db->quoteName('home') . ' = 1')
- ->where($db->quoteName('template') . ' = '
- . $db->quote($template))
- ->where($db->quoteName('client_id') . ' = ' . $clientId)
- );
- $db->execute();
- }
-
- /**
- * Parse an updates.xml and return the download URL.
- *
- * @param string $updatesUrl URL to the updates.xml file
- *
- * @return string|null Download URL or null on failure
- *
- * @since 02.01.01
- */
- /**
- * Send email notification on install or update.
- *
- * @param string $type install or update
- *
- * @return void
- *
- * @since 02.01.02
- */
- private function sendInstallNotification($type)
- {
- try
- {
- $config = Factory::getApplication()->getConfig();
- $siteName = $config->get('sitename', 'Joomla Site');
- $siteUrl = \Joomla\CMS\Uri\Uri::root();
- // Read version from manifest XML
- $manifestFile = JPATH_PLUGINS
- . '/system/mokowaas/mokowaas.xml';
- $version = '?.?.?';
-
- if (file_exists($manifestFile))
- {
- $xml = simplexml_load_file($manifestFile);
-
- if ($xml && isset($xml->version))
- {
- $version = (string) $xml->version;
- }
- }
-
- $mailer = Factory::getMailer();
- $mailer->addRecipient('webmaster@mokoconsulting.tech');
- $mailer->setSubject(
- sprintf('[%s] MokoWaaS %s — %s',
- $siteName, $type, $version)
- );
- $mailer->setBody(
- sprintf(
- "MokoWaaS plugin was %sd on %s\n\n"
- . "Version: %s\n"
- . "Site: %s\n"
- . "Time: %s\n"
- . "PHP: %s\n"
- . "Joomla: %s\n",
- $type,
- $siteName,
- $version,
- $siteUrl,
- date('Y-m-d H:i:s T'),
- PHP_VERSION,
- JVERSION
- )
- );
- $mailer->isHtml(false);
- $mailer->Send();
- }
- catch (\Exception $e)
- {
- // Don't break install if email fails
- }
- }
-
- private const BLOCK_START = '; ===== BEGIN MokoWaaS Overrides (do not edit this block) =====';
-
- /** Sentinel comment that marks the end of MokoWaaS overrides inside a Joomla override file. */
- private const BLOCK_END = '; ===== END MokoWaaS Overrides =====';
-
- /**
- * Build the placeholder → value map from the plugin's saved params.
- *
- * On first install the params row may not exist yet, so every value
- * falls back to a sensible default.
- *
- * @return array Associative array of placeholder => replacement value
- *
- * @since 02.01.08
- */
- private function getPlaceholders()
- {
- $params = $this->getPluginParams();
-
- return [
- '{{BRAND_NAME}}' => $params->get('brand_name', 'MokoWaaS'),
- '{{COMPANY_NAME}}' => $params->get('company_name', 'Moko Consulting'),
- '{{SUPPORT_URL}}' => $params->get('support_url', 'https://mokoconsulting.tech'),
- ];
- }
-
- /**
- * Load the plugin's saved params from the database.
- *
- * @return \Joomla\Registry\Registry
- *
- * @since 02.01.08
- */
- private function getPluginParams()
- {
- $db = Factory::getDbo();
- $query = $db->getQuery(true)
- ->select($db->quoteName('params'))
- ->from($db->quoteName('#__extensions'))
- ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
- ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
- ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'));
-
- $db->setQuery($query);
- $json = $db->loadResult();
-
- return new \Joomla\Registry\Registry($json ?: '{}');
- }
-
- /**
- * Resolve placeholders in an array of language strings.
- *
- * @param array $strings Key/value pairs (values may contain {{…}} tokens)
- *
- * @return array The same array with placeholders replaced
- *
- * @since 02.01.08
- */
- private function resolvePlaceholders(array $strings)
- {
- $placeholders = $this->getPlaceholders();
- $search = array_keys($placeholders);
- $replace = array_values($placeholders);
-
- foreach ($strings as $key => $value)
- {
- $strings[$key] = str_replace($search, $replace, $value);
- }
-
- return $strings;
- }
-
- /**
- * Install language override files to Joomla's global override directories.
- *
- * Reads each source override template shipped with the plugin, resolves
- * {{BRAND_NAME}} etc. from plugin params, then merges the resolved keys
- * into the destination file inside a clearly delimited block. Existing
- * overrides outside the block are never touched.
- *
- * @return void
- *
- * @since 02.01.08
- */
- private function installLanguageOverrides()
- {
- $app = Factory::getApplication();
- $pluginPath = JPATH_PLUGINS . '/system/mokowaas';
-
- $overrideSets = [
- // [source folder relative to plugin, Joomla destination base]
- ['language/overrides', JPATH_ROOT . '/language/overrides', 'frontend'],
- ['administrator/language/overrides', JPATH_ADMINISTRATOR . '/language/overrides', 'administrator'],
- ];
-
- foreach ($overrideSets as [$sourceDir, $destDir, $label])
- {
- foreach ($this->languageTags as $tag)
- {
- $source = $pluginPath . '/' . $sourceDir . '/' . $tag . '.override.ini';
- $dest = $destDir . '/' . $tag . '.override.ini';
-
- if (!file_exists($source))
- {
- continue;
- }
-
- if (!is_dir($destDir))
- {
- Folder::create($destDir);
- }
-
- $pluginOverrides = $this->resolvePlaceholders($this->parseLanguageFile($source));
-
- if (empty($pluginOverrides))
- {
- continue;
- }
-
- if ($this->mergeOverridesIntoFile($dest, $pluginOverrides))
- {
- $app->enqueueMessage(
- sprintf('Installed %s language overrides for %s', $label, $tag),
- 'message'
- );
- }
- else
- {
- $app->enqueueMessage(
- sprintf('Failed to install %s language overrides for %s', $label, $tag),
- 'warning'
- );
- }
- }
- }
- }
-
- /**
- * Update the mod_loginsupport module params to point to
- * Moko Consulting URLs at install time.
- *
- * @return void
- *
- * @since 02.01.08
- */
- private function updateLoginSupportUrls()
- {
- $db = Factory::getDbo();
- $query = $db->getQuery(true)
- ->select([$db->quoteName('id'), $db->quoteName('params')])
- ->from($db->quoteName('#__modules'))
- ->where($db->quoteName('module') . ' = '
- . $db->quote('mod_loginsupport'));
-
- $db->setQuery($query);
- $modules = $db->loadObjectList();
-
- if (empty($modules))
- {
- return;
- }
-
- $supportUrls = [
- 'forum_url' => 'https://mokoconsulting.tech/support',
- 'documentation_url' => 'https://mokoconsulting.tech/kb',
- 'news_url' => 'https://mokoconsulting.tech/news',
- ];
-
- foreach ($modules as $module)
- {
- $params = new \Joomla\Registry\Registry(
- $module->params ?: '{}'
- );
-
- foreach ($supportUrls as $key => $url)
- {
- $params->set($key, $url);
- }
-
- $update = $db->getQuery(true)
- ->update($db->quoteName('#__modules'))
- ->set($db->quoteName('params') . ' = '
- . $db->quote($params->toString()))
- ->where($db->quoteName('id') . ' = '
- . (int) $module->id);
-
- $db->setQuery($update);
+ ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
+ ->where($db->quoteName('folder') . ' = ' . $db->quote($group))
+ ->where($db->quoteName('element') . ' = ' . $db->quote($element));
+ $db->setQuery($query);
$db->execute();
}
-
- Factory::getApplication()->enqueueMessage(
- 'Updated login support URLs.', 'message'
- );
- }
-
- /**
- * Set Atum admin template branding params at install time.
- *
- * @return void
- *
- * @since 02.01.08
- */
- private function updateAtumBranding()
- {
- $mediaBase = 'media/plg_system_mokowaas/';
-
- $expected = [
- 'logoBrandLarge' => $mediaBase . 'logo.png',
- 'logoBrandSmall' => $mediaBase . 'favicon_256.png',
- 'loginLogo' => $mediaBase . 'logo.png',
- 'logoBrandLargeAlt' => '',
- 'logoBrandSmallAlt' => '',
- 'loginLogoAlt' => '',
- 'emptyLogoBrandLargeAlt' => '1',
- 'emptyLogoBrandSmallAlt' => '1',
- 'emptyLoginLogoAlt' => '1',
- 'hue' => 'hsl(219, 44%, 18%)',
- 'special-color' => '#1a2744',
- 'link-color' => '#0051ad',
- ];
-
- $db = Factory::getDbo();
- $query = $db->getQuery(true)
- ->select([$db->quoteName('id'), $db->quoteName('params')])
- ->from($db->quoteName('#__template_styles'))
- ->where($db->quoteName('template') . ' = '
- . $db->quote('atum'))
- ->where($db->quoteName('client_id') . ' = 1');
-
- $db->setQuery($query);
- $styles = $db->loadObjectList();
-
- if (empty($styles))
+ catch (\Throwable $e)
{
- return;
+ Log::add('Error enabling plugin ' . $group . '/' . $element . ': ' . $e->getMessage(), Log::WARNING, 'jerror');
}
-
- foreach ($styles as $style)
- {
- $params = new \Joomla\Registry\Registry(
- $style->params ?: '{}'
- );
-
- foreach ($expected as $key => $value)
- {
- $params->set($key, $value);
- }
-
- $update = $db->getQuery(true)
- ->update($db->quoteName('#__template_styles'))
- ->set($db->quoteName('params') . ' = '
- . $db->quote($params->toString()))
- ->where($db->quoteName('id') . ' = '
- . (int) $style->id);
-
- $db->setQuery($update);
- $db->execute();
- }
-
- Factory::getApplication()->enqueueMessage(
- 'Updated Atum template branding.', 'message'
- );
- }
-
- /**
- * Register the plugin in #__action_logs_extensions so it appears
- * as a filterable extension in System > Action Logs.
- *
- * @return void
- *
- * @since 02.01.08
- */
- /**
- * Provision health endpoint with Grafana if configured.
- *
- * On install/update, if the health endpoint is enabled and Grafana
- * credentials are set, generates a token (if missing) and triggers
- * Grafana datasource provisioning via cURL. All data is passed as
- * structured JSON — no shell commands are invoked.
- *
- * @return void
- *
- * @since 02.01.22
- */
- private function provisionHealthEndpoint()
- {
- $db = Factory::getDbo();
-
- $query = $db->getQuery(true)
- ->select($db->quoteName('params'))
- ->from($db->quoteName('#__extensions'))
- ->where($db->quoteName('element') . ' = '
- . $db->quote('mokowaas'))
- ->where($db->quoteName('folder') . ' = '
- . $db->quote('system'));
-
- $db->setQuery($query);
- $rawParams = $db->loadResult();
-
- if (empty($rawParams))
- {
- return;
- }
-
- $params = new \Joomla\Registry\Registry($rawParams);
- $changed = false;
-
- // Auto-generate token if missing
- if (empty($params->get('health_api_token', '')))
- {
- $params->set(
- 'health_api_token',
- bin2hex(random_bytes(32))
- );
- $changed = true;
- }
-
- if ($changed)
- {
- $db->setQuery(
- $db->getQuery(true)
- ->update($db->quoteName('#__extensions'))
- ->set($db->quoteName('params') . ' = '
- . $db->quote($params->toString()))
- ->where($db->quoteName('element') . ' = '
- . $db->quote('mokowaas'))
- ->where($db->quoteName('folder') . ' = '
- . $db->quote('system'))
- );
- $db->execute();
- }
-
- // Heartbeat receiver — register with Grafana provisioning
- $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
- $siteName = Factory::getConfig()->get('sitename', 'Joomla');
- $token = $params->get('health_api_token', '');
-
- $payload = json_encode([
- 'site_url' => $siteUrl,
- 'site_name' => $siteName,
- 'health_token' => $token,
- 'action' => 'register',
- ], JSON_UNESCAPED_SLASHES);
-
- $ch = curl_init('https://bench.mokoconsulting.tech/api/waas-heartbeat/register');
- curl_setopt($ch, CURLOPT_POST, true);
- curl_setopt($ch, CURLOPT_HTTPHEADER, [
- 'Content-Type: application/json',
- 'X-MokoWaaS-Key: moko-waas-hb-2026-x9k4m',
- ]);
- curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($ch, CURLOPT_TIMEOUT, 30);
- curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
-
- $response = curl_exec($ch);
- $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
- $error = curl_error($ch);
- curl_close($ch);
-
- $app = Factory::getApplication();
- $body = json_decode($response, true);
-
- if ($error)
- {
- $app->enqueueMessage('Grafana heartbeat failed: ' . $error, 'warning');
- Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas');
- }
- elseif ($code === 200)
- {
- $status = $body['status'] ?? 'ok';
- $app->enqueueMessage(
- 'Grafana heartbeat: ' . $status . ' (' . ($body['ds_uid'] ?? '') . ')',
- 'message'
- );
- }
- else
- {
- $msg = sprintf('Grafana heartbeat failed: HTTP %d — %s',
- $code, $body['error'] ?? $body['message'] ?? 'Unknown');
- $app->enqueueMessage($msg, 'warning');
- Log::add($msg, Log::WARNING, 'mokowaas');
- }
- }
-
- private function registerActionLogExtension()
- {
- $db = Factory::getDbo();
- $query = $db->getQuery(true)
- ->select('COUNT(*)')
- ->from($db->quoteName('#__action_logs_extensions'))
- ->where($db->quoteName('extension') . ' = '
- . $db->quote('plg_system_mokowaas'));
-
- $db->setQuery($query);
-
- if ((int) $db->loadResult() > 0)
- {
- return;
- }
-
- $row = (object) ['extension' => 'plg_system_mokowaas'];
- $db->insertObject('#__action_logs_extensions', $row);
-
- Factory::getApplication()->enqueueMessage(
- 'Registered MokoWaaS in Action Logs.', 'message'
- );
-
- // Register content type config for display formatting
- $configQuery = $db->getQuery(true)
- ->select('COUNT(*)')
- ->from($db->quoteName('#__action_log_config'))
- ->where($db->quoteName('type_alias') . ' = '
- . $db->quote('plg_system_mokowaas'));
-
- $db->setQuery($configQuery);
-
- if ((int) $db->loadResult() === 0)
- {
- $config = (object) [
- 'type_title' => 'MokoWaaS',
- 'type_alias' => 'plg_system_mokowaas',
- 'id_holder' => '',
- 'title_holder' => '',
- 'table_name' => '',
- 'text_prefix' => 'PLG_SYSTEM_MOKOWAAS',
- ];
-
- $db->insertObject('#__action_log_config', $config);
- }
- }
-
- /**
- * Remove the plugin from #__action_logs_extensions on uninstall.
- *
- * @return void
- *
- * @since 02.01.08
- */
- private function unregisterActionLogExtension()
- {
- $db = Factory::getDbo();
-
- $db->setQuery(
- $db->getQuery(true)
- ->delete($db->quoteName('#__action_logs_extensions'))
- ->where($db->quoteName('extension') . ' = '
- . $db->quote('plg_system_mokowaas'))
- );
- $db->execute();
-
- $db->setQuery(
- $db->getQuery(true)
- ->delete($db->quoteName('#__action_log_config'))
- ->where($db->quoteName('type_alias') . ' = '
- . $db->quote('plg_system_mokowaas'))
- );
- $db->execute();
- }
-
- /**
- * Remove only MokoWaaS overrides from Joomla's global override files.
- *
- * Strips the delimited MokoWaaS block and any duplicate keys that appear
- * outside the block (safety net for upgrades from older versions that wrote
- * keys inline). All other content is preserved verbatim.
- *
- * @return void
- *
- * @since 02.01.08
- */
- private function uninstallLanguageOverrides()
- {
- $app = Factory::getApplication();
- $pluginPath = JPATH_PLUGINS . '/system/mokowaas';
-
- $overrideSets = [
- ['language/overrides', JPATH_ROOT . '/language/overrides', 'frontend'],
- ['administrator/language/overrides', JPATH_ADMINISTRATOR . '/language/overrides', 'administrator'],
- ];
-
- foreach ($overrideSets as [$sourceDir, $destDir, $label])
- {
- foreach ($this->languageTags as $tag)
- {
- $source = $pluginPath . '/' . $sourceDir . '/' . $tag . '.override.ini';
- $dest = $destDir . '/' . $tag . '.override.ini';
-
- if (!file_exists($dest))
- {
- continue;
- }
-
- $pluginKeys = array_keys($this->parseLanguageFile($source));
-
- if ($this->removeOverridesFromFile($dest, $pluginKeys))
- {
- $app->enqueueMessage(
- sprintf('Removed %s language overrides for %s', $label, $tag),
- 'message'
- );
- }
- }
- }
- }
-
- /**
- * Merge plugin overrides into an existing Joomla override file.
- *
- * The method:
- * 1. Reads the destination file (if it exists) and preserves every line.
- * 2. Strips any previous MokoWaaS block so it can be rewritten cleanly.
- * 3. Collects keys that exist outside the block (user-set overrides).
- * 4. Appends a MokoWaaS block containing only keys NOT already
- * defined by the user — existing customisations are never touched.
- *
- * @param string $dest Absolute path to the Joomla override file
- * @param array $overrides Key/value pairs to inject
- *
- * @return boolean True on success
- *
- * @since 02.01.08
- */
- private function mergeOverridesIntoFile($dest, array $overrides)
- {
- $existingLines = [];
-
- if (file_exists($dest))
- {
- $existingLines = file($dest, FILE_IGNORE_NEW_LINES);
- }
-
- // Strip any previous MokoWaaS block
- $existingLines = $this->stripMokoWaaSBlock($existingLines);
-
- // Collect keys already defined outside the block (user overrides)
- $userKeys = [];
-
- foreach ($existingLines as $line)
- {
- $trimmed = trim($line);
-
- if ($trimmed !== '' && $trimmed[0] !== ';')
- {
- if (preg_match('/^([A-Z0-9_]+)\s*=/i', $trimmed, $m))
- {
- $userKeys[] = strtoupper($m[1]);
- }
- }
- }
-
- // Remove trailing blank lines so the block starts cleanly
- while (!empty($existingLines)
- && trim(end($existingLines)) === '')
- {
- array_pop($existingLines);
- }
-
- // Build the MokoWaaS block — skip keys the user already set
- $block = [];
- $block[] = '';
- $block[] = self::BLOCK_START;
- $block[] = '; Auto-generated on '
- . date('Y-m-d H:i:s') . ' — do not edit manually.';
-
- foreach ($overrides as $key => $value)
- {
- if (!in_array(strtoupper($key), $userKeys, true))
- {
- $block[] = strtoupper($key) . '="' . $value . '"';
- }
- }
-
- $block[] = self::BLOCK_END;
- $block[] = '';
-
- $content = implode("\n", array_merge($existingLines, $block));
-
- return File::write($dest, $content);
- }
-
- /**
- * Remove MokoWaaS overrides from an existing Joomla override file.
- *
- * Strips the delimited block and any stray keys that match, then rewrites
- * the file. If the file would be empty (or comments-only) it is deleted.
- *
- * @param string $dest Absolute path to the override file
- * @param array $keys The override keys to remove (uppercase)
- *
- * @return boolean True on success
- *
- * @since 02.01.08
- */
- private function removeOverridesFromFile($dest, array $keys)
- {
- if (!file_exists($dest))
- {
- return true;
- }
-
- $lines = file($dest, FILE_IGNORE_NEW_LINES);
-
- // Strip the MokoWaaS block
- $lines = $this->stripMokoWaaSBlock($lines);
-
- // Also strip any stray keys that match (legacy installs)
- $upperKeys = array_map('strtoupper', $keys);
- $cleaned = [];
-
- foreach ($lines as $line)
- {
- $trimmed = trim($line);
-
- if ($trimmed !== '' && $trimmed[0] !== ';')
- {
- if (preg_match('/^([A-Z0-9_]+)\s*=/i', $trimmed, $m))
- {
- if (in_array(strtoupper($m[1]), $upperKeys, true))
- {
- continue;
- }
- }
- }
-
- $cleaned[] = $line;
- }
-
- // Check whether any real keys remain
- $hasKeys = false;
-
- foreach ($cleaned as $line)
- {
- $trimmed = trim($line);
-
- if ($trimmed !== '' && $trimmed[0] !== ';')
- {
- $hasKeys = true;
- break;
- }
- }
-
- if (!$hasKeys)
- {
- return File::delete($dest);
- }
-
- return File::write($dest, implode("\n", $cleaned) . "\n");
- }
-
- /**
- * Remove the MokoWaaS sentinel block from an array of file lines.
- *
- * @param array $lines Lines of the file (no trailing newlines)
- *
- * @return array Lines with the block removed
- *
- * @since 02.01.08
- */
- private function stripMokoWaaSBlock(array $lines)
- {
- $out = [];
- $inBlock = false;
-
- foreach ($lines as $line)
- {
- if (trim($line) === self::BLOCK_START)
- {
- $inBlock = true;
- continue;
- }
-
- if (trim($line) === self::BLOCK_END)
- {
- $inBlock = false;
- continue;
- }
-
- if (!$inBlock)
- {
- $out[] = $line;
- }
- }
-
- return $out;
- }
-
- /**
- * Parse a language INI file and return the strings as an associative array.
- *
- * @param string $filePath The path to the language file
- *
- * @return array Array of language strings (key => value)
- *
- * @since 02.01.08
- */
- private function parseLanguageFile($filePath)
- {
- $strings = [];
-
- if (!file_exists($filePath))
- {
- return $strings;
- }
-
- $content = file_get_contents($filePath);
- $lines = explode("\n", $content);
-
- foreach ($lines as $line)
- {
- $line = trim($line);
-
- // Skip empty lines and comments
- if ($line === '' || $line[0] === ';')
- {
- continue;
- }
-
- // Parse KEY="VALUE" format
- if (preg_match('/^([A-Z0-9_]+)="(.+)"$/i', $line, $matches))
- {
- $strings[strtoupper($matches[1])] = $matches[2];
- }
- }
-
- return $strings;
}
}
diff --git a/updates.xml b/updates.xml
index de83745..e65d592 100644
--- a/updates.xml
+++ b/updates.xml
@@ -23,21 +23,20 @@
https://mokoconsulting.tech
- System - MokoWaaS
- System - MokoWaaS update (development)
- mokowaas
- plugin
- 02.01.44-dev
- site
- system
- development
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development
+ MokoWaaS
+ MokoWaaS development build.
+ pkg_mokowaas
+ package
+ 02.01.46
+ 2026-05-24
+ https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.44-dev.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.01.46-dev.zip
- 134158cce6b179a68477f35400f54e21f336bceae7b4a6197af3cb80db458a5a
-
+ 2b0f59fa377f1563a54b2fbb389cee50225865541b0d3afecb565032f2afcb3c
+ development
Moko Consulting
https://mokoconsulting.tech
+