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 -[![Version](https://img.shields.io/badge/version-02.01.43-blue.svg?logo=v&logoColor=white)](https://github.com/mokoconsulting-tech/MokoWaaS/releases/tag/v02) +[![Version](https://img.shields.io/badge/version-02.03.00-blue.svg?logo=v&logoColor=white)](https://github.com/mokoconsulting-tech/MokoWaaS/releases/tag/v02) [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE) [![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org) [![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](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 +