diff --git a/CHANGELOG.md b/CHANGELOG.md index a836f7a..b81d82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - License/subscription check - System email template branding (DB approach) +## [02.01.43] - 2026-05-23 + +### Added +- Site Aliases tab with Joomla subform repeatable-table UI +- Per-alias offline toggle with custom maintenance message (503 response) +- Per-alias robots meta directive (index/noindex/follow/nofollow/none) +- Per-alias backend redirect (admin panel redirects to primary domain) +- 6 MokoWaaS API endpoints: health, install, update, cache, backup, info +- Remote plugin install via `/?mokowaas=install` endpoint +- Remote update trigger via `/?mokowaas=update` endpoint +- Remote cache clear via `/?mokowaas=cache` endpoint (site + admin + opcache) +- Remote Akeeba Backup trigger via `/?mokowaas=backup` endpoint +- Compact site info via `/?mokowaas=info` endpoint + +### Changed +- Site aliases moved from comma-separated text field to structured subform +- Each alias now stores domain, offline, offline_message, robots, redirect_backend +- Heartbeat provisioning updated for subform alias format +- Grafana datasource names use domain-only (removed "MokoWaaS - " prefix) + +### Fixed +- Heartbeat receiver accepts any 200 status (registered/updated/ok) +- script.php uses heartbeat receiver instead of Grafana API (fixes 403 RBAC) + ## [02.01.37] - 2026-05-23 ### Added diff --git a/README.md b/README.md index ca91c96..f400e11 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.42 + VERSION: 02.01.43 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.18-blue.svg?logo=v&logoColor=white)](https://github.com/mokoconsulting-tech/MokoWaaS/releases/tag/v02) +[![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) [![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/Extension/MokoWaaS.php b/src/Extension/MokoWaaS.php index 3ec8e85..9766490 100644 --- a/src/Extension/MokoWaaS.php +++ b/src/Extension/MokoWaaS.php @@ -96,6 +96,9 @@ class MokoWaaS extends CMSPlugin // Security: HTTPS redirect (runs for all clients) $this->enforceHttps(); + // Site alias handling: offline page and backend redirect + $this->handleSiteAlias(); + // MokoWaaS API endpoints (run before routing) $mokoAction = $this->app->input->get('mokowaas', ''); @@ -880,14 +883,20 @@ class MokoWaaS extends CMSPlugin */ public function onBeforeCompileHead() { - if (!$this->app->isClient('administrator')) + $doc = $this->app->getDocument(); + + if ($doc->getType() !== 'html') { return; } - $doc = $this->app->getDocument(); + // Inject robots meta tag for alias domains (frontend only) + if ($this->app->isClient('site')) + { + $this->injectAliasRobots($doc); + } - if ($doc->getType() !== 'html') + if (!$this->app->isClient('administrator')) { return; } @@ -2559,6 +2568,142 @@ class MokoWaaS extends CMSPlugin } } + // ------------------------------------------------------------------ + // Site Alias handling + // ------------------------------------------------------------------ + + /** + * Get the alias configuration for the current request domain, if any. + * + * @return object|null Alias entry object or null if not an alias domain + * + * @since 02.01.43 + */ + protected function getCurrentAlias() + { + $currentHost = $_SERVER['HTTP_HOST'] ?? ''; + $primaryHost = parse_url(Uri::root(), PHP_URL_HOST); + + if (empty($currentHost) || strcasecmp($currentHost, $primaryHost) === 0) + { + return null; + } + + $aliases = $this->params->get('site_aliases', ''); + + if (empty($aliases)) + { + return null; + } + + // Subform returns JSON string or array + if (is_string($aliases)) + { + $aliases = json_decode($aliases, false); + } + + if (!is_array($aliases)) + { + return null; + } + + foreach ($aliases as $alias) + { + $alias = (object) $alias; + + if (isset($alias->domain) && strcasecmp(trim($alias->domain), $currentHost) === 0) + { + return $alias; + } + } + + return null; + } + + /** + * Handle site alias logic: offline page and backend redirect. + * + * Runs early in onAfterInitialise before routing occurs. + * + * @return void + * + * @since 02.01.43 + */ + protected function handleSiteAlias() + { + $alias = $this->getCurrentAlias(); + + if ($alias === null) + { + return; + } + + // Backend redirect: send admin requests to the primary domain + if (!empty($alias->redirect_backend) && $alias->redirect_backend === '1' + && $this->app->isClient('administrator')) + { + $primaryUrl = rtrim(Uri::root(), '/') . '/administrator' . Uri::getInstance()->toString(['path', 'query']); + $adminPath = str_replace(Uri::root() . 'administrator', '', Uri::getInstance()->toString(['path', 'query'])); + $primaryUrl = rtrim(Uri::root(), '/') . '/administrator' . $adminPath; + + $this->app->redirect($primaryUrl, 301); + } + + // Offline: show maintenance page for frontend requests + if (!empty($alias->offline) && $alias->offline === '1' + && $this->app->isClient('site')) + { + // Allow health API to still respond + if ($this->app->input->get('mokowaas', '') !== '') + { + return; + } + + $message = $alias->offline_message ?? 'This site is currently offline for maintenance.'; + $brandName = $this->params->get('brand_name', 'MokoWaaS'); + + header('HTTP/1.1 503 Service Unavailable'); + header('Retry-After: 3600'); + header('Content-Type: text/html; charset=utf-8'); + echo ''; + echo ''; + echo '' . htmlspecialchars($brandName) . ' - Maintenance'; + echo ''; + echo '
'; + echo '

