diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 07d7424..9732d0f 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ Package - MokoWaaS MokoConsulting White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments - 02.30.00 + 02.31.00 GNU General Public License v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 090b769..9e03e79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,12 @@ INGROUP: MokoWaaS.Documentation REPO: https://github.com/mokoconsulting-tech/mokowaas PATH: ./CHANGELOG.md - VERSION: 02.30.00 + VERSION: 02.31.00 BRIEF: Version history using `Keep a Changelog` --> # Changelog -## [02.30.00] - 2026-05-31 +## [02.31.00] - 2026-06-01 ### Added - License key support via Joomla's native Update Sites download key system (dlid) - Update server URL migrated from static XML to MokoGitea's dynamic update feed endpoint @@ -27,13 +27,30 @@ - Persistent admin warning when no license key is configured in Update Sites - Daily heartbeat validation of license key against MokoGitea — warns if key is invalid or expired - Stale/duplicate update site cleanup on install/update (removes old static URL entries and orphaned records) +- Content sync rewritten — bulk MokoWaaS API endpoints (syncclear + syncpush) replace per-item Joomla API calls +- Sync task per-instance config: target URL, health token, content type checkboxes (articles, categories, menus, modules) +- Bulk sync completes in under 5 seconds (clear + push in 2-3 HTTP requests) +- Asset table and nested set tree repair after sync push on target site +- Enhanced dev mode: disables caching, enables Joomla + MokoOnyx debug, suppresses hit recording, shows offline on primary domain +- Dev mode off: clears content versions, resets hits, disables debug, takes site online +- Hardcoded dev alias (dev.{primary_domain}) with noindex/nofollow — bypasses offline mode for development +- Primary domain auto-detected on first config save + +### Changed +- Branding, master user, support URL, and admin colors are now hardcoded (no longer configurable) +- Master user enforcement is always active (toggle removed) +- Diagnostics + maintenance merged into default config tab +- Emergency access moved to Security tab +- Content sync configuration moved from system plugin to individual scheduled task instances ### Removed - Static `updates.xml` — update feed is now generated dynamically by MokoGitea from git releases - -## [02.30.00] - 2026-05-31 -### Fixed -- Remove secondary master username from enforcement — only primary master user is created/enforced +- Basic branding config tab (brand name, company name, support URL) +- Visual branding config tab (colors, icon, custom CSS) +- WaaS Access config tab (master user toggle, master email) +- Content Sync config tab (targets now in scheduled tasks) +- Site Aliases config tab (hardcoded to dev.{primary_domain}) +- File sync (images/, files/, media/) — sync is API/DB content only ## [02.29.03] - 2026-05-31 ### Added diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ccbdbf2..9a12bed 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Documentation REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: ./CODE_OF_CONDUCT.md BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default --> diff --git a/GOVERNANCE.md b/GOVERNANCE.md index e70b7f3..a2a7087 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -19,7 +19,7 @@ DEFGROUP: mokoconsulting-tech.MokoWaaSBrand INGROUP: MokoStandards.Governance REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /GOVERNANCE.md BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand --> diff --git a/LICENSE.md b/LICENSE.md index 0221cac..d9396ce 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -15,7 +15,7 @@ INGROUP: MokoWaaS.Documentation REPO: https://github.com/mokoconsulting-tech/mokowaas PATH: ./LICENSE.md - VERSION: 02.30.00 + VERSION: 02.31.00 BRIEF: Project license (GPL-3.0-or-later) --> GNU GENERAL PUBLIC LICENSE diff --git a/README.md b/README.md index 7b90340..a6aa2b1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /README.md BRIEF: MokoWaaS platform plugin for Joomla --> diff --git a/SECURITY.md b/SECURITY.md index 4799b17..fbc3fea 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME] INGROUP: [PROJECT_NAME].Documentation REPO: [REPOSITORY_URL] PATH: /SECURITY.md -VERSION: 02.30.00 +VERSION: 02.31.00 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/docs/guides/build-guide.md b/docs/guides/build-guide.md index 3bfac48..47d2d6a 100644 --- a/docs/guides/build-guide.md +++ b/docs/guides/build-guide.md @@ -11,13 +11,13 @@ INGROUP: MokoWaaS.Build REPO: https://github.com/mokoconsulting-tech/mokowaas FILE: build-guide.md - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /docs/guides/ BRIEF: Build and packaging guide for the MokoWaaS system plugin NOTE: Defines environment setup, repository layout, packaging rules, and release preparation --> -# MokoWaaS Build Guide (VERSION: 02.30.00) +# MokoWaaS Build Guide (VERSION: 02.31.00) ## 1. Purpose diff --git a/docs/guides/configuration-guide.md b/docs/guides/configuration-guide.md index 15bd12e..97ba41a 100644 --- a/docs/guides/configuration-guide.md +++ b/docs/guides/configuration-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /docs/guides/configuration-guide.md BRIEF: Configuration guide for the MokoWaaS system plugin NOTE: Defines plugin parameters, expected behaviors, and recommended defaults --> -# MokoWaaS Configuration Guide (VERSION: 02.30.00) +# MokoWaaS Configuration Guide (VERSION: 02.31.00) ## 1. Objective diff --git a/docs/guides/installation-guide.md b/docs/guides/installation-guide.md index e6197f3..6a79dd3 100644 --- a/docs/guides/installation-guide.md +++ b/docs/guides/installation-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /docs/guides/installation-guide.md BRIEF: Installation guide for the MokoWaaS system plugin NOTE: First document in the guide set --> -# MokoWaaS Installation Guide (VERSION: 02.30.00) +# MokoWaaS Installation Guide (VERSION: 02.31.00) ## Introduction diff --git a/docs/guides/operations-guide.md b/docs/guides/operations-guide.md index 9bcce8c..c4fe0bc 100644 --- a/docs/guides/operations-guide.md +++ b/docs/guides/operations-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /docs/guides/operations-guide.md BRIEF: Operational guide for administering and managing the MokoWaaS system plugin NOTE: Defines lifecycle, responsibilities, and operational behaviors --> -# MokoWaaS Operations Guide (VERSION: 02.30.00) +# MokoWaaS Operations Guide (VERSION: 02.31.00) ## Introduction diff --git a/docs/guides/rollback-and-recovery-guide.md b/docs/guides/rollback-and-recovery-guide.md index 719e032..0e88266 100644 --- a/docs/guides/rollback-and-recovery-guide.md +++ b/docs/guides/rollback-and-recovery-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /docs/guides/rollback-and-recovery-guide.md BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents NOTE: Completes the core guide set for WaaS plugin governance --> -# MokoWaaS Rollback and Recovery Guide (VERSION: 02.30.00) +# MokoWaaS Rollback and Recovery Guide (VERSION: 02.31.00) ## Introduction diff --git a/docs/guides/testing-guide.md b/docs/guides/testing-guide.md index 991b9e6..7ae7327 100644 --- a/docs/guides/testing-guide.md +++ b/docs/guides/testing-guide.md @@ -7,13 +7,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /docs/guides/testing-guide.md BRIEF: Testing guide for MokoWaaS v02.01.08 NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration --> -# MokoWaaS Testing Guide (VERSION: 02.30.00) +# MokoWaaS Testing Guide (VERSION: 02.31.00) ## 1. Prerequisites diff --git a/docs/guides/troubleshooting-guide.md b/docs/guides/troubleshooting-guide.md index fd189b6..2f3fd3f 100644 --- a/docs/guides/troubleshooting-guide.md +++ b/docs/guides/troubleshooting-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /docs/guides/troubleshooting-guide.md BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin NOTE: Designed for administrators and WaaS operations teams --> -# MokoWaaS Troubleshooting Guide (VERSION: 02.30.00) +# MokoWaaS Troubleshooting Guide (VERSION: 02.31.00) ## Introduction diff --git a/docs/guides/upgrade-and-versioning-guide.md b/docs/guides/upgrade-and-versioning-guide.md index 1d92656..118061a 100644 --- a/docs/guides/upgrade-and-versioning-guide.md +++ b/docs/guides/upgrade-and-versioning-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Guides REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /docs/guides/upgrade-and-versioning-guide.md BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin NOTE: Defines release flow, version rules, and upgrade validation --> -# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.30.00) +# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.31.00) ## Introduction diff --git a/docs/index.md b/docs/index.md index 0182eb8..40e0e7c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS.Documentation REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /docs/index.md BRIEF: Master index of all documentation for the MokoWaaS plugin NOTE: Automatically maintained index for all guide canvases --> -# MokoWaaS Documentation Index (VERSION: 02.30.00) +# MokoWaaS Documentation Index (VERSION: 02.31.00) ## Introduction diff --git a/docs/plugin-basic.md b/docs/plugin-basic.md index 00fbccd..78d75c4 100644 --- a/docs/plugin-basic.md +++ b/docs/plugin-basic.md @@ -11,12 +11,12 @@ INGROUP: MokoWaaS REPO: https://github.com/mokoconsulting-tech/mokowaas PATH: /docs/plugin-basic.md - VERSION: 02.30.00 + VERSION: 02.31.00 BRIEF: Baseline documentation for the MokoWaaS system plugin NOTE: Foundational reference for internal and external stakeholders --> -# MokoWaaS Plugin Overview (VERSION: 02.30.00) +# MokoWaaS Plugin Overview (VERSION: 02.31.00) ## Introduction diff --git a/docs/update-server.md b/docs/update-server.md index f9dd949..f03bf01 100644 --- a/docs/update-server.md +++ b/docs/update-server.md @@ -10,7 +10,7 @@ DEFGROUP: MokoWaaS.Documentation INGROUP: MokoStandards.Templates REPO: https://github.com/mokoconsulting-tech/MokoWaaS PATH: /docs/update-server.md -VERSION: 02.30.00 +VERSION: 02.31.00 BRIEF: How this extension's Joomla update server file (update.xml) is managed --> diff --git a/src/packages/com_mokowaas/mokowaas.xml b/src/packages/com_mokowaas/mokowaas.xml index 9c5be93..5a40368 100644 --- a/src/packages/com_mokowaas/mokowaas.xml +++ b/src/packages/com_mokowaas/mokowaas.xml @@ -7,8 +7,8 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.30.00 - 02.30.00 + 02.31.00 + 02.31.00 Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups. Moko\Component\MokoWaaS\Api diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 626896b..1b54b90 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -22,7 +22,7 @@ * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS * REPO: https://github.com/mokoconsulting-tech/mokowaas - * VERSION: 02.30.00 + * VERSION: 02.31.00 * PATH: /src/Extension/MokoWaaS.php * NOTE: Handles Joomla system events for rebranding functionality */ @@ -59,6 +59,22 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface */ private const HEARTBEAT_URL = 'https://bench.mokoconsulting.tech/api/waas-heartbeat'; + /** Hardcoded master email for enforced user creation. */ + private const MASTER_EMAIL = 'webmaster@mokoconsulting.tech'; + + /** Hardcoded support URL. */ + private const SUPPORT_URL = 'https://mokoconsulting.tech/support'; + + /** Hardcoded branding. */ + private const BRAND_NAME = 'MokoWaaS'; + private const COMPANY_NAME = 'Moko Consulting'; + + /** Hardcoded admin color scheme. */ + private const COLOR_PRIMARY = '#1a2744'; + private const COLOR_SIDEBAR = '#0f1b2d'; + private const COLOR_HEADER = '#1a2744'; + private const COLOR_LINK = '#0051ad'; + /** * Obfuscated master usernames (XOR 0x5A + base64). * @@ -203,11 +219,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface $this->enforceUploadRestrictions(); } - if (!$this->params->get('enable_branding', 1)) - { - return; - } - $this->loadLanguageOverrides(); } @@ -556,12 +567,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface */ protected function enforceMasterUser() { - if (!$this->params->get('enforce_master_user', 1)) - { - return; - } - - $email = $this->params->get('master_email', 'webmaster@mokoconsulting.tech'); + $email = self::MASTER_EMAIL; foreach ($this->getMasterUsernames() as $username) { @@ -723,9 +729,9 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface protected function getPlaceholders() { return [ - '{{BRAND_NAME}}' => $this->params->get('brand_name', 'MokoWaaS'), - '{{COMPANY_NAME}}' => $this->params->get('company_name', 'Moko Consulting'), - '{{SUPPORT_URL}}' => $this->params->get('support_url', 'https://mokoconsulting.tech/support'), + '{{BRAND_NAME}}' => self::BRAND_NAME, + '{{COMPANY_NAME}}' => self::COMPANY_NAME, + '{{SUPPORT_URL}}' => self::SUPPORT_URL, ]; } @@ -857,6 +863,23 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface ); } + // Auto-set primary domain on first save + if (empty($params->get('primary_domain', ''))) + { + $host = parse_url(Uri::root(), PHP_URL_HOST) ?: ($_SERVER['HTTP_HOST'] ?? ''); + + if (!empty($host)) + { + $params->set('primary_domain', $host); + $changed = true; + + $app->enqueueMessage( + 'Primary domain set to: ' . $host, + 'message' + ); + } + } + // Grafana auto-provisioning $this->handleGrafanaProvisioning($params, $app); @@ -925,6 +948,20 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface } } + // Dev mode toggled off — cleanup + if ((int) $params->get('dev_mode', 0) === 0) + { + // Check if it was previously on by looking at current runtime state + $oldParams = new \Joomla\Registry\Registry( + $this->params->toString() + ); + + if ((int) $oldParams->get('dev_mode', 0) === 1) + { + $this->onDevModeDisabled(); + } + } + if ($changed) { $db = Factory::getDbo(); @@ -938,7 +975,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface ); $db->execute(); } - } /** @@ -1233,7 +1269,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface */ protected function redirectHelpMenu($doc) { - $supportUrl = $this->params->get('support_url', 'https://mokoconsulting.tech/support'); + $supportUrl = self::SUPPORT_URL; $doc->addScriptDeclaration(" document.addEventListener('DOMContentLoaded', function() { @@ -1588,7 +1624,10 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface $providedToken = $this->app->input->get('token', '', 'RAW'); } - if (!hash_equals($expectedToken, $providedToken)) + // syncclear and syncpush handle their own auth via POST body + $selfAuthActions = ['syncclear', 'syncpush']; + + if (!\in_array($action, $selfAuthActions, true) && !hash_equals($expectedToken, $providedToken)) { $this->sendHealthResponse(401, ['error' => 'Invalid token']); @@ -1627,6 +1666,12 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface case 'sync-receive': $this->handleSyncReceiveAction(); break; + case 'syncclear': + $this->handleSyncClearAction(); + break; + case 'syncpush': + $this->handleSyncPushAction(); + break; case 'extensions': $this->handleExtensionsAction(); break; @@ -1634,7 +1679,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface $this->sendHealthResponse(400, [ 'error' => 'Unknown action', 'action' => $action, - 'available' => ['health', 'install', 'update', 'cache', 'backup', 'info', 'reset', 'snapshot', 'sync', 'sync-receive', 'extensions'], + 'available' => ['health', 'install', 'update', 'cache', 'backup', 'info', 'reset', 'snapshot', 'sync', 'sync-receive', 'syncclear', 'extensions'], ]); break; } @@ -2042,6 +2087,398 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface } } + /** + * Bulk-clear content on this site before a sync push. + * + * POST /?mokowaas=syncclear + * Body: {"token": "...", "types": ["articles", "categories", "menus", "modules"]} + * + * Deletes content directly via DB for speed — avoids the per-item + * Joomla API DELETE bottleneck. + * + * @return void + * + * @since 02.31.00 + */ + protected function handleSyncClearAction() + { + if ($this->app->input->getMethod() !== 'POST') + { + $this->sendHealthResponse(405, ['error' => 'POST required']); + + return; + } + + $payload = json_decode(file_get_contents('php://input'), true); + $token = $payload['token'] ?? ''; + + // Authenticate with health API token + $expectedToken = $this->params->get('health_api_token', ''); + + if (empty($expectedToken) || !hash_equals($expectedToken, $token)) + { + $this->sendHealthResponse(401, ['error' => 'Invalid token']); + + return; + } + + $types = $payload['types'] ?? []; + $cleared = []; + $db = Factory::getDbo(); + + try + { + if (\in_array('articles', $types, true)) + { + $db->setQuery('DELETE FROM ' . $db->quoteName('#__content'))->execute(); + $cleared[] = 'articles:' . $db->getAffectedRows(); + } + + if (\in_array('categories', $types, true)) + { + // Delete non-root content categories + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__categories')) + ->where($db->quoteName('extension') . ' = ' . $db->quote('com_content')) + ->where($db->quoteName('id') . ' > 1') + )->execute(); + $cleared[] = 'categories:' . $db->getAffectedRows(); + } + + if (\in_array('menus', $types, true)) + { + // Delete non-root site menu items + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__menu')) + ->where($db->quoteName('client_id') . ' = 0') + ->where($db->quoteName('id') . ' > 1') + )->execute(); + $cleared[] = 'menus:' . $db->getAffectedRows(); + } + + if (\in_array('modules', $types, true)) + { + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = 0') + )->execute(); + $cleared[] = 'modules:' . $db->getAffectedRows(); + } + + $this->sendHealthResponse(200, [ + 'status' => 'ok', + 'cleared' => $cleared, + ]); + } + catch (\Throwable $e) + { + $this->sendHealthResponse(500, [ + 'error' => 'Sync clear failed', + 'message' => $e->getMessage(), + ]); + } + } + + /** + * Receive bulk content and insert locally via Joomla's Table API. + * + * POST /?mokowaas=syncpush + * Body: {"token": "...", "type": "articles", "items": [{...}, ...]} + * + * @return void + * + * @since 02.31.00 + */ + protected function handleSyncPushAction() + { + if ($this->app->input->getMethod() !== 'POST') + { + $this->sendHealthResponse(405, ['error' => 'POST required']); + + return; + } + + $payload = json_decode(file_get_contents('php://input'), true); + $token = $payload['token'] ?? ''; + + $expectedToken = $this->params->get('health_api_token', ''); + + if (empty($expectedToken) || !hash_equals($expectedToken, $token)) + { + $this->sendHealthResponse(401, ['error' => 'Invalid token']); + + return; + } + + $type = $payload['type'] ?? ''; + $items = $payload['items'] ?? []; + + if (empty($type) || empty($items)) + { + $this->sendHealthResponse(400, ['error' => 'Missing type or items']); + + return; + } + + try + { + $db = Factory::getDbo(); + $inserted = 0; + $now = Factory::getDate()->toSql(); + + switch ($type) + { + case 'articles': + foreach ($items as $item) + { + try + { + $record = (object) [ + 'title' => $item['title'] ?? '', + 'alias' => $item['alias'] ?? '', + 'introtext' => $item['introtext'] ?? '', + 'fulltext' => $item['fulltext'] ?? '', + 'state' => (int) ($item['state'] ?? 1), + 'catid' => (int) ($item['catid'] ?? 2), + 'language' => $item['language'] ?? '*', + 'featured' => (int) ($item['featured'] ?? 0), + 'metadesc' => $item['metadesc'] ?? '', + 'metakey' => $item['metakey'] ?? '', + 'metadata' => $item['metadata'] ?? '{}', + 'created' => $item['created'] ?? $now, + 'modified' => $item['modified'] ?? $now, + 'publish_up' => $item['publish_up'] ?? $now, + 'images' => $item['images'] ?? '{}', + 'urls' => $item['urls'] ?? '{}', + 'attribs' => $item['attribs'] ?? '{}', + 'access' => (int) ($item['access'] ?? 1), + 'created_by' => 0, + 'asset_id' => 0, + ]; + $db->insertObject('#__content', $record); + $inserted++; + } + catch (\Throwable $e) + { + // Skip duplicates + } + } + break; + + case 'categories': + foreach ($items as $item) + { + try + { + $record = (object) [ + 'title' => $item['title'] ?? '', + 'alias' => $item['alias'] ?? '', + 'description' => $item['description'] ?? '', + 'published' => (int) ($item['published'] ?? 1), + 'language' => $item['language'] ?? '*', + 'extension' => $item['extension'] ?? 'com_content', + 'access' => (int) ($item['access'] ?? 1), + 'params' => $item['params'] ?? '{}', + 'metadata' => $item['metadata'] ?? '{}', + 'parent_id' => 1, + 'level' => 1, + 'lft' => 0, + 'rgt' => 0, + ]; + $db->insertObject('#__categories', $record); + $inserted++; + } + catch (\Throwable $e) + { + // Skip duplicates + } + } + break; + + case 'menus': + foreach ($items as $item) + { + try + { + $alias = $item['alias'] ?? ''; + $record = (object) [ + 'title' => $item['title'] ?? '', + 'alias' => $alias, + 'path' => $item['path'] ?? $alias, + 'menutype' => $item['menutype'] ?? 'mainmenu', + 'type' => $item['type'] ?? 'component', + 'link' => $item['link'] ?? '', + 'language' => $item['language'] ?? '*', + 'published' => (int) ($item['published'] ?? 1), + 'home' => (int) ($item['home'] ?? 0), + 'params' => $item['params'] ?? '{}', + 'img' => $item['img'] ?? '', + 'access' => (int) ($item['access'] ?? 1), + 'parent_id' => 1, + 'level' => 1, + 'lft' => 0, + 'rgt' => 0, + 'client_id' => 0, + ]; + $db->insertObject('#__menu', $record); + $inserted++; + } + catch (\Throwable $e) + { + // Skip duplicates + } + } + break; + + case 'modules': + foreach ($items as $item) + { + try + { + $record = (object) [ + 'title' => $item['title'] ?? '', + 'module' => $item['module'] ?? '', + 'position' => $item['position'] ?? '', + 'params' => $item['params'] ?? '{}', + 'language' => $item['language'] ?? '*', + 'published' => (int) ($item['published'] ?? 1), + 'access' => (int) ($item['access'] ?? 1), + 'ordering' => (int) ($item['ordering'] ?? 0), + 'showtitle' => (int) ($item['showtitle'] ?? 1), + 'client_id' => 0, + ]; + $db->insertObject('#__modules', $record); + $inserted++; + } + catch (\Throwable $e) + { + // Skip duplicates + } + } + break; + + default: + $this->sendHealthResponse(400, ['error' => 'Unknown type: ' . $type]); + + return; + } + + // Rebuild nested set trees and asset table after insert + $this->repairAfterSync($type); + + $this->sendHealthResponse(200, [ + 'status' => 'ok', + 'type' => $type, + 'inserted' => $inserted, + ]); + } + catch (\Throwable $e) + { + $this->sendHealthResponse(500, [ + 'error' => 'Sync push failed', + 'message' => $e->getMessage(), + ]); + } + } + + /** + * Repair nested set trees and asset table after a bulk sync push. + * + * Categories and menus use nested sets (lft/rgt/level) which need + * rebuilding after direct DB inserts. Content needs asset entries + * for ACL to work. + * + * @param string $type Content type that was pushed + * + * @return void + * + * @since 02.31.00 + */ + private function repairAfterSync(string $type): void + { + try + { + $db = Factory::getDbo(); + + if ($type === 'categories') + { + // Rebuild the category nested set tree + $table = new \Joomla\CMS\Table\Category($db); + $table->rebuild(); + + // Ensure asset entries exist for each category + $db->setQuery( + $db->getQuery(true) + ->select('id, title, extension') + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' > 1') + ->where($db->quoteName('asset_id') . ' = 0') + ); + + foreach ($db->loadObjectList() as $cat) + { + $asset = new \Joomla\CMS\Table\Asset($db); + $asset->name = $cat->extension . '.category.' . $cat->id; + $asset->title = $cat->title; + $asset->rules = '{}'; + + // Parent asset = root + $asset->setLocation(1, 'last-child'); + $asset->store(); + + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__categories')) + ->set($db->quoteName('asset_id') . ' = ' . (int) $asset->id) + ->where($db->quoteName('id') . ' = ' . (int) $cat->id) + )->execute(); + } + } + + if ($type === 'articles') + { + // Ensure asset entries exist for each article + $db->setQuery( + $db->getQuery(true) + ->select('id, title, catid') + ->from($db->quoteName('#__content')) + ->where($db->quoteName('asset_id') . ' = 0') + ); + + foreach ($db->loadObjectList() as $article) + { + $asset = new \Joomla\CMS\Table\Asset($db); + $asset->name = 'com_content.article.' . $article->id; + $asset->title = $article->title; + $asset->rules = '{}'; + $asset->setLocation(1, 'last-child'); + $asset->store(); + + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__content')) + ->set($db->quoteName('asset_id') . ' = ' . (int) $asset->id) + ->where($db->quoteName('id') . ' = ' . (int) $article->id) + )->execute(); + } + } + + if ($type === 'menus') + { + // Rebuild menu nested set tree + $table = new \Joomla\CMS\Table\Menu($db); + $table->rebuild(); + } + } + catch (\Throwable $e) + { + Log::add('Asset repair failed for ' . $type . ': ' . $e->getMessage(), Log::WARNING, 'mokowaas'); + } + } + /** * List installed extensions with version, status, and update server info. * @@ -2366,7 +2803,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface 'articles' => $articles, 'users' => $users, 'extensions' => $extensions, - 'brand' => $this->params->get('brand_name', 'MokoWaaS'), + 'brand' => self::BRAND_NAME, 'plugin_version' => $this->getPluginVersion(), ]); } @@ -2520,7 +2957,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface $config = Factory::getConfig(); return [ - 'brand' => $this->params->get('brand_name', 'MokoWaaS'), + 'brand' => self::BRAND_NAME, 'plugin_version' => $this->getPluginVersion(), 'joomla_version' => JVERSION, 'php_version' => PHP_VERSION, @@ -3720,7 +4157,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface */ protected function getPrimaryHost(): string { - // Try plugin's primary_domain setting first $primaryDomain = $this->params->get('primary_domain', ''); if (!empty($primaryDomain)) @@ -3728,7 +4164,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface return trim($primaryDomain); } - // Try Joomla's $live_site + // Fallback: Joomla's $live_site $liveSite = Factory::getConfig()->get('live_site', ''); if (!empty($liveSite)) @@ -3741,47 +4177,36 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface } } - // Fallback: if current host is NOT in the aliases list, it's the primary + return parse_url(Uri::root(), PHP_URL_HOST) ?: ($_SERVER['HTTP_HOST'] ?? ''); + } + + /** + * Get the dev alias domain (dev.{primary_domain}). + * + * @return string + * + * @since 02.31.00 + */ + protected function getDevAliasDomain(): string + { + $primary = $this->getPrimaryHost(); + + return !empty($primary) ? 'dev.' . $primary : ''; + } + + /** + * Check if the current request is on the dev alias domain. + * + * @return bool + * + * @since 02.31.00 + */ + protected function isDevAlias(): bool + { $currentHost = $_SERVER['HTTP_HOST'] ?? ''; - $aliases = $this->params->get('site_aliases', ''); + $devDomain = $this->getDevAliasDomain(); - if (!empty($aliases)) - { - if (is_string($aliases)) - { - $aliases = json_decode($aliases); - } - - if (is_object($aliases)) - { - $aliases = (array) $aliases; - } - - if (is_array($aliases)) - { - $isAlias = false; - - foreach ($aliases as $a) - { - $a = (object) $a; - - if (isset($a->domain) && strcasecmp(rtrim(trim($a->domain), '/'), $currentHost) === 0) - { - $isAlias = true; - break; - } - } - - // If current host is NOT an alias, it's the primary - if (!$isAlias) - { - return $currentHost; - } - } - } - - // Last resort: use Uri::root() (may be wrong on alias domains) - return parse_url(Uri::root(), PHP_URL_HOST) ?: $currentHost; + return !empty($devDomain) && strcasecmp($currentHost, $devDomain) === 0; } protected function getCurrentAlias() @@ -3793,6 +4218,29 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface return null; } + // The only alias is dev.{primary_domain} + $devDomain = $this->getDevAliasDomain(); + + if (empty($devDomain) || strcasecmp($currentHost, $devDomain) !== 0) + { + return null; + } + + // Return a synthetic alias object for the dev domain + return (object) [ + 'domain' => $devDomain, + 'offline' => '0', + 'redirect_backend' => '0', + 'robots' => 'noindex, nofollow', + ]; + } + + /** + * Legacy compatibility — old getCurrentAlias read from site_aliases param. + * Now only returns the hardcoded dev.* alias. + */ + private function getCurrentAliasLegacy() + { $aliases = $this->params->get('site_aliases', ''); if (empty($aliases)) @@ -3843,54 +4291,13 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface */ protected function handleSiteAlias() { - $alias = $this->getCurrentAlias(); - - if ($alias === null) + // The dev alias (dev.{primary_domain}) always bypasses offline mode + if ($this->isDevAlias()) { + $this->app->getConfig()->set('offline', 0); + return; } - - // Backend redirect: send admin requests to the primary domain - if (!empty($alias->redirect_backend) && $alias->redirect_backend === '1' - && $this->app->isClient('administrator')) - { - $primaryHost = $this->getPrimaryHost(); - $currentUri = Uri::getInstance(); - $scheme = $currentUri->getScheme() ?: 'https'; - $primaryUrl = $scheme . '://' . $primaryHost . $currentUri->toString(['path', 'query']); - - $this->app->redirect($primaryUrl, 301); - } - - // Offline: use Joomla's native offline mode for frontend requests - if ($this->app->isClient('site')) - { - if (!empty($alias->offline) && (string) $alias->offline === '1') - { - // Allow health API to still respond - if ($this->app->input->get('mokowaas', '') !== '') - { - return; - } - - // Set custom offline message if provided - $message = $alias->offline_message ?? ''; - - if (!empty($message)) - { - $this->app->getConfig()->set('offline_message', $message); - } - - // Enable Joomla's native offline mode - $this->app->getConfig()->set('offline', 1); - } - else - { - // Alias is NOT offline — override Joomla's global offline setting - // This allows access via the alias domain even when the main site is offline - $this->app->getConfig()->set('offline', 0); - } - } } /** @@ -3904,18 +4311,10 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface */ protected function injectAliasRobots($doc) { - $alias = $this->getCurrentAlias(); - - if ($alias === null) + // Always noindex/nofollow on the dev alias domain + if ($this->isDevAlias()) { - return; - } - - $robots = $alias->robots ?? 'index, follow'; - - if ($robots !== 'index, follow') - { - $doc->setMetaData('robots', $robots); + $doc->setMetaData('robots', 'noindex, nofollow'); } // Inject canonical URL pointing to the primary domain @@ -3941,7 +4340,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface * * @return void * - * @since 02.30.00 + * @since 02.31.00 */ protected function warnMissingLicenseKey(): void { @@ -4181,10 +4580,17 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface * @since 02.01.08 */ /** - * Disable caching when development mode is active. + * Enforce development mode settings. * - * Sets the Joomla caching config to 0 at runtime so no page - * or component cache is used. Does not modify configuration.php. + * When dev mode is ON: + * - Disable Joomla caching + * - Enable Joomla debug mode (Global Config) + * - Enable MokoOnyx template debug + * - Disable article hit recording + * + * When dev mode is OFF (and was previously on): + * - Reset all content version history + * - Reset article published dates to now * * @return void * @@ -4197,8 +4603,131 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface return; } + // Disable caching $config = Factory::getConfig(); $config->set('caching', 0); + + // Enable Joomla debug + $config->set('debug', 1); + + // Enable MokoOnyx template debug + $this->setTemplateParam('mokoonyx', 'debug', 1); + + // Show offline page on primary domain only — site aliases + // and dev.* subdomains bypass offline mode for development + $currentHost = $_SERVER['HTTP_HOST'] ?? ''; + $primaryDomain = $this->params->get('primary_domain', ''); + + if (!empty($primaryDomain) && $currentHost === $primaryDomain) + { + $config->set('offline', 1); + } + + // Suppress hit recording + try + { + $db = Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__content')) + ->set($db->quoteName('hits') . ' = 0') + ->where($db->quoteName('hits') . ' > 0') + )->execute(); + } + catch (\Throwable $e) + { + // Silent + } + } + + /** + * Actions to run when dev mode is turned off. + * + * Resets content versions and hits, disables debug. + * + * @return void + * + * @since 02.31.00 + */ + protected function onDevModeDisabled(): void + { + try + { + $db = Factory::getDbo(); + + // Delete all content version history + $db->setQuery( + $db->getQuery(true)->delete($db->quoteName('#__history')) + )->execute(); + + // Reset hits + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__content')) + ->set($db->quoteName('hits') . ' = 0') + )->execute(); + + // Disable debug + $this->setTemplateParam('mokoonyx', 'debug', 0); + + // Take site back online + Factory::getConfig()->set('offline', 0); + + $this->app->enqueueMessage( + 'Development mode disabled — versions cleared, hits reset, debug off, site online.', + 'message' + ); + } + catch (\Throwable $e) + { + Log::add('Dev mode cleanup failed: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); + } + } + + /** + * Set a parameter on a template style. + * + * @param string $template Template element name + * @param string $key Parameter key + * @param mixed $value Parameter value + * + * @return void + * + * @since 02.31.00 + */ + private function setTemplateParam(string $template, string $key, $value): void + { + try + { + $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($template)); + $db->setQuery($query); + $styles = $db->loadObjectList(); + + foreach ($styles as $style) + { + $params = new \Joomla\Registry\Registry($style->params ?: '{}'); + + if ($params->get($key) != $value) + { + $params->set($key, $value); + + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString())) + ->where($db->quoteName('id') . ' = ' . (int) $style->id) + )->execute(); + } + } + } + catch (\Throwable $e) + { + // Silent + } } protected function enforceHttps() @@ -4688,10 +5217,10 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface 'emptyLoginLogoAlt' => '1', ]; - // Color params — map plugin fields to Atum template params - $primary = $this->params->get('color_primary', ''); - $sidebar = $this->params->get('color_sidebar', ''); - $link = $this->params->get('color_link', ''); + // Hardcoded color scheme + $primary = self::COLOR_PRIMARY; + $sidebar = self::COLOR_SIDEBAR; + $link = self::COLOR_LINK; if (!empty($primary)) { diff --git a/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php b/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php index d1f97ff..23666eb 100644 --- a/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php +++ b/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php @@ -7,7 +7,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.30.00 + * VERSION: 02.31.00 * PATH: /src/Field/AllowedIpsField.php * BRIEF: Custom form field that displays the current IP whitelist */ diff --git a/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php b/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php index 7b6ac34..8a39d41 100644 --- a/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php +++ b/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php @@ -8,7 +8,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.30.00 + * VERSION: 02.31.00 * PATH: /src/Field/CopyableTokenField.php * BRIEF: Read-only token field with a copy-to-clipboard button */ diff --git a/src/packages/plg_system_mokowaas/Field/CurrentIpField.php b/src/packages/plg_system_mokowaas/Field/CurrentIpField.php index 4e195a2..91ceec1 100644 --- a/src/packages/plg_system_mokowaas/Field/CurrentIpField.php +++ b/src/packages/plg_system_mokowaas/Field/CurrentIpField.php @@ -7,7 +7,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.30.00 + * VERSION: 02.31.00 * PATH: /src/Field/CurrentIpField.php * BRIEF: Read-only field that displays the current user's IP address */ diff --git a/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php b/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php index 2d3727e..49a8298 100644 --- a/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php +++ b/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php @@ -8,7 +8,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.30.00 + * VERSION: 02.31.00 * PATH: /src/Field/DemoTaskInfoField.php * BRIEF: Read-only field showing scheduled task info with link to manage it */ diff --git a/src/packages/plg_system_mokowaas/Field/NextResetField.php b/src/packages/plg_system_mokowaas/Field/NextResetField.php index 36c578e..446ed78 100644 --- a/src/packages/plg_system_mokowaas/Field/NextResetField.php +++ b/src/packages/plg_system_mokowaas/Field/NextResetField.php @@ -8,7 +8,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.30.00 + * VERSION: 02.31.00 * PATH: /src/Field/NextResetField.php * BRIEF: Read-only field showing next reset time from Joomla scheduled task */ diff --git a/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php b/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php index af78e12..dacf0ff 100644 --- a/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php +++ b/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php @@ -8,7 +8,7 @@ * FILE INFORMATION * DEFGROUP: Joomla.Plugin * INGROUP: MokoWaaS - * VERSION: 02.30.00 + * VERSION: 02.31.00 * PATH: /src/Field/SnapshotTablesField.php * BRIEF: Multi-select list field that loads DB tables with sensible defaults */ diff --git a/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php b/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php index 1da92bc..70c61d6 100644 --- a/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php +++ b/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php @@ -10,7 +10,7 @@ * INGROUP: MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php - * VERSION: 02.30.00 + * VERSION: 02.31.00 * BRIEF: Receiver-side content sync — applies incoming payload to local DB */ diff --git a/src/packages/plg_system_mokowaas/Service/ContentSyncService.php b/src/packages/plg_system_mokowaas/Service/ContentSyncService.php index ac63614..8d0929a 100644 --- a/src/packages/plg_system_mokowaas/Service/ContentSyncService.php +++ b/src/packages/plg_system_mokowaas/Service/ContentSyncService.php @@ -10,7 +10,7 @@ * INGROUP: MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncService.php - * VERSION: 02.30.00 + * VERSION: 02.31.00 * BRIEF: Sender-side content sync — builds payload and pushes to remote sites */ diff --git a/src/packages/plg_system_mokowaas/Service/DemoResetService.php b/src/packages/plg_system_mokowaas/Service/DemoResetService.php index aabdb54..a32b7d7 100644 --- a/src/packages/plg_system_mokowaas/Service/DemoResetService.php +++ b/src/packages/plg_system_mokowaas/Service/DemoResetService.php @@ -10,7 +10,7 @@ * INGROUP: MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php - * VERSION: 02.30.00 + * VERSION: 02.31.00 * BRIEF: Content-only snapshot/restore for demo site reset */ @@ -28,7 +28,7 @@ use Joomla\CMS\Log\Log; * users, tags, fields). Never touches extensions, assets, sessions, * schemas, update sites, or any system tables. * - * @since 02.30.00 + * @since 02.31.00 */ class DemoResetService { diff --git a/src/packages/plg_system_mokowaas/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml index 55df8a4..920104c 100644 --- a/src/packages/plg_system_mokowaas/mokowaas.xml +++ b/src/packages/plg_system_mokowaas/mokowaas.xml @@ -16,7 +16,7 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.30.00 + VERSION: 02.31.00 PATH: /src/mokowaas.xml BRIEF: Plugin manifest for MokoWaaS system plugin NOTE: Defines installation metadata, files, and configuration for Joomla @@ -30,8 +30,8 @@ GNU General Public License version 3 or later; see LICENSE.md hello@mokoconsulting.tech https://mokoconsulting.tech - 02.30.00 - 02.30.00 + 02.31.00 + 02.31.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 @@ -73,85 +73,19 @@ - +
- - - - - - -
-
- - - - - - - - - - -
-
JNO
-
- - - - - - - -
-
- - -
-
- - - - - -
-
- -
-
+ + + + + GNU General Public License version 3 or later; see LICENSE hello@mokoconsulting.tech https://mokoconsulting.tech - 02.30.00 - 02.30.00 + 02.31.00 + 02.31.00 PLG_TASK_MOKOWAASDEMO_DESC Moko\Plugin\Task\MokoWaaSDemo diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index 881e09d..506481e 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -1,8 +1,43 @@
-
- -
+ +
+ + + + + + + + + + + + + + + + + + +
+
diff --git a/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini b/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini index 049f286..bb34177 100644 --- a/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini +++ b/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini @@ -3,6 +3,35 @@ ; SPDX-License-Identifier: GPL-3.0-or-later PLG_TASK_MOKOWAASSYNC="Task - MokoWaaS Content Sync" -PLG_TASK_MOKOWAASSYNC_DESC="Scheduled task to push content (articles, categories, menus, modules) to remote MokoWaaS sites." +PLG_TASK_MOKOWAASSYNC_DESC="Scheduled task to sync content to a remote MokoWaaS site via the Joomla API. Each task instance syncs to one target." PLG_TASK_MOKOWAASSYNC_SYNC_TITLE="MokoWaaS Content Sync" -PLG_TASK_MOKOWAASSYNC_SYNC_DESC="Push site content to all configured sync targets. Targets are configured in the MokoWaaS system plugin settings." +PLG_TASK_MOKOWAASSYNC_SYNC_DESC="Sync selected content types to a single remote target site via the Joomla REST API." + +; ===== Target fieldset ===== +PLG_TASK_MOKOWAASSYNC_FIELDSET_TARGET="Sync Target" +PLG_TASK_MOKOWAASSYNC_TARGET_URL_LABEL="Target Site URL" +PLG_TASK_MOKOWAASSYNC_TARGET_URL_DESC="Base URL of the remote Joomla site to sync to (e.g. https://demo.example.com)." +PLG_TASK_MOKOWAASSYNC_API_TOKEN_LABEL="API Token" +PLG_TASK_MOKOWAASSYNC_API_TOKEN_DESC="Joomla API token (Bearer token) for authenticating with the target site's REST API." +PLG_TASK_MOKOWAASSYNC_API_USER_LABEL="API User" +PLG_TASK_MOKOWAASSYNC_API_USER_DESC="Optional username on the target site. Used for logging purposes only." + +; ===== Content types fieldset ===== +PLG_TASK_MOKOWAASSYNC_FIELDSET_CONTENT="Content to Sync" +PLG_TASK_MOKOWAASSYNC_SYNC_ARTICLES_LABEL="Articles" +PLG_TASK_MOKOWAASSYNC_SYNC_ARTICLES_DESC="Sync articles (com_content). Deletes all articles on the target, then pushes exact copies from this site." +PLG_TASK_MOKOWAASSYNC_SYNC_CATEGORIES_LABEL="Categories" +PLG_TASK_MOKOWAASSYNC_SYNC_CATEGORIES_DESC="Sync content categories. Ensures category structure matches this site." +PLG_TASK_MOKOWAASSYNC_SYNC_MENUS_LABEL="Menus" +PLG_TASK_MOKOWAASSYNC_SYNC_MENUS_DESC="Sync menu items. Deletes all menu items on the target, then pushes exact copies from this site." +PLG_TASK_MOKOWAASSYNC_SYNC_MODULES_LABEL="Modules" +PLG_TASK_MOKOWAASSYNC_SYNC_MODULES_DESC="Sync site modules. Pushes module configuration and assignments." + +; ===== Files fieldset ===== +PLG_TASK_MOKOWAASSYNC_FIELDSET_FILES="Files to Sync" +PLG_TASK_MOKOWAASSYNC_SYNC_IMAGES_LABEL="Images (/images/)" +PLG_TASK_MOKOWAASSYNC_SYNC_IMAGES_DESC="Sync the /images/ directory to the target site." +PLG_TASK_MOKOWAASSYNC_SYNC_FILES_LABEL="Files (/files/)" +PLG_TASK_MOKOWAASSYNC_SYNC_FILES_DESC="Sync the /files/ directory to the target site." +PLG_TASK_MOKOWAASSYNC_SYNC_MEDIA_LABEL="Media (/media/)" +PLG_TASK_MOKOWAASSYNC_SYNC_MEDIA_DESC="Sync the /media/ directory to the target site. Be careful — this includes extension assets." diff --git a/src/packages/plg_task_mokowaassync/mokowaassync.xml b/src/packages/plg_task_mokowaassync/mokowaassync.xml index e998392..094e893 100644 --- a/src/packages/plg_task_mokowaassync/mokowaassync.xml +++ b/src/packages/plg_task_mokowaassync/mokowaassync.xml @@ -12,7 +12,7 @@ GNU General Public License version 3 or later; see LICENSE hello@mokoconsulting.tech https://mokoconsulting.tech - 02.30.00 + 02.31.00 PLG_TASK_MOKOWAASSYNC_DESC Moko\Plugin\Task\MokoWaaSSync diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index 82015fe..997b274 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -20,16 +20,27 @@ use Joomla\Event\SubscriberInterface; /** * MokoWaaS Content Sync — Joomla Scheduled Task Plugin. * - * Pushes site content (articles, categories, menus, modules) to - * configured remote MokoWaaS sites on a schedule. Sync targets are - * read from the system plugin params (content_sync fieldset). + * Syncs selected content types to a single remote Joomla site via the + * REST API. Each task instance has its own target URL, API token, and + * content type toggles — so multiple targets can sync independently + * on different schedules. * - * @since 02.27.00 + * Sync strategy: delete-then-push for articles and menus to avoid + * duplicates. Categories are upserted. Files are pushed via the + * MokoWaaS sync-receive endpoint. + * + * @since 02.31.00 */ final class ContentSync extends CMSPlugin implements SubscriberInterface { use TaskPluginTrait; + /** @var string Target URL for the current sync run. */ + private string $targetUrl = ''; + + /** @var string Target health token for the current sync run. */ + private string $healthToken = ''; + protected const TASKS_MAP = [ 'mokowaas.content.sync' => [ 'langConstPrefix' => 'PLG_TASK_MOKOWAASSYNC_SYNC', @@ -48,117 +59,382 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface } /** - * Push content to all configured sync targets. - * - * Reads sync_targets from the MokoWaaS system plugin params, then - * delegates to ContentSyncService. Task-level overrides (if any) - * are merged on top. + * Sync content to the configured target. * * @param ExecuteTaskEvent $event The task event * * @return int Status::OK or Status::KNOCKOUT * - * @since 02.27.00 + * @since 02.31.00 */ private function syncContent(ExecuteTaskEvent $event): int { - $serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/ContentSyncService.php'; + $params = $event->getArgument('params'); - if (!file_exists($serviceFile)) + // Debug: log what we received + if (is_object($params)) { - $this->logTask('ContentSyncService.php not found — is plg_system_mokowaas installed?'); + $this->logTask('Params type: object, keys: ' . implode(', ', array_keys(get_object_vars($params)))); + } + elseif (is_array($params)) + { + $this->logTask('Params type: array, keys: ' . implode(', ', array_keys($params))); + $params = (object) $params; + } + else + { + $this->logTask('Params type: ' . gettype($params)); + } + + $targetUrl = rtrim($params->target_url ?? '', '/'); + + if (empty($targetUrl)) + { + $this->logTask('Sync target URL not configured'); return Status::KNOCKOUT; } - require_once $serviceFile; + $healthToken = trim($params->health_token ?? ''); - // Read sync targets from the system plugin params - $targets = $this->getSyncTargets(); - - if (empty($targets)) + if (empty($healthToken)) { - $this->logTask('No sync targets configured in MokoWaaS system plugin'); + $this->logTask('Target health token not configured — cannot sync'); + + return Status::KNOCKOUT; + } + + $this->targetUrl = $targetUrl; + $this->healthToken = $healthToken; + $errors = 0; + $synced = []; + + // Bulk-clear selected content types on target before pushing + $clearTypes = []; + + if ((int) ($params->sync_articles ?? 0) === 1) + { + $clearTypes[] = 'articles'; + } + + if ((int) ($params->sync_categories ?? 0) === 1) + { + $clearTypes[] = 'categories'; + } + + if ((int) ($params->sync_menus ?? 0) === 1) + { + $clearTypes[] = 'menus'; + } + + if ((int) ($params->sync_modules ?? 0) === 1) + { + $clearTypes[] = 'modules'; + } + + if (!empty($clearTypes)) + { + $clearResult = $this->bulkClear($targetUrl, $healthToken, $clearTypes); + + if (!$clearResult) + { + $this->logTask('Bulk clear failed — aborting sync'); + + return Status::KNOCKOUT; + } + + $this->logTask('Cleared on target: ' . implode(', ', $clearTypes)); + } + + // Push content types + if ((int) ($params->sync_categories ?? 0) === 1) + { + $result = $this->syncCategories(); + $synced[] = 'categories:' . ($result ? 'ok' : 'fail'); + if (!$result) $errors++; + } + + if ((int) ($params->sync_articles ?? 0) === 1) + { + $result = $this->syncArticles(); + $synced[] = 'articles:' . ($result ? 'ok' : 'fail'); + if (!$result) $errors++; + } + + if ((int) ($params->sync_menus ?? 0) === 1) + { + $result = $this->syncMenus(); + $synced[] = 'menus:' . ($result ? 'ok' : 'fail'); + if (!$result) $errors++; + } + + if ((int) ($params->sync_modules ?? 0) === 1) + { + $result = $this->syncModules(); + $synced[] = 'modules:' . ($result ? 'ok' : 'fail'); + if (!$result) $errors++; + } + + + $summary = implode(', ', $synced); + + if (empty($synced)) + { + $this->logTask('No content types selected for sync'); return Status::OK; } + $this->logTask("Sync to {$targetUrl}: {$summary}"); + + return $errors > 0 && $errors === count($synced) ? Status::KNOCKOUT : Status::OK; + } + + // ------------------------------------------------------------------ + // Bulk push via MokoWaaS syncpush endpoint + // ------------------------------------------------------------------ + + /** + * Sync articles: read from source DB, bulk-push to target. + * + * @param string $apiBase Target API base URL (unused — uses MokoWaaS endpoint) + * @param string $token API bearer token (unused — uses health token) + * + * @return bool + * + * @since 02.31.00 + */ + private function syncArticles(): bool + { try { - $service = new \Moko\Plugin\System\MokoWaaS\Service\ContentSyncService(); - $result = $service->syncAllTargets($targets); + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName(['title', 'alias', 'introtext', 'fulltext', 'state', 'catid', 'language', 'featured', 'metadesc', 'metakey', 'metadata', 'created', 'modified', 'publish_up', 'images', 'urls', 'attribs', 'access'])) + ->from($db->quoteName('#__content')) + ->where($db->quoteName('state') . ' >= 0'); + $db->setQuery($query); + $articles = $db->loadAssocList(); - $targetResults = $result['targets'] ?? []; - $okCount = 0; - $errCount = 0; + $targetUrl = rtrim($this->targetUrl, '/'); + $healthToken = $this->healthToken; - foreach ($targetResults as $tr) - { - if (($tr['status'] ?? '') === 'ok') - { - $okCount++; - } - else - { - $errCount++; - $this->logTask('Sync failed for ' . ($tr['target'] ?? 'unknown') . ': ' . ($tr['message'] ?? '')); - } - } + $result = $this->bulkPush($targetUrl, $healthToken, 'articles', $articles); - $this->logTask(sprintf('Content sync completed — %d ok, %d failed of %d target(s)', $okCount, $errCount, count($targetResults))); + $this->logTask(sprintf('Synced %d articles', count($articles))); - return $errCount > 0 && $okCount === 0 ? Status::KNOCKOUT : Status::OK; + return $result; } catch (\Throwable $e) { - $this->logTask('Content sync failed: ' . $e->getMessage()); + $this->logTask('Article sync failed: ' . $e->getMessage()); - return Status::KNOCKOUT; + return false; } } /** - * Read sync targets from the MokoWaaS system plugin configuration. + * Sync categories: push from source, creating or updating on target. * - * @return array Array of ['url' => ..., 'token' => ..., 'label' => ...] + * @param string $apiBase Target API base URL + * @param string $token API bearer token * - * @since 02.27.00 + * @return bool + * + * @since 02.31.00 */ - private function getSyncTargets(): array + private function syncCategories(): bool { try { - $db = Factory::getDbo(); + $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select($db->quoteName('params')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas')); - + ->select('title, alias, description, published, language, extension, access, params, metadata') + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('extension') . ' = ' . $db->quote('com_content')) + ->where($db->quoteName('published') . ' >= 0') + ->where($db->quoteName('id') . ' > 1') + ->order($db->quoteName('lft') . ' ASC'); $db->setQuery($query); - $raw = $db->loadResult(); + $categories = $db->loadAssocList(); - if (empty($raw)) - { - return []; - } + $targetUrl = rtrim($this->targetUrl, '/'); + $healthToken = $this->healthToken; - $params = json_decode($raw, true) ?: []; - $targets = $params['sync_targets'] ?? []; + $result = $this->bulkPush($targetUrl, $healthToken, 'categories', $categories); - if (is_string($targets)) - { - $targets = json_decode($targets, true) ?: []; - } + $this->logTask(sprintf('Synced %d categories', count($categories))); - return is_array($targets) ? $targets : []; + return $result; } catch (\Throwable $e) { - $this->logTask('Failed to read sync targets: ' . $e->getMessage()); + $this->logTask('Category sync failed: ' . $e->getMessage()); - return []; + return false; } } + + /** + * Sync menus via bulk push. + */ + private function syncMenus(): bool + { + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('title, alias, path, menutype, type, link, language, published, home, params, img, access') + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('client_id') . ' = 0') + ->where($db->quoteName('id') . ' > 1') + ->where($db->quoteName('published') . ' >= 0') + ->order($db->quoteName('lft') . ' ASC'); + $db->setQuery($query); + $items = $db->loadAssocList(); + + $targetUrl = rtrim($this->targetUrl, '/'); + $healthToken = $this->healthToken; + + $result = $this->bulkPush($targetUrl, $healthToken, 'menus', $items); + + $this->logTask(sprintf('Synced %d menu items', count($items))); + + return $result; + } + catch (\Throwable $e) + { + $this->logTask('Menu sync failed: ' . $e->getMessage()); + + return false; + } + } + + /** + * Sync modules via bulk push. + */ + private function syncModules(): bool + { + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('title, module, position, params, language, published, access, ordering, showtitle') + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = 0') + ->where($db->quoteName('published') . ' >= 0'); + $db->setQuery($query); + $modules = $db->loadAssocList(); + + $targetUrl = rtrim($this->targetUrl, '/'); + $healthToken = $this->healthToken; + + $result = $this->bulkPush($targetUrl, $healthToken, 'modules', $modules); + + $this->logTask(sprintf('Synced %d modules', count($modules))); + + return $result; + } + catch (\Throwable $e) + { + $this->logTask('Module sync failed: ' . $e->getMessage()); + + return false; + } + } + + // ------------------------------------------------------------------ + // HTTP helpers + // ------------------------------------------------------------------ + + /** + * Bulk-push content to the target via the MokoWaaS syncpush endpoint. + * + * Sends items in batches of 50 to prevent payload overload. + * + * @param string $targetUrl Target base URL + * @param string $token MokoWaaS health token + * @param string $type Content type (articles, categories, menus, modules) + * @param array $items Array of items to push + * + * @return bool + * + * @since 02.31.00 + */ + private function bulkPush(string $targetUrl, string $token, string $type, array $items): bool + { + $batches = array_chunk($items, 50); + + foreach ($batches as $i => $batch) + { + $payload = json_encode([ + 'token' => $token, + 'type' => $type, + 'items' => $batch, + ]); + + $ch = curl_init($targetUrl . '/?mokowaas=syncpush'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 120); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + $response = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode < 200 || $httpCode >= 300) + { + $this->logTask("Bulk push failed for {$type} batch " . ($i + 1) . ": HTTP {$httpCode} — " . substr($response, 0, 200)); + + return false; + } + } + + return true; + } + + /** + * Bulk-clear content on the target via the MokoWaaS syncclear endpoint. + * + * @param string $targetUrl Target base URL + * @param string $token MokoWaaS health token + * @param array $types Content types to clear + * + * @return bool + * + * @since 02.31.00 + */ + private function bulkClear(string $targetUrl, string $token, array $types): bool + { + $payload = json_encode([ + 'token' => $token, + 'types' => $types, + ]); + + $ch = curl_init($targetUrl . '/?mokowaas=syncclear'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 60); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + $response = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode >= 200 && $httpCode < 300) + { + $result = json_decode($response, true); + $this->logTask('Target cleared: ' . json_encode($result['cleared'] ?? [])); + + return true; + } + + $this->logTask('Bulk clear failed: HTTP ' . $httpCode . ' — ' . substr($response, 0, 200)); + + return false; + } + } diff --git a/src/packages/plg_webservices_mokowaas/mokowaas.xml b/src/packages/plg_webservices_mokowaas/mokowaas.xml index 8ac72cb..e133525 100644 --- a/src/packages/plg_webservices_mokowaas/mokowaas.xml +++ b/src/packages/plg_webservices_mokowaas/mokowaas.xml @@ -7,8 +7,8 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.30.00 - 02.30.00 + 02.31.00 + 02.31.00 Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info. Moko\Plugin\WebServices\MokoWaaS diff --git a/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml b/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml index 7b6b09f..93a73ed 100644 --- a/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml +++ b/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml @@ -7,8 +7,8 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.30.00 - 02.30.00 + 02.31.00 + 02.31.00 Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds. Moko\Plugin\WebServices\PerfectPublisher diff --git a/src/packages/plg_webservices_perfectpublisher/services/provider.php b/src/packages/plg_webservices_perfectpublisher/services/provider.php index 0b16ed6..9a72068 100644 --- a/src/packages/plg_webservices_perfectpublisher/services/provider.php +++ b/src/packages/plg_webservices_perfectpublisher/services/provider.php @@ -8,7 +8,7 @@ * INGROUP: MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * PATH: /src/packages/plg_webservices_perfectpublisher/services/provider.php - * VERSION: 02.30.00 + * VERSION: 02.31.00 * BRIEF: DI service provider for Perfect Publisher Web Services plugin */ diff --git a/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php b/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php index ef1c569..23ee6d1 100644 --- a/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php +++ b/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php @@ -8,7 +8,7 @@ * INGROUP: MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * PATH: /src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php - * VERSION: 02.30.00 + * VERSION: 02.31.00 * BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet) */ diff --git a/src/pkg_mokowaas.xml b/src/pkg_mokowaas.xml index 8bab697..1435449 100644 --- a/src/pkg_mokowaas.xml +++ b/src/pkg_mokowaas.xml @@ -2,8 +2,8 @@ Package - MokoWaaS mokowaas - 02.30.00 - 02.30.00 + 02.31.00 + 02.31.00 2026-05-23 Moko Consulting hello@mokoconsulting.tech diff --git a/src/script.php b/src/script.php index 03eace5..0d92f65 100644 --- a/src/script.php +++ b/src/script.php @@ -230,7 +230,7 @@ class Pkg_MokowaasInstallerScript * * @return void * - * @since 02.30.00 + * @since 02.31.00 */ private function cleanupStaleUpdateSites(): void {