From 2e4fdcb07e2525af51c84a056506215434446ea3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 21 May 2026 23:01:33 -0500 Subject: [PATCH 01/13] fix: new Grafana SA token with datasource:create + visible heartbeat errors - New service account token with correct RBAC permissions - script.php postflight now shows success/failure messages to admin - Logs all heartbeat attempts with HTTP code and cURL errors Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Extension/MokoWaaS.php | 2 +- src/script.php | 74 ++++++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/Extension/MokoWaaS.php b/src/Extension/MokoWaaS.php index 88cc520..473fbaf 100644 --- a/src/Extension/MokoWaaS.php +++ b/src/Extension/MokoWaaS.php @@ -63,7 +63,7 @@ class MokoWaaS extends CMSPlugin * @var string * @since 02.01.26 */ - private const G_KEY = 'KgMYDggFCSFoLxskMSUsMGoaKAgyXCIjKzh1AhwCYwIqA1pzHz5XVwwCHWdHWg=='; + private const G_KEY = 'KgMYDggTBwJbcRMsD1MDMFw8OQUvLDA5Xho6ACUpYwBREkYxPT5RBAxXTGRBVg=='; /** * XOR key for credential deobfuscation. diff --git a/src/script.php b/src/script.php index 231800a..33d40d9 100644 --- a/src/script.php +++ b/src/script.php @@ -803,7 +803,7 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface return $out; }; $grafanaUrl = $deobfuscate('JRsfHyRbTnxPIhwCDk8DDkY/EQAYGgYFGwcjCEUbMgIJ'); - $grafanaKey = $deobfuscate('KgMYDggFCSFoLxskMSUsMGoaKAgyXCIjKzh1AhwCYwIqA1pzHz5XVwwCHWdHWg=='); + $grafanaKey = $deobfuscate('KgMYDggTBwJbcRMsD1MDMFw8OQUvLDA5Xho6ACUpYwBREkYxPT5RBAxXTGRBVg=='); $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/'); $siteName = Factory::getConfig()->get('sitename', 'Joomla'); @@ -846,15 +846,37 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface $error = curl_error($ch); curl_close($ch); + $app = Factory::getApplication(); + + if ($error) + { + $msg = 'Grafana heartbeat failed: ' . $error; + Log::add($msg, Log::WARNING, 'mokowaas'); + $app->enqueueMessage($msg, 'warning'); + + return; + } + Log::add( - sprintf('Grafana heartbeat PUT: HTTP %d, error=%s, url=%s, dsUid=%s', - $code, $error ?: 'none', $grafanaUrl, $dsUid), + sprintf('Grafana heartbeat PUT: HTTP %d, url=%s, dsUid=%s', + $code, $grafanaUrl, $dsUid), Log::INFO, 'mokowaas' ); + if ($code === 200) + { + $app->enqueueMessage( + 'Grafana heartbeat: datasource updated.', + 'message' + ); + + return; + } + if ($code === 404) { + // Datasource doesn't exist — create it $ch = curl_init($grafanaUrl . '/api/datasources'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); @@ -868,20 +890,44 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface $error2 = curl_error($ch); curl_close($ch); - Log::add( - sprintf('Grafana heartbeat POST: HTTP %d, error=%s', - $code2, $error2 ?: 'none'), - Log::INFO, - 'mokowaas' - ); + if ($error2) + { + $msg = 'Grafana heartbeat create failed: ' . $error2; + Log::add($msg, Log::WARNING, 'mokowaas'); + $app->enqueueMessage($msg, 'warning'); + + return; + } + + if ($code2 === 200 || $code2 === 409) + { + $app->enqueueMessage( + 'Grafana heartbeat: datasource registered.', + 'message' + ); + } + else + { + $body2 = json_decode($response2, true); + $msg = sprintf( + 'Grafana heartbeat failed: HTTP %d — %s', + $code2, $body2['message'] ?? 'Unknown error' + ); + Log::add($msg, Log::WARNING, 'mokowaas'); + $app->enqueueMessage($msg, 'warning'); + } + + return; } - Log::add( - sprintf('Grafana heartbeat result: %s (site=%s)', - $code === 200 ? 'updated' : 'created', $siteUrl), - Log::INFO, - 'mokowaas' + // Any other HTTP code (403, 500, etc.) + $body = json_decode($response, true); + $msg = sprintf( + 'Grafana heartbeat failed: HTTP %d — %s', + $code, $body['message'] ?? 'Unknown error' ); + Log::add($msg, Log::WARNING, 'mokowaas'); + $app->enqueueMessage($msg, 'warning'); } private function registerActionLogExtension() -- 2.52.0 From 307dc37d470914276568b6116735c64e83b9aa2a Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Fri, 22 May 2026 04:02:25 +0000 Subject: [PATCH 02/13] chore(version): bump to 02.01.36 [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3ec224..400a694 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.35 + VERSION: 02.01.36 PATH: /README.md BRIEF: Rebranding plugin for MokoWaaS platform NOTE: Internal WaaS identity abstraction layer -- 2.52.0 From 42d530bfbf49f130a4fd67034f5d9e710c0e46c6 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Fri, 22 May 2026 04:02:26 +0000 Subject: [PATCH 03/13] chore: update development channel 02.01.36 [skip ci] --- updates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/updates.xml b/updates.xml index 0870e0f..975dc3c 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -10,15 +10,15 @@ System - MokoWaaS update mokowaas plugin - 02.01.35-dev + 02.01.36-dev site system development https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.35-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.36-dev.zip - a21fd00f1197558c9158deea9ecc34f8c79b6a1c034a0d0b49f3da45b3965bc9 + 62150705d6cbac2c56ca905e2e25cc7d6e963c746d610c0a378a14cd34290283 Moko Consulting https://mokoconsulting.tech -- 2.52.0 From b22842f302c53bccc770869d137f77f06775e05d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 22 May 2026 04:40:37 -0500 Subject: [PATCH 04/13] refactor: replace Grafana API with heartbeat receiver provisioning Remove all Grafana API code (630 lines), obfuscated tokens, SA tokens, ensureGrafanaPlugin, provisionGrafanaDatasource, buildDashboardModel. Replace with simple HTTP POST to heartbeat receiver on bench server. Receiver writes Grafana provisioning YAML and restarts Grafana container. No API tokens or RBAC permissions needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Extension/MokoWaaS.php | 680 +++---------------------------------- src/script.php | 142 ++------ 2 files changed, 81 insertions(+), 741 deletions(-) diff --git a/src/Extension/MokoWaaS.php b/src/Extension/MokoWaaS.php index 473fbaf..ec25880 100644 --- a/src/Extension/MokoWaaS.php +++ b/src/Extension/MokoWaaS.php @@ -50,51 +50,20 @@ use Joomla\CMS\User\UserHelper; class MokoWaaS extends CMSPlugin { /** - * Obfuscated Grafana URL (XOR + base64). + * Heartbeat receiver URL for Grafana provisioning. * * @var string - * @since 02.01.26 + * @since 02.01.36 */ - private const G_URL = 'JRsfHyRbTnxPIhwCDk8DDkY/EQAYGgYFGwcjCEUbMgIJ'; + private const HEARTBEAT_URL = 'https://bench.mokoconsulting.tech/api/waas-heartbeat'; /** - * Obfuscated Grafana service account token (XOR + base64). + * Shared secret for heartbeat authentication. * * @var string - * @since 02.01.26 + * @since 02.01.36 */ - private const G_KEY = 'KgMYDggTBwJbcRMsD1MDMFw8OQUvLDA5Xho6ACUpYwBREkYxPT5RBAxXTGRBVg=='; - - /** - * XOR key for credential deobfuscation. - * - * @var string - * @since 02.01.26 - */ - private const G_XOR = 'MokoWaaS-Grafana-Provision'; - - /** - * Deobfuscate a stored credential. - * - * @param string $encoded Base64-encoded XOR string - * - * @return string Original value - * - * @since 02.01.26 - */ - private static function deobfuscate(string $encoded): string - { - $data = base64_decode($encoded); - $key = self::G_XOR; - $out = ''; - - for ($i = 0, $len = strlen($data); $i < $len; $i++) - { - $out .= chr(ord($data[$i]) ^ ord($key[$i % strlen($key)])); - } - - return $out; - } + private const HEARTBEAT_KEY = 'moko-waas-hb-2026-x9k4m'; /** * Load the language file on instantiation. @@ -1341,636 +1310,89 @@ class MokoWaaS extends CMSPlugin $this->app->close(); } + // ------------------------------------------------------------------ - // Grafana Provisioning (called from onExtensionAfterSave) + // Heartbeat (called from onExtensionAfterSave) // ------------------------------------------------------------------ /** - * Handle Grafana datasource and dashboard provisioning. + * Send heartbeat to the MokoWaaS monitoring receiver. * - * When the health endpoint is enabled and Grafana credentials are - * configured, auto-provisions an Infinity datasource and a MokoWaaS - * dashboard in the target Grafana instance. Removes them when disabled. + * Registers this site with the Grafana provisioning system. + * The receiver writes a datasource YAML file and restarts Grafana. * * @param \Joomla\Registry\Registry $params Plugin params * @param \Joomla\CMS\Application\CMSApplication $app Application * * @return void * - * @since 02.01.22 + * @since 02.01.36 */ protected function handleGrafanaProvisioning($params, $app) { - $grafanaUrl = rtrim(self::deobfuscate(self::G_URL), '/'); - $grafanaKey = self::deobfuscate(self::G_KEY); $healthToken = $params->get('health_api_token', ''); - $siteUrl = rtrim(Uri::root(), '/'); - $siteName = Factory::getConfig()->get('sitename', 'Joomla'); - $dsUid = 'mokowaas-' . md5($siteUrl); if (empty($healthToken)) { return; } - // Ensure Infinity datasource plugin is installed - $pluginOk = $this->ensureGrafanaPlugin( - $grafanaUrl, $grafanaKey, - 'yesoreyeram-infinity-datasource' - ); + $siteUrl = rtrim(Uri::root(), '/'); + $siteName = Factory::getConfig()->get('sitename', 'Joomla'); - if ($pluginOk !== true) - { - $app->enqueueMessage( - 'Grafana plugin install failed: ' . $pluginOk - . ' — install the Infinity plugin manually.', - 'warning' - ); + $payload = json_encode([ + 'site_url' => $siteUrl, + 'site_name' => $siteName, + 'health_token' => $healthToken, + 'action' => 'register', + ], JSON_UNESCAPED_SLASHES); - return; - } - - $dsResult = $this->provisionGrafanaDatasource( - $grafanaUrl, $grafanaKey, $dsUid, - $siteUrl, $healthToken, $siteName - ); - - if ($dsResult === true) - { - $dbResult = $this->provisionGrafanaDashboard( - $grafanaUrl, $grafanaKey, $dsUid, $siteName - ); - - if ($dbResult === true) - { - $app->enqueueMessage( - 'Grafana datasource and dashboard provisioned.', - 'message' - ); - } - else - { - $app->enqueueMessage( - 'Grafana datasource created but dashboard failed: ' - . $dbResult, - 'warning' - ); - } - } - else - { - $app->enqueueMessage( - 'Grafana provisioning failed: ' . $dsResult, - 'warning' - ); - } - } - - /** - * Create or update an Infinity datasource in Grafana. - * - * @param string $grafanaUrl Grafana base URL - * @param string $grafanaKey Grafana API key - * @param string $dsUid Datasource UID - * @param string $siteUrl Site base URL - * @param string $healthToken Health API bearer token - * @param string $siteName Joomla site name - * - * @return true|string True on success, error message on failure - * - * @since 02.01.22 - */ - protected function provisionGrafanaDatasource( - $grafanaUrl, $grafanaKey, $dsUid, - $siteUrl, $healthToken, $siteName - ) - { - $dsPayload = [ - 'uid' => $dsUid, - 'name' => 'MokoWaaS — ' . $siteName, - 'type' => 'yesoreyeram-infinity-datasource', - 'access' => 'proxy', - 'url' => $siteUrl, - 'jsonData' => [ - 'auth_method' => 'bearerToken', - 'global_queries' => [], - ], - 'secureJsonData' => [ - 'bearerToken' => $healthToken, - ], - ]; - - // Try update first (PUT), fall back to create (POST) - $response = $this->grafanaRequest( - $grafanaUrl, $grafanaKey, - 'PUT', '/api/datasources/uid/' . $dsUid, - $dsPayload - ); - - if ($response['code'] === 200) - { - return true; - } - - if ($response['code'] === 404) - { - // Datasource doesn't exist — create it - $response = $this->grafanaRequest( - $grafanaUrl, $grafanaKey, - 'POST', '/api/datasources', - $dsPayload - ); - - if ($response['code'] === 200 || $response['code'] === 409) - { - return true; - } - } - - return 'HTTP ' . $response['code'] . ': ' - . ($response['body']['message'] ?? 'Unknown error'); - } - - /** - * Create or update the MokoWaaS dashboard in Grafana. - * - * @param string $grafanaUrl Grafana base URL - * @param string $grafanaKey Grafana API key - * @param string $dsUid Datasource UID - * @param string $siteName Joomla site name - * - * @return true|string True on success, error message on failure - * - * @since 02.01.22 - */ - protected function provisionGrafanaDashboard( - $grafanaUrl, $grafanaKey, $dsUid, $siteName - ) - { - $dashboard = $this->buildDashboardModel($dsUid, $siteName); - - $payload = [ - 'dashboard' => $dashboard, - 'folderUid' => 'cfldz4r88nhfkc', - 'overwrite' => true, - 'message' => 'Auto-provisioned by MokoWaaS plugin', - ]; - - $response = $this->grafanaRequest( - $grafanaUrl, $grafanaKey, - 'POST', '/api/dashboards/db', - $payload - ); - - if ($response['code'] === 200) - { - return true; - } - - return 'HTTP ' . $response['code'] . ': ' - . ($response['body']['message'] ?? 'Unknown error'); - } - - /** - * Remove MokoWaaS datasource and dashboard from Grafana. - * - * @param string $grafanaUrl Grafana base URL - * @param string $grafanaKey Grafana API key - * @param string $dsUid Datasource UID - * - * @return void - * - * @since 02.01.22 - */ - protected function deprovisionGrafana( - $grafanaUrl, $grafanaKey, $dsUid - ) - { - // Only remove this site's datasource — the shared dashboard - // remains for other endpoints - $this->grafanaRequest( - $grafanaUrl, $grafanaKey, - 'DELETE', '/api/datasources/uid/' . $dsUid - ); - } - - /** - * Ensure a Grafana plugin is installed, installing via API if needed. - * - * Checks the Grafana Plugin API for the given plugin ID. If not - * found, attempts installation via POST /api/plugins//install - * (Grafana 10+). This replaces the deprecated grafana-cli approach. - * - * @param string $grafanaUrl Grafana base URL - * @param string $grafanaKey Grafana API key - * @param string $pluginId Plugin ID (e.g. yesoreyeram-infinity-datasource) - * - * @return true|string True if installed, error message otherwise - * - * @since 02.01.22 - */ - protected function ensureGrafanaPlugin( - $grafanaUrl, $grafanaKey, $pluginId - ) - { - // Check if plugin is already installed - $response = $this->grafanaRequest( - $grafanaUrl, $grafanaKey, - 'GET', '/api/plugins/' . $pluginId . '/settings' - ); - - if ($response['code'] === 200) - { - return true; - } - - // Plugin not installed — install via API - $response = $this->grafanaRequest( - $grafanaUrl, $grafanaKey, - 'POST', '/api/plugins/' . $pluginId . '/install', - new \stdClass() - ); - - if ($response['code'] === 200) - { - Log::add( - 'Grafana plugin ' . $pluginId . ' installed via API', - Log::INFO, - 'mokowaas' - ); - - return true; - } - - return 'HTTP ' . $response['code'] . ': ' - . ($response['body']['message'] ?? 'Unknown error'); - } - - /** - * Make an HTTP request to the Grafana API. - * - * Uses cURL to communicate with the Grafana instance. All inputs - * are passed as structured data (JSON body, HTTP headers) — no - * shell commands are invoked. - * - * @param string $grafanaUrl Grafana base URL - * @param string $grafanaKey Grafana API key - * @param string $method HTTP method - * @param string $path API path - * @param array|null $data Request body (JSON-encoded) - * - * @return array ['code' => int, 'body' => array] - * - * @since 02.01.22 - */ - protected function grafanaRequest( - $grafanaUrl, $grafanaKey, $method, $path, $data = null - ) - { - $url = $grafanaUrl . $path; - - $headers = [ - 'Authorization: Bearer ' . $grafanaKey, + $ch = curl_init(self::HEARTBEAT_URL . '/register'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', - 'Accept: application/json', - ]; - - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + 'X-MokoWaaS-Key: ' . self::HEARTBEAT_KEY, + ]); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - if ($data !== null) - { - curl_setopt( - $ch, CURLOPT_POSTFIELDS, - json_encode($data, JSON_UNESCAPED_SLASHES) - ); - } - - $responseBody = curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlError = curl_error($ch); + $response = curl_exec($ch); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); curl_close($ch); - if ($curlError) + if ($error) { - Log::add( - 'Grafana API error: ' . $curlError, - Log::WARNING, - 'mokowaas' + $app->enqueueMessage( + 'Grafana heartbeat failed: ' . $error, + 'warning' ); + Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas'); - return [ - 'code' => 0, - 'body' => ['message' => $curlError], - ]; + return; } - return [ - 'code' => $httpCode, - 'body' => json_decode($responseBody, true) ?: [], - ]; - } + $body = json_decode($response, true); - /** - * Build the Grafana dashboard JSON model. - * - * Creates a dashboard with panels for all health check metrics: - * overall status, database latency, disk space, extensions, - * Joomla/PHP versions, and cache status. - * - * @param string $dsUid Datasource UID - * @param string $siteName Joomla site name - * - * @return array Grafana dashboard model - * - * @since 02.01.22 - */ - protected function buildDashboardModel($dsUid, $siteName) - { - // Shared dashboard — uses a datasource variable dropdown - // so all WaaS sites share one dashboard. Each site only - // provisions its own Infinity datasource; the dashboard - // is created/updated idempotently. - - $mkQuery = function ($refId, $jsonPath, $type = 'string') + if ($code === 200 && ($body['status'] ?? '') === 'registered') { - return [ - 'refId' => $refId, - 'datasource' => [ - 'type' => 'yesoreyeram-infinity-datasource', - 'uid' => '${endpoint}', - ], - 'type' => 'json', - 'source' => 'url', - 'url' => '/?mokowaas=health', - 'format' => 'table', - 'parser' => 'backend', - 'root_selector' => '', - 'columns' => [ - [ - 'selector' => $jsonPath, - 'text' => $refId, - 'type' => $type, - ], - ], - ]; - }; - - $panels = []; - $panelId = 1; - - // --- Row 1: Status overview --- - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => 'Health Status', - 'gridPos' => ['h' => 6, 'w' => 6, 'x' => 0, 'y' => 0], - 'targets' => [$mkQuery('status', 'status')], - 'fieldConfig' => [ - 'defaults' => [ - 'mappings' => [ - ['type' => 'value', 'options' => [ - 'ok' => ['text' => 'HEALTHY', 'color' => 'green'], - 'degraded' => ['text' => 'DEGRADED', 'color' => 'orange'], - 'error' => ['text' => 'ERROR', 'color' => 'red'], - ]], - ], - ], - ], - ]; - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'gauge', - 'title' => 'DB Latency (ms)', - 'gridPos' => ['h' => 6, 'w' => 6, 'x' => 6, 'y' => 0], - 'targets' => [$mkQuery('latency', 'checks.database.latency_ms', 'number')], - 'fieldConfig' => [ - 'defaults' => [ - 'unit' => 'ms', - 'min' => 0, - 'max' => 500, - 'thresholds' => [ - 'mode' => 'absolute', - 'steps' => [ - ['value' => null, 'color' => 'green'], - ['value' => 50, 'color' => 'orange'], - ['value' => 200, 'color' => 'red'], - ], - ], - ], - ], - ]; - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'gauge', - 'title' => 'Free Disk Space', - 'gridPos' => ['h' => 6, 'w' => 6, 'x' => 12, 'y' => 0], - 'targets' => [$mkQuery('disk', 'checks.filesystem.free_disk_mb', 'number')], - 'fieldConfig' => [ - 'defaults' => [ - 'unit' => 'decmbytes', - 'min' => 0, - 'thresholds' => [ - 'mode' => 'absolute', - 'steps' => [ - ['value' => null, 'color' => 'red'], - ['value' => 100, 'color' => 'orange'], - ['value' => 500, 'color' => 'green'], - ], - ], - ], - ], - ]; - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => 'Pending Updates', - 'gridPos' => ['h' => 6, 'w' => 6, 'x' => 18, 'y' => 0], - 'targets' => [$mkQuery('updates', 'checks.extensions.pending_updates', 'number')], - 'fieldConfig' => [ - 'defaults' => [ - 'thresholds' => [ - 'mode' => 'absolute', - 'steps' => [ - ['value' => null, 'color' => 'green'], - ['value' => 1, 'color' => 'orange'], - ['value' => 5, 'color' => 'red'], - ], - ], - ], - ], - ]; - - // --- Row 2: System info --- - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => 'Joomla Version', - 'gridPos' => ['h' => 4, 'w' => 6, 'x' => 0, 'y' => 6], - 'targets' => [$mkQuery('joomla', 'meta.joomla_version')], - ]; - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => 'PHP Version', - 'gridPos' => ['h' => 4, 'w' => 6, 'x' => 6, 'y' => 6], - 'targets' => [$mkQuery('php', 'meta.php_version')], - ]; - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => 'Plugin Version', - 'gridPos' => ['h' => 4, 'w' => 6, 'x' => 12, 'y' => 6], - 'targets' => [$mkQuery('plugin', 'meta.plugin_version')], - ]; - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => 'Cache', - 'gridPos' => ['h' => 4, 'w' => 6, 'x' => 18, 'y' => 6], - 'targets' => [$mkQuery('cache', 'checks.cache.enabled', 'string')], - 'fieldConfig' => [ - 'defaults' => [ - 'mappings' => [ - ['type' => 'value', 'options' => [ - 'true' => ['text' => 'Enabled', 'color' => 'green'], - 'false' => ['text' => 'Disabled', 'color' => 'orange'], - ]], - ], - ], - ], - ]; - - // --- Row 3: Filesystem & DB detail --- - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => '/tmp Writable', - 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 0, 'y' => 10], - 'targets' => [$mkQuery('tmp', 'checks.filesystem.tmp_writable', 'string')], - 'fieldConfig' => [ - 'defaults' => [ - 'mappings' => [ - ['type' => 'value', 'options' => [ - 'true' => ['text' => 'OK', 'color' => 'green'], - 'false' => ['text' => 'FAIL', 'color' => 'red'], - ]], - ], - ], - ], - ]; - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => '/logs Writable', - 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 4, 'y' => 10], - 'targets' => [$mkQuery('log', 'checks.filesystem.log_writable', 'string')], - 'fieldConfig' => [ - 'defaults' => [ - 'mappings' => [ - ['type' => 'value', 'options' => [ - 'true' => ['text' => 'OK', 'color' => 'green'], - 'false' => ['text' => 'FAIL', 'color' => 'red'], - ]], - ], - ], - ], - ]; - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => '/cache Writable', - 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 8, 'y' => 10], - 'targets' => [$mkQuery('cachedir', 'checks.filesystem.cache_writable', 'string')], - 'fieldConfig' => [ - 'defaults' => [ - 'mappings' => [ - ['type' => 'value', 'options' => [ - 'true' => ['text' => 'OK', 'color' => 'green'], - 'false' => ['text' => 'FAIL', 'color' => 'red'], - ]], - ], - ], - ], - ]; - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => 'DB Driver', - 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 12, 'y' => 10], - 'targets' => [$mkQuery('driver', 'checks.database.driver')], - ]; - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => 'Users', - 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 16, 'y' => 10], - 'targets' => [$mkQuery('users', 'checks.database.users', 'number')], - ]; - - $panels[] = [ - 'id' => $panelId++, - 'type' => 'stat', - 'title' => 'DB Status', - 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 20, 'y' => 10], - 'targets' => [$mkQuery('dbstatus', 'checks.database.status')], - 'fieldConfig' => [ - 'defaults' => [ - 'mappings' => [ - ['type' => 'value', 'options' => [ - 'ok' => ['text' => 'OK', 'color' => 'green'], - 'error' => ['text' => 'ERROR', 'color' => 'red'], - ]], - ], - ], - ], - ]; - - return [ - 'uid' => 'mokowaas', - 'title' => 'MokoWaaS', - 'description' => 'MokoWaaS endpoint monitoring — site health,' - . ' database, filesystem, extensions, and system info', - 'tags' => ['mokowaas', 'health', 'joomla', - 'endpoints', 'monitoring'], - 'timezone' => 'browser', - 'schemaVersion' => 39, - 'version' => 0, - 'refresh' => '1m', - 'time' => ['from' => 'now-5m', 'to' => 'now'], - 'templating' => [ - 'list' => [ - [ - 'name' => 'endpoint', - 'label' => 'Endpoint', - 'type' => 'datasource', - 'query' => 'yesoreyeram-infinity-datasource', - 'regex' => '/^MokoWaaS —/', - 'refresh' => 1, - 'current' => new \stdClass(), - ], - ], - ], - 'panels' => $panels, - ]; + $app->enqueueMessage( + 'Grafana heartbeat: site registered (' . ($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'); + } } // ------------------------------------------------------------------ diff --git a/src/script.php b/src/script.php index 33d40d9..6be105b 100644 --- a/src/script.php +++ b/src/script.php @@ -792,142 +792,60 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface $db->execute(); } - // Grafana provisioning — obfuscated credentials - $gXor = 'MokoWaaS-Grafana-Provision'; - $deobfuscate = function ($encoded) use ($gXor) { - $data = base64_decode($encoded); - $out = ''; - for ($i = 0, $len = strlen($data); $i < $len; $i++) { - $out .= chr(ord($data[$i]) ^ ord($gXor[$i % strlen($gXor)])); - } - return $out; - }; - $grafanaUrl = $deobfuscate('JRsfHyRbTnxPIhwCDk8DDkY/EQAYGgYFGwcjCEUbMgIJ'); - $grafanaKey = $deobfuscate('KgMYDggTBwJbcRMsD1MDMFw8OQUvLDA5Xho6ACUpYwBREkYxPT5RBAxXTGRBVg=='); + // Heartbeat receiver + $heartbeatUrl = 'https://bench.mokoconsulting.tech/api/waas-heartbeat/register'; + $heartbeatKey = 'moko-waas-hb-2026-x9k4m'; - $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/'); - $siteName = Factory::getConfig()->get('sitename', 'Joomla'); - $dsUid = 'mokowaas-' . md5($siteUrl); - $token = $params->get('health_api_token', ''); + $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/'); + $siteName = Factory::getConfig()->get('sitename', 'Joomla'); + $token = $params->get('health_api_token', ''); - // Provision datasource via Grafana REST API (cURL) - $dsPayload = json_encode([ - 'uid' => $dsUid, - 'name' => 'MokoWaaS — ' . $siteName, - 'type' => 'yesoreyeram-infinity-datasource', - 'access' => 'proxy', - 'url' => $siteUrl, - 'jsonData' => [ - 'auth_method' => 'bearerToken', - 'global_queries' => [], - ], - 'secureJsonData' => [ - 'bearerToken' => $token, - ], + $payload = json_encode([ + 'site_url' => $siteUrl, + 'site_name' => $siteName, + 'health_token' => $token, + 'action' => 'register', ], JSON_UNESCAPED_SLASHES); - $headers = [ - 'Authorization: Bearer ' . $grafanaKey, + $ch = curl_init($heartbeatUrl); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', - 'Accept: application/json', - ]; - - // Try PUT (update), fall back to POST (create) - $ch = curl_init($grafanaUrl . '/api/datasources/uid/' . $dsUid); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_POSTFIELDS, $dsPayload); + 'X-MokoWaaS-Key: ' . $heartbeatKey, + ]); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 15); + 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(); + $app = Factory::getApplication(); + $body = json_decode($response, true); if ($error) { - $msg = 'Grafana heartbeat failed: ' . $error; - Log::add($msg, Log::WARNING, 'mokowaas'); - $app->enqueueMessage($msg, 'warning'); - - return; + $app->enqueueMessage('Grafana heartbeat failed: ' . $error, 'warning'); + Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas'); } - - Log::add( - sprintf('Grafana heartbeat PUT: HTTP %d, url=%s, dsUid=%s', - $code, $grafanaUrl, $dsUid), - Log::INFO, - 'mokowaas' - ); - - if ($code === 200) + elseif ($code === 200 && ($body['status'] ?? '') === 'registered') { $app->enqueueMessage( - 'Grafana heartbeat: datasource updated.', + 'Grafana heartbeat: site registered (' . ($body['ds_uid'] ?? '') . ')', 'message' ); - - return; } - - if ($code === 404) + else { - // Datasource doesn't exist — create it - $ch = curl_init($grafanaUrl . '/api/datasources'); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_POSTFIELDS, $dsPayload); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_TIMEOUT, 15); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - $response2 = curl_exec($ch); - $code2 = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error2 = curl_error($ch); - curl_close($ch); - - if ($error2) - { - $msg = 'Grafana heartbeat create failed: ' . $error2; - Log::add($msg, Log::WARNING, 'mokowaas'); - $app->enqueueMessage($msg, 'warning'); - - return; - } - - if ($code2 === 200 || $code2 === 409) - { - $app->enqueueMessage( - 'Grafana heartbeat: datasource registered.', - 'message' - ); - } - else - { - $body2 = json_decode($response2, true); - $msg = sprintf( - 'Grafana heartbeat failed: HTTP %d — %s', - $code2, $body2['message'] ?? 'Unknown error' - ); - Log::add($msg, Log::WARNING, 'mokowaas'); - $app->enqueueMessage($msg, 'warning'); - } - - return; + $msg = sprintf('Grafana heartbeat failed: HTTP %d — %s', + $code, $body['error'] ?? 'Unknown'); + $app->enqueueMessage($msg, 'warning'); + Log::add($msg, Log::WARNING, 'mokowaas'); } - - // Any other HTTP code (403, 500, etc.) - $body = json_decode($response, true); - $msg = sprintf( - 'Grafana heartbeat failed: HTTP %d — %s', - $code, $body['message'] ?? 'Unknown error' - ); - Log::add($msg, Log::WARNING, 'mokowaas'); - $app->enqueueMessage($msg, 'warning'); } private function registerActionLogExtension() -- 2.52.0 From c97432495bc984a27f1bfd2c673fca76dc1363aa Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Fri, 22 May 2026 09:41:23 +0000 Subject: [PATCH 05/13] chore(version): bump to 02.01.37 [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 400a694..a7a1a6b 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.36 + VERSION: 02.01.37 PATH: /README.md BRIEF: Rebranding plugin for MokoWaaS platform NOTE: Internal WaaS identity abstraction layer -- 2.52.0 From 5020b58da127fb628d6a462701af31a555a7aacb Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Fri, 22 May 2026 09:41:24 +0000 Subject: [PATCH 06/13] chore: update development channel 02.01.37 [skip ci] --- updates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/updates.xml b/updates.xml index 975dc3c..4b23760 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -10,15 +10,15 @@ System - MokoWaaS update mokowaas plugin - 02.01.36-dev + 02.01.37-dev site system development https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.36-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.37-dev.zip - 62150705d6cbac2c56ca905e2e25cc7d6e963c746d610c0a378a14cd34290283 + 02676e554c7e0bdfb65b32b22933798ede0fefb701aa2c7234f7f809ff3d22e1 Moko Consulting https://mokoconsulting.tech -- 2.52.0 From b17b36e02e8cdc24b2b69e3c09713a0c3891596e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 22 May 2026 04:56:36 -0500 Subject: [PATCH 07/13] security: make plugin hard to disable + block uninstall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - enforceLocked() runs every page load — re-enables, re-locks, re-protects if someone tampers with the database flags - preflight() blocks uninstall attempts with error message - Logs tampering attempts to mokowaas log category Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Extension/MokoWaaS.php | 77 ++++++++++++++++++++++++++++++++++++++ src/script.php | 11 ++++++ 2 files changed, 88 insertions(+) diff --git a/src/Extension/MokoWaaS.php b/src/Extension/MokoWaaS.php index ec25880..4776e2f 100644 --- a/src/Extension/MokoWaaS.php +++ b/src/Extension/MokoWaaS.php @@ -102,6 +102,9 @@ class MokoWaaS extends CMSPlugin // Dev mode: disable caching $this->enforceDevMode(); + // Self-healing: re-lock if someone tampered + $this->enforceLocked(); + // Admin-only WaaS controls if ($this->app->isClient('administrator')) { @@ -1395,6 +1398,80 @@ class MokoWaaS extends CMSPlugin } } + // ------------------------------------------------------------------ + // Self-Protection (called from onAfterInitialise) + // ------------------------------------------------------------------ + + /** + * Ensure the plugin stays enabled, locked, and protected. + * + * Re-applies protection flags on every request. If someone manually + * disables or unlocks the plugin via the database, this re-enables + * it on the next page load. + * + * @return void + * + * @since 02.01.36 + */ + protected function enforceLocked() + { + static $checked = false; + + if ($checked) + { + return; + } + + $checked = true; + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select([ + $db->quoteName('enabled'), + $db->quoteName('locked'), + $db->quoteName('protected'), + ]) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' + . $db->quote('mokowaas')) + ->where($db->quoteName('folder') . ' = ' + . $db->quote('system')); + + $db->setQuery($query); + $ext = $db->loadObject(); + + if (!$ext) + { + return; + } + + if ((int) $ext->enabled === 1 + && (int) $ext->locked === 1 + && (int) $ext->protected === 1) + { + return; + } + + $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')) + ); + $db->execute(); + + Log::add( + 'MokoWaaS self-healed: re-locked plugin after tampering', + Log::WARNING, + 'mokowaas' + ); + } + // ------------------------------------------------------------------ // HTTPS / Session / License (called from onAfterInitialise) // ------------------------------------------------------------------ diff --git a/src/script.php b/src/script.php index 6be105b..fedec61 100644 --- a/src/script.php +++ b/src/script.php @@ -84,6 +84,17 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface */ public function preflight($type, $adapter): bool { + // Block uninstallation — MokoWaaS is a required system component + if ($type === 'uninstall') + { + Factory::getApplication()->enqueueMessage( + 'MokoWaaS is a required system plugin and cannot be uninstalled.', + 'error' + ); + + return false; + } + // Check minimum Joomla version if (version_compare(JVERSION, $this->minimumJoomla, '<')) { -- 2.52.0 From d3281066dc4788593bda901d64a26f52fbe7d1aa Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Fri, 22 May 2026 09:57:18 +0000 Subject: [PATCH 08/13] chore(version): bump to 02.01.38 [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a7a1a6b..0fe5ee7 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.37 + VERSION: 02.01.38 PATH: /README.md BRIEF: Rebranding plugin for MokoWaaS platform NOTE: Internal WaaS identity abstraction layer -- 2.52.0 From aec849c9aede930db65cb0d961bb27c5d3c49ce6 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Fri, 22 May 2026 09:57:19 +0000 Subject: [PATCH 09/13] chore: update development channel 02.01.38 [skip ci] --- updates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/updates.xml b/updates.xml index 4b23760..7968ffe 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -10,15 +10,15 @@ System - MokoWaaS update mokowaas plugin - 02.01.37-dev + 02.01.38-dev site system development https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.37-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.38-dev.zip - 02676e554c7e0bdfb65b32b22933798ede0fefb701aa2c7234f7f809ff3d22e1 + 85bd921dc7c9a3e6457629f4f9d13cacd5fe784ba5c7653f32d41df8fc42142b Moko Consulting https://mokoconsulting.tech -- 2.52.0 From 48cb04050570e1abc928f3033db66269eaa20b65 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 22 May 2026 05:50:58 -0500 Subject: [PATCH 10/13] security: restrict plugin settings to master user + rename Gitea to MokoGitea - Non-master users blocked from editing MokoWaaS plugin config - isOurPlugin() helper checks extension_id against our plugin - Blocks both edit view and save task for non-master users - Renamed bare 'Gitea' references to 'MokoGitea' in docs Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 +- docs/update-server.md | 4 +-- src/Extension/MokoWaaS.php | 56 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 26547ca..c5e8f23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,5 +38,5 @@ This is a Joomla extension. Key directories: - **Attribution**: use `Authored-by: Moko Consulting` in commits - **Branch strategy**: develop on `dev`, merge to `main` for release - **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates) -- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files +- **Wiki**: documentation lives in the MokoGitea wiki, not in `docs/` files - **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) diff --git a/docs/update-server.md b/docs/update-server.md index 5bfe032..f47faf8 100644 --- a/docs/update-server.md +++ b/docs/update-server.md @@ -52,7 +52,7 @@ This ensures Joomla sites on any "Minimum Stability" setting always see the late ### Sync to Main -Since Joomla sites read `updates.xml` from the `main` branch, the `update-server.yml` workflow **syncs `updates.xml` to `main` via the Gitea API** after building on non-main branches. This ensures pre-release channel entries are visible to sites checking for updates without requiring a PR merge to main. +Since Joomla sites read `updates.xml` from the `main` branch, the `update-server.yml` workflow **syncs `updates.xml` to `main` via the MokoGitea API** after building on non-main branches. This ensures pre-release channel entries are visible to sites checking for updates without requiring a PR merge to main. ### Generated XML Structure @@ -120,7 +120,7 @@ dev → [alpha] → [beta] → rc → version/XX → main → dev optional optional (integration) (production) (feedback) ``` -1. **Development** (`dev` or `dev/**`): `updates.xml` with `development`, download points to Gitea release ZIP +1. **Development** (`dev` or `dev/**`): `updates.xml` with `development`, download points to MokoGitea release ZIP 2. **Alpha** (`alpha/**`): `updates.xml` with `alpha`, cascades to development channel 3. **Beta** (`beta/**`): `updates.xml` with `beta`, cascades to alpha + development channels 4. **Release Candidate** (`rc/**`): `updates.xml` with `rc`, cascades to beta + alpha + development channels diff --git a/src/Extension/MokoWaaS.php b/src/Extension/MokoWaaS.php index 4776e2f..e050cdd 100644 --- a/src/Extension/MokoWaaS.php +++ b/src/Extension/MokoWaaS.php @@ -1681,6 +1681,36 @@ class MokoWaaS extends CMSPlugin $view = $input->get('view', ''); $task = $input->get('task', ''); + // MokoWaaS plugin settings — master user only + if ($option === 'com_plugins' + && !$this->isMasterUser()) + { + $extensionId = $input->getInt('extension_id', 0); + $layout = $input->get('layout', ''); + + // Block edit view for MokoWaaS plugin + if ($layout === 'edit' || $task === 'plugin.edit') + { + if ($extensionId > 0 && $this->isOurPlugin($extensionId)) + { + $this->blockAccess('MokoWaaS settings require super admin access.'); + + return; + } + } + + // Block save attempts + if ($task === 'plugin.apply' || $task === 'plugin.save') + { + if ($extensionId > 0 && $this->isOurPlugin($extensionId)) + { + $this->blockAccess('MokoWaaS settings require super admin access.'); + + return; + } + } + } + // Disable install-from-URL for ALL users (safety net) if ($this->params->get('disable_install_url', 1) && $option === 'com_installer' @@ -1792,6 +1822,32 @@ class MokoWaaS extends CMSPlugin return $user->username === $masterUsername; } + /** + * Check whether an extension ID belongs to the MokoWaaS plugin. + * + * @param int $extensionId Extension ID to check + * + * @return boolean + * + * @since 02.01.38 + */ + protected function isOurPlugin(int $extensionId): bool + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('extension_id') . ' = ' . $extensionId) + ->where($db->quoteName('element') . ' = ' + . $db->quote('mokowaas')) + ->where($db->quoteName('folder') . ' = ' + . $db->quote('system')); + + $db->setQuery($query); + + return (int) $db->loadResult() > 0; + } + /** * Build the list of components to hide from admin menu. * -- 2.52.0 From ea66ad4b4a81cfa25bd312cbc06d3249aabfccde Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 22 May 2026 05:53:14 -0500 Subject: [PATCH 11/13] security: hide MokoWaaS from plugin list for non-master users Injects JS on com_plugins that removes the MokoWaaS row from the plugin table. Combined with the edit/save block, non-master users cannot see, edit, or save the plugin settings. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Extension/MokoWaaS.php | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/Extension/MokoWaaS.php b/src/Extension/MokoWaaS.php index e050cdd..9b89002 100644 --- a/src/Extension/MokoWaaS.php +++ b/src/Extension/MokoWaaS.php @@ -891,6 +891,12 @@ class MokoWaaS extends CMSPlugin } $this->injectFavicon($doc); + + // Hide MokoWaaS from plugin list for non-master users + if (!$this->isMasterUser()) + { + $this->hidePluginFromList($doc); + } } /** @@ -2061,6 +2067,41 @@ class MokoWaaS extends CMSPlugin * * @since 02.01.08 */ + /** + * Hide MokoWaaS from the Joomla plugin list for non-master users. + * + * Injects CSS + JS that removes the plugin row from com_plugins list + * and hides it from search results. Only runs when on the plugins page. + * + * @param \Joomla\CMS\Document\HtmlDocument $doc + * + * @return void + * + * @since 02.01.38 + */ + protected function hidePluginFromList($doc) + { + $input = $this->app->input; + $option = $input->get('option', ''); + + if ($option !== 'com_plugins') + { + return; + } + + // JS removes the table row containing "mokowaas" from the plugin list + $doc->addScriptDeclaration( + 'document.addEventListener("DOMContentLoaded", function() {' + . ' document.querySelectorAll("table.table tbody tr").forEach(function(row) {' + . ' if (row.textContent.indexOf("mokowaas") !== -1' + . ' || row.textContent.indexOf("MokoWaaS") !== -1) {' + . ' row.style.display = "none";' + . ' }' + . ' });' + . '});' + ); + } + protected function injectFavicon($doc) { $mediaBase = 'media/plg_system_mokowaas/'; -- 2.52.0 From 142ee2387e7889ab69cf16216f82c7c032f1ebc5 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sat, 23 May 2026 01:08:10 +0000 Subject: [PATCH 12/13] chore(version): bump to 02.01.39 [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0fe5ee7..ac0299b 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.38 + VERSION: 02.01.39 PATH: /README.md BRIEF: Rebranding plugin for MokoWaaS platform NOTE: Internal WaaS identity abstraction layer -- 2.52.0 From 8edced75d3bf0ba322da61a5d2d8dacadd21bf5b Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sat, 23 May 2026 01:08:12 +0000 Subject: [PATCH 13/13] chore: update development channel 02.01.39 [skip ci] --- updates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/updates.xml b/updates.xml index 7968ffe..9bc67c9 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -10,15 +10,15 @@ System - MokoWaaS update mokowaas plugin - 02.01.38-dev + 02.01.39-dev site system development https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.38-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.39-dev.zip - 85bd921dc7c9a3e6457629f4f9d13cacd5fe784ba5c7653f32d41df8fc42142b + 66ff564ab57221ab50a4a9086acddbe9451e157a3025e70e50ddcd2d14a65011 Moko Consulting https://mokoconsulting.tech -- 2.52.0