' . htmlspecialchars($brandName) . '

'; + echo '

' . htmlspecialchars($message) . '

'; + echo '
'; + $this->app->close(); + } + } + + /** + * Inject robots meta tag for alias domains. + * + * @param \Joomla\CMS\Document\HtmlDocument $doc Document object + * + * @return void + * + * @since 02.01.43 + */ + protected function injectAliasRobots($doc) + { + $alias = $this->getCurrentAlias(); + + if ($alias === null) + { + return; + } + + $robots = $alias->robots ?? 'index, follow'; + + if ($robots !== 'index, follow') + { + $doc->setMetaData('robots', $robots); + } + } + // ------------------------------------------------------------------ // Heartbeat (called from onExtensionAfterSave) // ------------------------------------------------------------------ @@ -2566,8 +2711,9 @@ class MokoWaaS extends CMSPlugin /** * Send heartbeat to the MokoWaaS monitoring receiver. * - * Registers this site (and any aliases) with the Grafana provisioning system. + * Registers this site's primary domain with the Grafana provisioning system. * The receiver writes a datasource YAML file and restarts Grafana. + * Alias domains are not registered to avoid duplicate datasource UIDs. * * @param \Joomla\Registry\Registry $params Plugin params * @param \Joomla\CMS\Application\CMSApplication $app Application @@ -2588,21 +2734,8 @@ class MokoWaaS extends CMSPlugin $siteUrl = rtrim(Uri::root(), '/'); $siteName = Factory::getConfig()->get('sitename', 'Joomla'); - // Register primary domain + // Register primary domain only — aliases should not get separate datasources $this->sendHeartbeat($siteUrl, $siteName, $healthToken, $app); - - // Register any alias domains - $aliases = $params->get('site_aliases', ''); - - if (!empty($aliases)) - { - foreach (array_filter(array_map('trim', explode(',', $aliases))) as $alias) - { - $aliasUrl = 'https://' . ltrim($alias, 'https://'); - $aliasUrl = rtrim($aliasUrl, '/'); - $this->sendHeartbeat($aliasUrl, $siteName . ' (' . $alias . ')', $healthToken, $app); - } - } } /** @@ -2650,16 +2783,18 @@ class MokoWaaS extends CMSPlugin $app->enqueueMessage('Grafana heartbeat failed (' . $siteUrl . '): ' . $error, 'warning'); Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas'); } - elseif ($code === 200 && ($body['status'] ?? '') === 'registered') + elseif ($code === 200) { + $status = $body['status'] ?? 'ok'; $app->enqueueMessage( - 'Grafana heartbeat: ' . $siteUrl . ' registered (' . ($body['ds_uid'] ?? '') . ')', + 'Grafana heartbeat: ' . $siteUrl . ' ' . $status . ' (' . ($body['ds_uid'] ?? '') . ')', 'message' ); } else { - $msg = sprintf('Grafana heartbeat failed (%s): HTTP %d', $siteUrl, $code); + $msg = sprintf('Grafana heartbeat failed (%s): HTTP %d — %s', + $siteUrl, $code, $body['error'] ?? $body['message'] ?? 'Unknown'); $app->enqueueMessage($msg, 'warning'); Log::add($msg, Log::WARNING, 'mokowaas'); } diff --git a/src/forms/alias_entry.xml b/src/forms/alias_entry.xml new file mode 100644 index 0000000..1613b71 --- /dev/null +++ b/src/forms/alias_entry.xml @@ -0,0 +1,55 @@ + +
+ + + + + + + + + + + + + + + + + + diff --git a/src/language/en-GB/plg_system_mokowaas.ini b/src/language/en-GB/plg_system_mokowaas.ini index 348613a..415df26 100644 --- a/src/language/en-GB/plg_system_mokowaas.ini +++ b/src/language/en-GB/plg_system_mokowaas.ini @@ -130,5 +130,18 @@ PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file exte PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)" PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes." -PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Site Aliases" -PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Comma-separated list of additional domains this site is accessible on (e.g. www.example.com,alias.example.com). Each alias gets its own Grafana datasource for health monitoring." +; ===== Site Aliases fieldset ===== +PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases" +PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC="Configure additional domains that mirror this site. Each alias can have its own offline status, robots directive, and backend redirect behavior." +PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Domain Aliases" +PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Add domain aliases that serve as mirrors of this site. Each alias gets its own Grafana monitoring datasource." +PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_LABEL="Domain" +PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_DESC="The alias domain name (e.g. www.example.com). Do not include https:// prefix." +PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_LABEL="Offline" +PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_DESC="Show an offline maintenance page when visitors access the site through this alias domain." +PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_MSG_LABEL="Offline Message" +PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_MSG_DESC="Custom message to display when this alias is set to offline." +PLG_SYSTEM_MOKOWAAS_ALIAS_ROBOTS_LABEL="Robots" +PLG_SYSTEM_MOKOWAAS_ALIAS_ROBOTS_DESC="Meta robots directive for this alias domain. Use 'noindex, nofollow' to prevent search engines from indexing the alias." +PLG_SYSTEM_MOKOWAAS_ALIAS_REDIRECT_BACKEND_LABEL="Redirect Backend" +PLG_SYSTEM_MOKOWAAS_ALIAS_REDIRECT_BACKEND_DESC="Redirect admin panel requests on this alias to the primary domain. Frontend stays on the alias domain." diff --git a/src/language/en-US/plg_system_mokowaas.ini b/src/language/en-US/plg_system_mokowaas.ini index 28eafa4..62cfa64 100644 --- a/src/language/en-US/plg_system_mokowaas.ini +++ b/src/language/en-US/plg_system_mokowaas.ini @@ -130,5 +130,18 @@ PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file exte PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)" PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes." -PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Site Aliases" -PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Comma-separated list of additional domains this site is accessible on (e.g. www.example.com,alias.example.com). Each alias gets its own Grafana datasource for health monitoring." +; ===== Site Aliases fieldset ===== +PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases" +PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC="Configure additional domains that mirror this site. Each alias can have its own offline status, robots directive, and backend redirect behavior." +PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Domain Aliases" +PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Add domain aliases that serve as mirrors of this site. Each alias gets its own Grafana monitoring datasource." +PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_LABEL="Domain" +PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_DESC="The alias domain name (e.g. www.example.com). Do not include https:// prefix." +PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_LABEL="Offline" +PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_DESC="Show an offline maintenance page when visitors access the site through this alias domain." +PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_MSG_LABEL="Offline Message" +PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_MSG_DESC="Custom message to display when this alias is set to offline." +PLG_SYSTEM_MOKOWAAS_ALIAS_ROBOTS_LABEL="Robots" +PLG_SYSTEM_MOKOWAAS_ALIAS_ROBOTS_DESC="Meta robots directive for this alias domain. Use 'noindex, nofollow' to prevent search engines from indexing the alias." +PLG_SYSTEM_MOKOWAAS_ALIAS_REDIRECT_BACKEND_LABEL="Redirect Backend" +PLG_SYSTEM_MOKOWAAS_ALIAS_REDIRECT_BACKEND_DESC="Redirect admin panel requests on this alias to the primary domain. Frontend stays on the alias domain." diff --git a/src/mokowaas.xml b/src/mokowaas.xml index 0e00fa9..fe19a47 100644 --- a/src/mokowaas.xml +++ b/src/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.42 + 02.01.43 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 @@ -44,6 +44,7 @@ script.php Extension Field + forms payload services language @@ -268,6 +269,22 @@ description="PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC" rows="5" filter="raw" /> +
+ +
-
enqueueMessage('Grafana heartbeat failed: ' . $error, 'warning'); Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas'); } - elseif ($code === 200 && ($body['status'] ?? '') === 'registered') + elseif ($code === 200) { + $status = $body['status'] ?? 'ok'; $app->enqueueMessage( - 'Grafana heartbeat: site registered (' . ($body['ds_uid'] ?? '') . ')', + 'Grafana heartbeat: ' . $status . ' (' . ($body['ds_uid'] ?? '') . ')', 'message' ); } else { $msg = sprintf('Grafana heartbeat failed: HTTP %d — %s', - $code, $body['error'] ?? 'Unknown'); + $code, $body['error'] ?? $body['message'] ?? 'Unknown'); $app->enqueueMessage($msg, 'warning'); Log::add($msg, Log::WARNING, 'mokowaas'); }