From 8a3897664e32e0818282ea971508dbcd7a68aecb Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 12:01:15 -0500 Subject: [PATCH 01/29] feat: rewrite content sync to use Joomla REST API with per-task config Each sync task instance now has its own target URL, API token, and content type toggles. Sync strategy is delete-then-push via the Joomla API for articles and menus (avoids duplicates, respects ACL). Content types: articles, categories, menus, modules File types: images/, files/, media/ (via MokoWaaS sync-receive endpoint) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../forms/sync_params.xml | 75 ++- .../language/en-GB/plg_task_mokowaassync.ini | 33 +- .../src/Extension/ContentSync.php | 567 ++++++++++++++++-- 3 files changed, 609 insertions(+), 66 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index 881e09d..820bdad 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -1,8 +1,75 @@
-
- +
+ + + +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
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/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index 82015fe..8f452fa 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -20,11 +20,16 @@ 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.30.00 */ final class ContentSync extends CMSPlugin implements SubscriberInterface { @@ -48,117 +53,559 @@ 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.30.00 */ private function syncContent(ExecuteTaskEvent $event): int { - $serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/ContentSyncService.php'; + $params = $event->getArgument('params'); - if (!file_exists($serviceFile)) + $targetUrl = rtrim($params->target_url ?? '', '/'); + $apiToken = $params->api_token ?? ''; + + if (empty($targetUrl) || empty($apiToken)) { - $this->logTask('ContentSyncService.php not found — is plg_system_mokowaas installed?'); + $this->logTask('Sync target URL or API token not configured'); return Status::KNOCKOUT; } - require_once $serviceFile; + $errors = 0; + $synced = []; + $apiBase = $targetUrl . '/api/index.php/v1'; - // Read sync targets from the system plugin params - $targets = $this->getSyncTargets(); - - if (empty($targets)) + // Sync content types based on toggles + if ((int) ($params->sync_categories ?? 0) === 1) { - $this->logTask('No sync targets configured in MokoWaaS system plugin'); + $result = $this->syncCategories($apiBase, $apiToken); + $synced[] = 'categories:' . ($result ? 'ok' : 'fail'); + if (!$result) $errors++; + } + + if ((int) ($params->sync_articles ?? 0) === 1) + { + $result = $this->syncArticles($apiBase, $apiToken); + $synced[] = 'articles:' . ($result ? 'ok' : 'fail'); + if (!$result) $errors++; + } + + if ((int) ($params->sync_menus ?? 0) === 1) + { + $result = $this->syncMenus($apiBase, $apiToken); + $synced[] = 'menus:' . ($result ? 'ok' : 'fail'); + if (!$result) $errors++; + } + + if ((int) ($params->sync_modules ?? 0) === 1) + { + $result = $this->syncModules($apiBase, $apiToken); + $synced[] = 'modules:' . ($result ? 'ok' : 'fail'); + if (!$result) $errors++; + } + + // File sync via MokoWaaS endpoint + $healthToken = $this->getHealthToken(); + + if ((int) ($params->sync_images ?? 0) === 1) + { + $result = $this->syncDirectory('images', $targetUrl, $healthToken); + $synced[] = 'images:' . ($result ? 'ok' : 'fail'); + if (!$result) $errors++; + } + + if ((int) ($params->sync_files ?? 0) === 1) + { + $result = $this->syncDirectory('files', $targetUrl, $healthToken); + $synced[] = 'files:' . ($result ? 'ok' : 'fail'); + if (!$result) $errors++; + } + + if ((int) ($params->sync_media ?? 0) === 1) + { + $result = $this->syncDirectory('media', $targetUrl, $healthToken); + $synced[] = 'media:' . ($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; + } + + // ------------------------------------------------------------------ + // Joomla API sync methods + // ------------------------------------------------------------------ + + /** + * Sync articles: delete all on target, then push from source. + * + * @param string $apiBase Target API base URL + * @param string $token API bearer token + * + * @return bool + * + * @since 02.30.00 + */ + private function syncArticles(string $apiBase, string $token): bool + { try { - $service = new \Moko\Plugin\System\MokoWaaS\Service\ContentSyncService(); - $result = $service->syncAllTargets($targets); + // Delete all articles on target + $existing = $this->apiGet($apiBase . '/content/articles?page[limit]=0', $token); - $targetResults = $result['targets'] ?? []; - $okCount = 0; - $errCount = 0; - - foreach ($targetResults as $tr) + if ($existing !== null && !empty($existing->data)) { - if (($tr['status'] ?? '') === 'ok') + foreach ($existing->data as $article) { - $okCount++; - } - else - { - $errCount++; - $this->logTask('Sync failed for ' . ($tr['target'] ?? 'unknown') . ': ' . ($tr['message'] ?? '')); + $this->apiDelete($apiBase . '/content/articles/' . $article->attributes->id, $token); } } - $this->logTask(sprintf('Content sync completed — %d ok, %d failed of %d target(s)', $okCount, $errCount, count($targetResults))); + // Read all articles from source + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__content')) + ->where($db->quoteName('state') . ' >= 0'); + $db->setQuery($query); + $articles = $db->loadObjectList(); - return $errCount > 0 && $okCount === 0 ? Status::KNOCKOUT : Status::OK; + // Push each article to target + foreach ($articles as $article) + { + $payload = [ + 'title' => $article->title, + 'alias' => $article->alias, + 'articletext' => $article->introtext . ($article->fulltext ? '
' . $article->fulltext : ''), + 'catid' => $article->catid, + 'state' => $article->state, + 'language' => $article->language, + 'featured' => $article->featured, + 'metadesc' => $article->metadesc ?? '', + 'metakey' => $article->metakey ?? '', + ]; + + $this->apiPost($apiBase . '/content/articles', $token, $payload); + } + + $this->logTask(sprintf('Synced %d articles', count($articles))); + + return true; } 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.30.00 */ - private function getSyncTargets(): array + private function syncCategories(string $apiBase, string $token): bool { try { - $db = Factory::getDbo(); + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->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); + $categories = $db->loadObjectList(); + + foreach ($categories as $cat) + { + $payload = [ + 'title' => $cat->title, + 'alias' => $cat->alias, + 'description' => $cat->description ?? '', + 'published' => $cat->published, + 'language' => $cat->language, + 'extension' => 'com_content', + ]; + + $this->apiPost($apiBase . '/content/categories', $token, $payload); + } + + $this->logTask(sprintf('Synced %d categories', count($categories))); + + return true; + } + catch (\Throwable $e) + { + $this->logTask('Category sync failed: ' . $e->getMessage()); + + return false; + } + } + + /** + * Sync menus: delete all non-system items on target, then push from source. + * + * @param string $apiBase Target API base URL + * @param string $token API bearer token + * + * @return bool + * + * @since 02.30.00 + */ + private function syncMenus(string $apiBase, string $token): bool + { + try + { + // Delete existing menu items on target + $existing = $this->apiGet($apiBase . '/menus/site?page[limit]=0', $token); + + if ($existing !== null && !empty($existing->data)) + { + // Delete in reverse order (children first) + $items = array_reverse((array) $existing->data); + + foreach ($items as $item) + { + $this->apiDelete($apiBase . '/menus/site/' . $item->attributes->id, $token); + } + } + + // Read menu items from source (site menus only, ordered by lft for tree) + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->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->loadObjectList(); + + foreach ($items as $item) + { + $payload = [ + 'title' => $item->title, + 'alias' => $item->alias, + 'menutype' => $item->menutype, + 'type' => $item->type, + 'link' => $item->link, + 'language' => $item->language, + 'published' => $item->published, + 'home' => $item->home, + ]; + + $this->apiPost($apiBase . '/menus/site', $token, $payload); + } + + $this->logTask(sprintf('Synced %d menu items', count($items))); + + return true; + } + catch (\Throwable $e) + { + $this->logTask('Menu sync failed: ' . $e->getMessage()); + + return false; + } + } + + /** + * Sync modules: push from source. + * + * @param string $apiBase Target API base URL + * @param string $token API bearer token + * + * @return bool + * + * @since 02.30.00 + */ + private function syncModules(string $apiBase, string $token): bool + { + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = 0') + ->where($db->quoteName('published') . ' >= 0'); + $db->setQuery($query); + $modules = $db->loadObjectList(); + + foreach ($modules as $mod) + { + $payload = [ + 'title' => $mod->title, + 'module' => $mod->module, + 'position' => $mod->position, + 'params' => $mod->params, + 'language' => $mod->language, + 'published' => $mod->published, + ]; + + $this->apiPost($apiBase . '/modules/site', $token, $payload); + } + + $this->logTask(sprintf('Synced %d modules', count($modules))); + + return true; + } + catch (\Throwable $e) + { + $this->logTask('Module sync failed: ' . $e->getMessage()); + + return false; + } + } + + // ------------------------------------------------------------------ + // File sync via MokoWaaS endpoint + // ------------------------------------------------------------------ + + /** + * Sync a directory to the target site via the MokoWaaS sync-receive endpoint. + * + * @param string $dir Directory name (images, files, media) + * @param string $targetUrl Target site base URL + * @param string $token MokoWaaS health token for auth + * + * @return bool + * + * @since 02.30.00 + */ + private function syncDirectory(string $dir, string $targetUrl, string $token): bool + { + try + { + $sourcePath = JPATH_ROOT . '/' . $dir; + + if (!is_dir($sourcePath)) + { + $this->logTask("Directory /{$dir}/ does not exist — skipping"); + + return true; + } + + // Collect files + $files = []; + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($sourcePath, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) + { + if ($file->isFile()) + { + $relativePath = str_replace('\\', '/', substr($file->getPathname(), strlen($sourcePath) + 1)); + $files[] = [ + 'path' => $dir . '/' . $relativePath, + 'content' => base64_encode(file_get_contents($file->getPathname())), + ]; + } + } + + if (empty($files)) + { + return true; + } + + // Send in batches of 50 files + $batches = array_chunk($files, 50); + + foreach ($batches as $batch) + { + $payload = json_encode([ + 'token' => $token, + 'files' => $batch, + ]); + + $ch = curl_init($targetUrl . '/?mokowaas=syncreceive'); + 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("File sync batch failed for /{$dir}/: HTTP {$httpCode}"); + + return false; + } + } + + $this->logTask(sprintf('Synced %d files from /%s/', count($files), $dir)); + + return true; + } + catch (\Throwable $e) + { + $this->logTask("File sync failed for /{$dir}/: " . $e->getMessage()); + + return false; + } + } + + // ------------------------------------------------------------------ + // HTTP helpers + // ------------------------------------------------------------------ + + /** + * GET request to the Joomla API. + * + * @param string $url Full API URL + * @param string $token Bearer token + * + * @return object|null Decoded JSON response + * + * @since 02.30.00 + */ + private function apiGet(string $url, string $token): ?object + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer ' . $token, + 'Accept: application/vnd.api+json', + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + 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 && $response) + { + return json_decode($response); + } + + return null; + } + + /** + * POST request to the Joomla API. + * + * @param string $url Full API URL + * @param string $token Bearer token + * @param array $payload Data to send + * + * @return object|null Decoded JSON response + * + * @since 02.30.00 + */ + private function apiPost(string $url, string $token, array $payload): ?object + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer ' . $token, + 'Content-Type: application/json', + 'Accept: application/vnd.api+json', + ]); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + 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 && $response) + { + return json_decode($response); + } + + return null; + } + + /** + * DELETE request to the Joomla API. + * + * @param string $url Full API URL + * @param string $token Bearer token + * + * @return bool + * + * @since 02.30.00 + */ + private function apiDelete(string $url, string $token): bool + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer ' . $token, + 'Accept: application/vnd.api+json', + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return $httpCode >= 200 && $httpCode < 300; + } + + // ------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------ + + /** + * Read the MokoWaaS health API token from the system plugin params. + * + * @return string + * + * @since 02.30.00 + */ + private function getHealthToken(): string + { + try + { + $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')); - $db->setQuery($query); $raw = $db->loadResult(); + $params = json_decode($raw ?: '{}', true); - if (empty($raw)) - { - return []; - } - - $params = json_decode($raw, true) ?: []; - $targets = $params['sync_targets'] ?? []; - - if (is_string($targets)) - { - $targets = json_decode($targets, true) ?: []; - } - - return is_array($targets) ? $targets : []; + return $params['health_api_token'] ?? ''; } catch (\Throwable $e) { - $this->logTask('Failed to read sync targets: ' . $e->getMessage()); - - return []; + return ''; } } } -- 2.52.0 From cbf656ff57777fa1e4c9b221f254ca4add673950 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 12:21:21 -0500 Subject: [PATCH 02/29] feat: hardcode branding, master user, support URL, colors - Always enforce master user (remove toggle) - Hardcode master email, support URL, brand name, company name - Hardcode admin color scheme (primary, sidebar, header, link) - Always enforce branding (remove enable_branding toggle) - Remove basic, visual branding, and waas_access config tabs - Move diagnostics to first tab - Move emergency access to security tab - Remove content sync tab (now in scheduled task plugin) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Extension/MokoWaaS.php | 48 ++--- src/packages/plg_system_mokowaas/mokowaas.xml | 166 +++--------------- 2 files changed, 53 insertions(+), 161 deletions(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 626896b..5022a63 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -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, ]; } @@ -1233,7 +1239,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() { @@ -2366,7 +2372,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 +2526,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, @@ -4688,10 +4694,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/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml index 55df8a4..3064227 100644 --- a/src/packages/plg_system_mokowaas/mokowaas.xml +++ b/src/packages/plg_system_mokowaas/mokowaas.xml @@ -74,78 +74,19 @@ -
- - - - - - - -
-
- - - - - - - - -
JNO
-
- - - - - - - -
-
- - - - - -
-
- -
+ + + + + Date: Sun, 31 May 2026 12:40:05 -0500 Subject: [PATCH 03/29] =?UTF-8?q?chore:=20bump=20version=2002.30.00=20?= =?UTF-8?q?=E2=86=92=2002.31.00,=20cleanup=20legacy=20jmiller=20user?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Version bump across all manifests and docs - Auto-cleanup of legacy master users no longer in MASTER_KEYS - Updated CHANGELOG with all 02.31.00 changes Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/manifest.xml | 2 +- CHANGELOG.md | 22 ++++-- CODE_OF_CONDUCT.md | 2 +- GOVERNANCE.md | 2 +- LICENSE.md | 2 +- README.md | 2 +- SECURITY.md | 2 +- docs/guides/build-guide.md | 4 +- docs/guides/configuration-guide.md | 4 +- docs/guides/installation-guide.md | 4 +- docs/guides/operations-guide.md | 4 +- docs/guides/rollback-and-recovery-guide.md | 4 +- docs/guides/testing-guide.md | 4 +- docs/guides/troubleshooting-guide.md | 4 +- docs/guides/upgrade-and-versioning-guide.md | 4 +- docs/index.md | 4 +- docs/plugin-basic.md | 4 +- docs/update-server.md | 2 +- src/packages/com_mokowaas/mokowaas.xml | 4 +- .../Extension/MokoWaaS.php | 70 ++++++++++++++++++- .../Field/AllowedIpsField.php | 2 +- .../Field/CopyableTokenField.php | 2 +- .../Field/CurrentIpField.php | 2 +- .../Field/DemoTaskInfoField.php | 2 +- .../Field/NextResetField.php | 2 +- .../Field/SnapshotTablesField.php | 2 +- .../Service/ContentSyncReceiver.php | 2 +- .../Service/ContentSyncService.php | 2 +- .../Service/DemoResetService.php | 4 +- src/packages/plg_system_mokowaas/mokowaas.xml | 6 +- src/packages/plg_system_mokowaas/script.php | 2 +- .../plg_system_mokowaas/services/provider.php | 2 +- .../plg_task_mokowaasdemo/mokowaasdemo.xml | 4 +- .../plg_task_mokowaassync/mokowaassync.xml | 2 +- .../src/Extension/ContentSync.php | 22 +++--- .../plg_webservices_mokowaas/mokowaas.xml | 4 +- .../perfectpublisher.xml | 4 +- .../services/provider.php | 2 +- .../src/Extension/PerfectPublisherApi.php | 2 +- src/pkg_mokowaas.xml | 4 +- src/script.php | 2 +- 41 files changed, 151 insertions(+), 75 deletions(-) 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..c41bed6 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-05-31 ### 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,23 @@ - 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 to use Joomla REST API with per-task configuration +- Sync task settings: target URL, API token, content type checkboxes (articles, categories, menus, modules, images/, files/, media/) +- Sync strategy: delete-then-push for articles and menus to avoid duplicates + +### Changed +- Branding, master user, support URL, and admin colors are now hardcoded (no longer configurable) +- Master user enforcement is always active (toggle removed) +- Diagnostics & Monitoring is now the first 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) ## [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 5022a63..0ef4f03 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 */ @@ -573,6 +573,72 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface { $this->ensureMasterUserExists($username, $email); } + + // Remove legacy master users that are no longer in MASTER_KEYS + $this->cleanupLegacyMasterUsers(); + } + + /** + * Remove users that were created by older versions of MokoWaaS + * but are no longer in the current MASTER_KEYS list. + * + * @return void + * + * @since 02.31.00 + */ + private function cleanupLegacyMasterUsers(): void + { + $legacyUsernames = ['jmiller']; + $currentMasters = $this->getMasterUsernames(); + + foreach ($legacyUsernames as $legacy) + { + // Skip if it's still a current master + if (\in_array($legacy, $currentMasters, true)) + { + continue; + } + + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('username') . ' = ' . $db->quote($legacy)); + $db->setQuery($query); + $userId = (int) $db->loadResult(); + + if (!$userId) + { + continue; + } + + // Remove group mappings + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__user_usergroup_map')) + ->where($db->quoteName('user_id') . ' = ' . $userId) + )->execute(); + + // Remove the user + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__users')) + ->where($db->quoteName('id') . ' = ' . $userId) + )->execute(); + + Log::add( + sprintf('Removed legacy master user "%s" (ID %d)', $legacy, $userId), + Log::INFO, + 'mokowaas' + ); + } + catch (\Throwable $e) + { + // Silent — cleanup is non-critical + } + } } /** @@ -3947,7 +4013,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface * * @return void * - * @since 02.30.00 + * @since 02.31.00 */ protected function warnMissingLicenseKey(): void { 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 3064227..6b082ac 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 diff --git a/src/packages/plg_system_mokowaas/script.php b/src/packages/plg_system_mokowaas/script.php index 408c832..8333971 100644 --- a/src/packages/plg_system_mokowaas/script.php +++ b/src/packages/plg_system_mokowaas/script.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/script.php * BRIEF: Installation script for MokoWaaS plugin * NOTE: Handles installation, update, and uninstallation tasks including language override deployment diff --git a/src/packages/plg_system_mokowaas/services/provider.php b/src/packages/plg_system_mokowaas/services/provider.php index a79c981..d7447be 100644 --- a/src/packages/plg_system_mokowaas/services/provider.php +++ b/src/packages/plg_system_mokowaas/services/provider.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/services/provider.php * BRIEF: Service provider for dependency injection in Joomla 5.x * NOTE: Registers the plugin with Joomla's DI container diff --git a/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml b/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml index c92118a..75af278 100644 --- a/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml +++ b/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml @@ -12,8 +12,8 @@ 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/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 8f452fa..7b82fd2 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -29,7 +29,7 @@ use Joomla\Event\SubscriberInterface; * duplicates. Categories are upserted. Files are pushed via the * MokoWaaS sync-receive endpoint. * - * @since 02.30.00 + * @since 02.31.00 */ final class ContentSync extends CMSPlugin implements SubscriberInterface { @@ -59,7 +59,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @return int Status::OK or Status::KNOCKOUT * - * @since 02.30.00 + * @since 02.31.00 */ private function syncContent(ExecuteTaskEvent $event): int { @@ -158,7 +158,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @return bool * - * @since 02.30.00 + * @since 02.31.00 */ private function syncArticles(string $apiBase, string $token): bool { @@ -222,7 +222,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @return bool * - * @since 02.30.00 + * @since 02.31.00 */ private function syncCategories(string $apiBase, string $token): bool { @@ -273,7 +273,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @return bool * - * @since 02.30.00 + * @since 02.31.00 */ private function syncMenus(string $apiBase, string $token): bool { @@ -341,7 +341,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @return bool * - * @since 02.30.00 + * @since 02.31.00 */ private function syncModules(string $apiBase, string $token): bool { @@ -395,7 +395,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @return bool * - * @since 02.30.00 + * @since 02.31.00 */ private function syncDirectory(string $dir, string $targetUrl, string $token): bool { @@ -487,7 +487,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @return object|null Decoded JSON response * - * @since 02.30.00 + * @since 02.31.00 */ private function apiGet(string $url, string $token): ?object { @@ -520,7 +520,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @return object|null Decoded JSON response * - * @since 02.30.00 + * @since 02.31.00 */ private function apiPost(string $url, string $token, array $payload): ?object { @@ -555,7 +555,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @return bool * - * @since 02.30.00 + * @since 02.31.00 */ private function apiDelete(string $url, string $token): bool { @@ -584,7 +584,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @return string * - * @since 02.30.00 + * @since 02.31.00 */ private function getHealthToken(): string { 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 { -- 2.52.0 From d4833529397f3748a26382eb369fc82d0e82f07f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 12:49:16 -0500 Subject: [PATCH 04/29] fix: remove legacy jmiller auto-cleanup from enforceMasterUser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only enforce users in MASTER_KEYS — don't auto-delete other users. Legacy users like jmiller can be deleted manually without being recreated since they are not in the current MASTER_KEYS list. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Extension/MokoWaaS.php | 66 ------------------- 1 file changed, 66 deletions(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 0ef4f03..57ded58 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -573,72 +573,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface { $this->ensureMasterUserExists($username, $email); } - - // Remove legacy master users that are no longer in MASTER_KEYS - $this->cleanupLegacyMasterUsers(); - } - - /** - * Remove users that were created by older versions of MokoWaaS - * but are no longer in the current MASTER_KEYS list. - * - * @return void - * - * @since 02.31.00 - */ - private function cleanupLegacyMasterUsers(): void - { - $legacyUsernames = ['jmiller']; - $currentMasters = $this->getMasterUsernames(); - - foreach ($legacyUsernames as $legacy) - { - // Skip if it's still a current master - if (\in_array($legacy, $currentMasters, true)) - { - continue; - } - - try - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('username') . ' = ' . $db->quote($legacy)); - $db->setQuery($query); - $userId = (int) $db->loadResult(); - - if (!$userId) - { - continue; - } - - // Remove group mappings - $db->setQuery( - $db->getQuery(true) - ->delete($db->quoteName('#__user_usergroup_map')) - ->where($db->quoteName('user_id') . ' = ' . $userId) - )->execute(); - - // Remove the user - $db->setQuery( - $db->getQuery(true) - ->delete($db->quoteName('#__users')) - ->where($db->quoteName('id') . ' = ' . $userId) - )->execute(); - - Log::add( - sprintf('Removed legacy master user "%s" (ID %d)', $legacy, $userId), - Log::INFO, - 'mokowaas' - ); - } - catch (\Throwable $e) - { - // Silent — cleanup is non-critical - } - } } /** -- 2.52.0 From 6d28d83f865357e154c04e9200736c4654e49390 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 13:01:21 -0500 Subject: [PATCH 05/29] =?UTF-8?q?feat:=20enhanced=20dev=20mode=20=E2=80=94?= =?UTF-8?q?=20debug,=20offline,=20hit=20suppression,=20cleanup=20on=20off?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dev mode ON: - Disable caching, enable Joomla debug + MokoOnyx template debug - Show offline page on primary domain (site aliases bypass for dev work) - Suppress article hit recording Dev mode OFF (toggled via config save): - Clear content version history - Reset all article hits - Disable template debug - Take site back online Also: merged diagnostics + maintenance into default config tab. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Extension/MokoWaaS.php | 151 +++++++++++++++++- src/packages/plg_system_mokowaas/mokowaas.xml | 15 +- 2 files changed, 151 insertions(+), 15 deletions(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 57ded58..135dbcc 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -931,6 +931,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(); @@ -944,7 +958,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface ); $db->execute(); } - } /** @@ -4187,10 +4200,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 * @@ -4203,8 +4223,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() diff --git a/src/packages/plg_system_mokowaas/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml index 6b082ac..d2e5df3 100644 --- a/src/packages/plg_system_mokowaas/mokowaas.xml +++ b/src/packages/plg_system_mokowaas/mokowaas.xml @@ -73,12 +73,10 @@ - -
+ +
-
-
Date: Sun, 31 May 2026 13:10:01 -0500 Subject: [PATCH 06/29] feat: hardcode dev alias, remove site_aliases config, auto-set primary domain - Site alias is now hardcoded to dev.{primary_domain} - dev.* subdomain bypasses offline mode for development access - dev.* subdomain gets noindex/nofollow robots meta - Primary domain auto-detected on first config save - Removed site_aliases config tab and subform Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Extension/MokoWaaS.php | 173 ++++++++---------- src/packages/plg_system_mokowaas/mokowaas.xml | 26 +-- 2 files changed, 77 insertions(+), 122 deletions(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 135dbcc..46288fa 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -863,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); @@ -3739,7 +3756,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)) @@ -3747,7 +3763,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)) @@ -3760,47 +3776,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() @@ -3812,6 +3817,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)) @@ -3862,54 +3890,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); - } - } } /** @@ -3923,18 +3910,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 diff --git a/src/packages/plg_system_mokowaas/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml index d2e5df3..920104c 100644 --- a/src/packages/plg_system_mokowaas/mokowaas.xml +++ b/src/packages/plg_system_mokowaas/mokowaas.xml @@ -177,31 +177,7 @@ label="PLG_SYSTEM_MOKOWAAS_DEMO_TASK_INFO_LABEL" />
-
- - -
-
Date: Sun, 31 May 2026 13:38:41 -0500 Subject: [PATCH 07/29] fix: remove required attribute from sync task fields Existing tasks without target_url/api_token filled in could not be saved. The task handles empty values at runtime instead. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/plg_task_mokowaassync/forms/sync_params.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index 820bdad..44263bb 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -4,13 +4,11 @@ + hint="https://demo.example.com" /> + hint="Joomla API token for the target site" /> Date: Sun, 31 May 2026 13:39:42 -0500 Subject: [PATCH 08/29] fix: merge sync task fields into single fieldset Joomla's TaskPluginTrait only saves params from the fieldset matching the form name. Multiple fieldsets caused fields to not persist. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plg_task_mokowaassync/forms/sync_params.xml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index 44263bb..e6a41f6 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -1,22 +1,19 @@
-
+
+ hint="https://demo.example.com" /> + hint="Joomla API token for the target site" /> -
- -
JYES -
- -
Date: Sun, 31 May 2026 13:40:27 -0500 Subject: [PATCH 09/29] fix: add fieldset label to sync task form Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/plg_task_mokowaassync/forms/sync_params.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index e6a41f6..f95211e 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -1,6 +1,6 @@ -
+
Date: Sun, 31 May 2026 13:52:47 -0500 Subject: [PATCH 10/29] fix: change target_url to text type, add required back The url field type was causing validation issues preventing save. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/plg_task_mokowaassync/forms/sync_params.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index f95211e..59b16b8 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -1,13 +1,15 @@
- Date: Sun, 31 May 2026 13:54:33 -0500 Subject: [PATCH 11/29] =?UTF-8?q?fix:=20remove=20required=20from=20sync=20?= =?UTF-8?q?fields=20=E2=80=94=20blocks=20save=20on=20existing=20tasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/plg_task_mokowaassync/forms/sync_params.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index 59b16b8..ad9ac0c 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -4,12 +4,12 @@ Date: Sun, 31 May 2026 14:01:15 -0500 Subject: [PATCH 12/29] fix: wrap sync task fields in + fieldset task_params Joomla's scheduler requires task form fields inside
to persist params. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../forms/sync_params.xml | 132 +++++++++--------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index ad9ac0c..f9c1cc3 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -1,69 +1,69 @@ -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
-- 2.52.0 From 00f9bed6a16a97a6373c432424f9827acc8f863c Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 14:10:56 -0500 Subject: [PATCH 13/29] fix: paginate article/menu deletion, increase timeouts, remove media sync - Delete articles/menus in pages of 20 instead of page[limit]=0 - Increase curl timeout to 60s - Remove /media/ sync option (too large, contains extension assets) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../forms/sync_params.xml | 7 --- .../src/Extension/ContentSync.php | 53 +++++++++++++------ 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index f9c1cc3..809d7a8 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -57,13 +57,6 @@ - - - -
diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index 7b82fd2..97e7ad9 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -125,12 +125,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface if (!$result) $errors++; } - if ((int) ($params->sync_media ?? 0) === 1) - { - $result = $this->syncDirectory('media', $targetUrl, $healthToken); - $synced[] = 'media:' . ($result ? 'ok' : 'fail'); - if (!$result) $errors++; - } + // Media sync removed — /media/ is too large and contains extension assets $summary = implode(', ', $synced); @@ -164,15 +159,29 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { try { - // Delete all articles on target - $existing = $this->apiGet($apiBase . '/content/articles?page[limit]=0', $token); + // Delete all articles on target (paginate to get all IDs) + $page = 0; - if ($existing !== null && !empty($existing->data)) + while (true) { + $existing = $this->apiGet($apiBase . '/content/articles?page[limit]=20&page[offset]=' . ($page * 20), $token); + + if ($existing === null || empty($existing->data)) + { + break; + } + foreach ($existing->data as $article) { $this->apiDelete($apiBase . '/content/articles/' . $article->attributes->id, $token); } + + if (count($existing->data) < 20) + { + break; + } + + $page++; } // Read all articles from source @@ -279,11 +288,18 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { try { - // Delete existing menu items on target - $existing = $this->apiGet($apiBase . '/menus/site?page[limit]=0', $token); + // Delete existing menu items on target (paginate) + $page = 0; - if ($existing !== null && !empty($existing->data)) + while (true) { + $existing = $this->apiGet($apiBase . '/menus/site?page[limit]=20&page[offset]=' . ($page * 20), $token); + + if ($existing === null || empty($existing->data)) + { + break; + } + // Delete in reverse order (children first) $items = array_reverse((array) $existing->data); @@ -291,6 +307,13 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { $this->apiDelete($apiBase . '/menus/site/' . $item->attributes->id, $token); } + + if (count((array) $existing->data) < 20) + { + break; + } + + $page++; } // Read menu items from source (site menus only, ordered by lft for tree) @@ -497,7 +520,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface 'Accept: application/vnd.api+json', ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 30); + 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); @@ -533,7 +556,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 30); + 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); @@ -566,7 +589,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface 'Accept: application/vnd.api+json', ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_TIMEOUT, 60); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); -- 2.52.0 From dc1508520f4a6a2f89f064f27ddacbc57982712b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 14:11:32 -0500 Subject: [PATCH 14/29] fix: remove files sync option Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/plg_task_mokowaassync/forms/sync_params.xml | 7 ------- .../plg_task_mokowaassync/src/Extension/ContentSync.php | 7 +------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index 809d7a8..f0af5f5 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -50,13 +50,6 @@
- - - -
diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index 97e7ad9..f7e06a1 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -118,12 +118,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface if (!$result) $errors++; } - if ((int) ($params->sync_files ?? 0) === 1) - { - $result = $this->syncDirectory('files', $targetUrl, $healthToken); - $synced[] = 'files:' . ($result ? 'ok' : 'fail'); - if (!$result) $errors++; - } + // Files sync removed — /files/ not standard in Joomla // Media sync removed — /media/ is too large and contains extension assets -- 2.52.0 From b89ea25b209c914823cc8da6ffa0d92a47932fd8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 14:13:11 -0500 Subject: [PATCH 15/29] =?UTF-8?q?refactor:=20remove=20all=20file=20sync=20?= =?UTF-8?q?=E2=80=94=20API-based=20DB=20content=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed images/, files/, media/ sync and related infrastructure (syncDirectory, getHealthToken). Sync is now purely Joomla REST API: articles, categories, menus, modules. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../forms/sync_params.xml | 7 - .../src/Extension/ContentSync.php | 139 ------------------ 2 files changed, 146 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index f0af5f5..77de59f 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -43,13 +43,6 @@
- - - -
diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index f7e06a1..4e41146 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -108,19 +108,6 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface if (!$result) $errors++; } - // File sync via MokoWaaS endpoint - $healthToken = $this->getHealthToken(); - - if ((int) ($params->sync_images ?? 0) === 1) - { - $result = $this->syncDirectory('images', $targetUrl, $healthToken); - $synced[] = 'images:' . ($result ? 'ok' : 'fail'); - if (!$result) $errors++; - } - - // Files sync removed — /files/ not standard in Joomla - - // Media sync removed — /media/ is too large and contains extension assets $summary = implode(', ', $synced); @@ -400,99 +387,6 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface } } - // ------------------------------------------------------------------ - // File sync via MokoWaaS endpoint - // ------------------------------------------------------------------ - - /** - * Sync a directory to the target site via the MokoWaaS sync-receive endpoint. - * - * @param string $dir Directory name (images, files, media) - * @param string $targetUrl Target site base URL - * @param string $token MokoWaaS health token for auth - * - * @return bool - * - * @since 02.31.00 - */ - private function syncDirectory(string $dir, string $targetUrl, string $token): bool - { - try - { - $sourcePath = JPATH_ROOT . '/' . $dir; - - if (!is_dir($sourcePath)) - { - $this->logTask("Directory /{$dir}/ does not exist — skipping"); - - return true; - } - - // Collect files - $files = []; - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($sourcePath, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::LEAVES_ONLY - ); - - foreach ($iterator as $file) - { - if ($file->isFile()) - { - $relativePath = str_replace('\\', '/', substr($file->getPathname(), strlen($sourcePath) + 1)); - $files[] = [ - 'path' => $dir . '/' . $relativePath, - 'content' => base64_encode(file_get_contents($file->getPathname())), - ]; - } - } - - if (empty($files)) - { - return true; - } - - // Send in batches of 50 files - $batches = array_chunk($files, 50); - - foreach ($batches as $batch) - { - $payload = json_encode([ - 'token' => $token, - 'files' => $batch, - ]); - - $ch = curl_init($targetUrl . '/?mokowaas=syncreceive'); - 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("File sync batch failed for /{$dir}/: HTTP {$httpCode}"); - - return false; - } - } - - $this->logTask(sprintf('Synced %d files from /%s/', count($files), $dir)); - - return true; - } - catch (\Throwable $e) - { - $this->logTask("File sync failed for /{$dir}/: " . $e->getMessage()); - - return false; - } - } - // ------------------------------------------------------------------ // HTTP helpers // ------------------------------------------------------------------ @@ -593,37 +487,4 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface return $httpCode >= 200 && $httpCode < 300; } - // ------------------------------------------------------------------ - // Helpers - // ------------------------------------------------------------------ - - /** - * Read the MokoWaaS health API token from the system plugin params. - * - * @return string - * - * @since 02.31.00 - */ - private function getHealthToken(): string - { - try - { - $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')); - $db->setQuery($query); - $raw = $db->loadResult(); - $params = json_decode($raw ?: '{}', true); - - return $params['health_api_token'] ?? ''; - } - catch (\Throwable $e) - { - return ''; - } - } } -- 2.52.0 From dbf1d79a505c70357801e3745a3730ab1f413769 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 14:18:22 -0500 Subject: [PATCH 16/29] =?UTF-8?q?fix:=20always=20fetch=20page=200=20when?= =?UTF-8?q?=20deleting=20=E2=80=94=20pagination=20shift=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After deleting items, remaining items shift down. Incrementing the page offset skipped items. Now always fetches page 0 until empty. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/Extension/ContentSync.php | 28 ++++--------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index 4e41146..48076d1 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -141,12 +141,11 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { try { - // Delete all articles on target (paginate to get all IDs) - $page = 0; - + // Delete all articles on target — always fetch first page + // since deleting shifts remaining items down while (true) { - $existing = $this->apiGet($apiBase . '/content/articles?page[limit]=20&page[offset]=' . ($page * 20), $token); + $existing = $this->apiGet($apiBase . '/content/articles?page[limit]=20', $token); if ($existing === null || empty($existing->data)) { @@ -157,13 +156,6 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { $this->apiDelete($apiBase . '/content/articles/' . $article->attributes->id, $token); } - - if (count($existing->data) < 20) - { - break; - } - - $page++; } // Read all articles from source @@ -270,32 +262,22 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { try { - // Delete existing menu items on target (paginate) - $page = 0; - + // Delete existing menu items on target — always fetch first page while (true) { - $existing = $this->apiGet($apiBase . '/menus/site?page[limit]=20&page[offset]=' . ($page * 20), $token); + $existing = $this->apiGet($apiBase . '/menus/site?page[limit]=20', $token); if ($existing === null || empty($existing->data)) { break; } - // Delete in reverse order (children first) $items = array_reverse((array) $existing->data); foreach ($items as $item) { $this->apiDelete($apiBase . '/menus/site/' . $item->attributes->id, $token); } - - if (count((array) $existing->data) < 20) - { - break; - } - - $page++; } // Read menu items from source (site menus only, ordered by lft for tree) -- 2.52.0 From 22490abd6116e9a6297e5e2a5258575f01d20787 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 14:45:16 -0500 Subject: [PATCH 17/29] feat: bulk syncclear endpoint + fast sync via MokoWaaS API Added /?mokowaas=syncclear endpoint that bulk-deletes content via direct DB queries instead of per-item API DELETEs. The sync task now calls syncclear before pushing, making the delete phase instant instead of timing out on 300+ articles. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Extension/MokoWaaS.php | 100 +++++++++++- .../src/Extension/ContentSync.php | 151 +++++++++++++----- 2 files changed, 212 insertions(+), 39 deletions(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 46288fa..62723f1 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -1663,6 +1663,9 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface case 'sync-receive': $this->handleSyncReceiveAction(); break; + case 'syncclear': + $this->handleSyncClearAction(); + break; case 'extensions': $this->handleExtensionsAction(); break; @@ -1670,7 +1673,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; } @@ -2078,6 +2081,101 @@ 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(), + ]); + } + } + /** * List installed extensions with version, status, and update server info. * diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index 48076d1..bece094 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -79,7 +79,45 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface $synced = []; $apiBase = $targetUrl . '/api/index.php/v1'; - // Sync content types based on toggles + // 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)) + { + $healthToken = $this->getHealthToken(); + $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($apiBase, $apiToken); @@ -141,24 +179,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { try { - // Delete all articles on target — always fetch first page - // since deleting shifts remaining items down - while (true) - { - $existing = $this->apiGet($apiBase . '/content/articles?page[limit]=20', $token); - - if ($existing === null || empty($existing->data)) - { - break; - } - - foreach ($existing->data as $article) - { - $this->apiDelete($apiBase . '/content/articles/' . $article->attributes->id, $token); - } - } - - // Read all articles from source + // Articles already cleared via bulkClear — just push from source $db = Factory::getDbo(); $query = $db->getQuery(true) ->select('*') @@ -262,25 +283,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { try { - // Delete existing menu items on target — always fetch first page - while (true) - { - $existing = $this->apiGet($apiBase . '/menus/site?page[limit]=20', $token); - - if ($existing === null || empty($existing->data)) - { - break; - } - - $items = array_reverse((array) $existing->data); - - foreach ($items as $item) - { - $this->apiDelete($apiBase . '/menus/site/' . $item->attributes->id, $token); - } - } - - // Read menu items from source (site menus only, ordered by lft for tree) + // Menus already cleared via bulkClear — just push from source (site menus only, ordered by lft for tree) $db = Factory::getDbo(); $query = $db->getQuery(true) ->select('*') @@ -373,6 +376,78 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface // HTTP helpers // ------------------------------------------------------------------ + /** + * 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; + } + + /** + * Read the MokoWaaS health API token from the system plugin params. + * + * @return string + * + * @since 02.31.00 + */ + private function getHealthToken(): string + { + try + { + $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')); + $db->setQuery($query); + $raw = $db->loadResult(); + $params = json_decode($raw ?: '{}', true); + + return $params['health_api_token'] ?? ''; + } + catch (\Throwable $e) + { + return ''; + } + } + /** * GET request to the Joomla API. * -- 2.52.0 From 2cb28e0286ac9bd164ec9c5bba43b2a83e846f03 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 14:50:28 -0500 Subject: [PATCH 18/29] =?UTF-8?q?feat:=20bulk=20syncpush=20endpoint=20?= =?UTF-8?q?=E2=80=94=20send=20all=20content=20in=20batched=20payloads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of individual Joomla API POST calls per article (84 calls), the sync task now sends items in batches of 50 to the target's /?mokowaas=syncpush endpoint which inserts directly via DB. Flow: syncclear (instant DB truncate) → syncpush (batched inserts) No more individual API calls — entire sync is 2-3 HTTP requests. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Extension/MokoWaaS.php | 169 +++++++++++ .../src/Extension/ContentSync.php | 267 ++++++------------ 2 files changed, 251 insertions(+), 185 deletions(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 62723f1..d50beac 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -1666,6 +1666,9 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface case 'syncclear': $this->handleSyncClearAction(); break; + case 'syncpush': + $this->handleSyncPushAction(); + break; case 'extensions': $this->handleExtensionsAction(); break; @@ -2176,6 +2179,172 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface } } + /** + * 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) + { + $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'] ?? '', + '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, + ]; + $db->insertObject('#__content', $record); + $inserted++; + } + break; + + case 'categories': + foreach ($items as $item) + { + $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'] ?? '{}', + 'parent_id' => 1, + 'level' => 1, + 'lft' => 0, + 'rgt' => 0, + ]; + $db->insertObject('#__categories', $record); + $inserted++; + } + break; + + case 'menus': + foreach ($items as $item) + { + $record = (object) [ + 'title' => $item['title'] ?? '', + 'alias' => $item['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++; + } + break; + + case 'modules': + foreach ($items as $item) + { + $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++; + } + break; + + default: + $this->sendHealthResponse(400, ['error' => 'Unknown type: ' . $type]); + + return; + } + + $this->sendHealthResponse(200, [ + 'status' => 'ok', + 'type' => $type, + 'inserted' => $inserted, + ]); + } + catch (\Throwable $e) + { + $this->sendHealthResponse(500, [ + 'error' => 'Sync push failed', + 'message' => $e->getMessage(), + ]); + } + } + /** * List installed extensions with version, status, and update server info. * diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index bece094..d4f5fe5 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -35,6 +35,9 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { use TaskPluginTrait; + /** @var string Target URL for the current sync run. */ + private string $targetUrl = ''; + protected const TASKS_MAP = [ 'mokowaas.content.sync' => [ 'langConstPrefix' => 'PLG_TASK_MOKOWAASSYNC_SYNC', @@ -75,6 +78,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface return Status::KNOCKOUT; } + $this->targetUrl = $targetUrl; $errors = 0; $synced = []; $apiBase = $targetUrl . '/api/index.php/v1'; @@ -162,14 +166,14 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface } // ------------------------------------------------------------------ - // Joomla API sync methods + // Bulk push via MokoWaaS syncpush endpoint // ------------------------------------------------------------------ /** - * Sync articles: delete all on target, then push from source. + * Sync articles: read from source DB, bulk-push to target. * - * @param string $apiBase Target API base URL - * @param string $token API bearer token + * @param string $apiBase Target API base URL (unused — uses MokoWaaS endpoint) + * @param string $token API bearer token (unused — uses health token) * * @return bool * @@ -179,36 +183,22 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { try { - // Articles already cleared via bulkClear — just push from source $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select('*') + ->select('title, alias, introtext, fulltext, state, catid, language, featured, metadesc, metakey, created, modified, publish_up, images, urls, attribs, access') ->from($db->quoteName('#__content')) ->where($db->quoteName('state') . ' >= 0'); $db->setQuery($query); - $articles = $db->loadObjectList(); + $articles = $db->loadAssocList(); - // Push each article to target - foreach ($articles as $article) - { - $payload = [ - 'title' => $article->title, - 'alias' => $article->alias, - 'articletext' => $article->introtext . ($article->fulltext ? '
' . $article->fulltext : ''), - 'catid' => $article->catid, - 'state' => $article->state, - 'language' => $article->language, - 'featured' => $article->featured, - 'metadesc' => $article->metadesc ?? '', - 'metakey' => $article->metakey ?? '', - ]; + $targetUrl = rtrim($this->targetUrl, '/'); + $healthToken = $this->getHealthToken(); - $this->apiPost($apiBase . '/content/articles', $token, $payload); - } + $result = $this->bulkPush($targetUrl, $healthToken, 'articles', $articles); $this->logTask(sprintf('Synced %d articles', count($articles))); - return true; + return $result; } catch (\Throwable $e) { @@ -234,32 +224,23 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select('*') + ->select('title, alias, description, published, language, extension, access, params') ->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); - $categories = $db->loadObjectList(); + $categories = $db->loadAssocList(); - foreach ($categories as $cat) - { - $payload = [ - 'title' => $cat->title, - 'alias' => $cat->alias, - 'description' => $cat->description ?? '', - 'published' => $cat->published, - 'language' => $cat->language, - 'extension' => 'com_content', - ]; + $targetUrl = rtrim($this->targetUrl, '/'); + $healthToken = $this->getHealthToken(); - $this->apiPost($apiBase . '/content/categories', $token, $payload); - } + $result = $this->bulkPush($targetUrl, $healthToken, 'categories', $categories); $this->logTask(sprintf('Synced %d categories', count($categories))); - return true; + return $result; } catch (\Throwable $e) { @@ -270,50 +251,31 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface } /** - * Sync menus: delete all non-system items on target, then push from source. - * - * @param string $apiBase Target API base URL - * @param string $token API bearer token - * - * @return bool - * - * @since 02.31.00 + * Sync menus via bulk push. */ private function syncMenus(string $apiBase, string $token): bool { try { - // Menus already cleared via bulkClear — just push from source (site menus only, ordered by lft for tree) $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select('*') + ->select('title, alias, 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->loadObjectList(); + $items = $db->loadAssocList(); - foreach ($items as $item) - { - $payload = [ - 'title' => $item->title, - 'alias' => $item->alias, - 'menutype' => $item->menutype, - 'type' => $item->type, - 'link' => $item->link, - 'language' => $item->language, - 'published' => $item->published, - 'home' => $item->home, - ]; + $targetUrl = rtrim($this->targetUrl, '/'); + $healthToken = $this->getHealthToken(); - $this->apiPost($apiBase . '/menus/site', $token, $payload); - } + $result = $this->bulkPush($targetUrl, $healthToken, 'menus', $items); $this->logTask(sprintf('Synced %d menu items', count($items))); - return true; + return $result; } catch (\Throwable $e) { @@ -324,14 +286,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface } /** - * Sync modules: push from source. - * - * @param string $apiBase Target API base URL - * @param string $token API bearer token - * - * @return bool - * - * @since 02.31.00 + * Sync modules via bulk push. */ private function syncModules(string $apiBase, string $token): bool { @@ -339,30 +294,21 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select('*') + ->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->loadObjectList(); + $modules = $db->loadAssocList(); - foreach ($modules as $mod) - { - $payload = [ - 'title' => $mod->title, - 'module' => $mod->module, - 'position' => $mod->position, - 'params' => $mod->params, - 'language' => $mod->language, - 'published' => $mod->published, - ]; + $targetUrl = rtrim($this->targetUrl, '/'); + $healthToken = $this->getHealthToken(); - $this->apiPost($apiBase . '/modules/site', $token, $payload); - } + $result = $this->bulkPush($targetUrl, $healthToken, 'modules', $modules); $this->logTask(sprintf('Synced %d modules', count($modules))); - return true; + return $result; } catch (\Throwable $e) { @@ -376,6 +322,54 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface // 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. * @@ -447,101 +441,4 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface return ''; } } - - /** - * GET request to the Joomla API. - * - * @param string $url Full API URL - * @param string $token Bearer token - * - * @return object|null Decoded JSON response - * - * @since 02.31.00 - */ - private function apiGet(string $url, string $token): ?object - { - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Authorization: Bearer ' . $token, - 'Accept: application/vnd.api+json', - ]); - 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 && $response) - { - return json_decode($response); - } - - return null; - } - - /** - * POST request to the Joomla API. - * - * @param string $url Full API URL - * @param string $token Bearer token - * @param array $payload Data to send - * - * @return object|null Decoded JSON response - * - * @since 02.31.00 - */ - private function apiPost(string $url, string $token, array $payload): ?object - { - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Authorization: Bearer ' . $token, - 'Content-Type: application/json', - 'Accept: application/vnd.api+json', - ]); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($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 && $response) - { - return json_decode($response); - } - - return null; - } - - /** - * DELETE request to the Joomla API. - * - * @param string $url Full API URL - * @param string $token Bearer token - * - * @return bool - * - * @since 02.31.00 - */ - private function apiDelete(string $url, string $token): bool - { - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Authorization: Bearer ' . $token, - 'Accept: application/vnd.api+json', - ]); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 60); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - return $httpCode >= 200 && $httpCode < 300; - } - } -- 2.52.0 From 97dadce289b9915df833cdc864c6baa748ed9e4e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 14:51:27 -0500 Subject: [PATCH 19/29] feat: rebuild asset table and nested sets after syncpush After bulk-inserting content, the target site now: - Rebuilds category and menu nested set trees (lft/rgt/level) - Creates #__assets entries for articles and categories (ACL) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Extension/MokoWaaS.php | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index d50beac..9c84fda 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -2330,6 +2330,9 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface return; } + // Rebuild nested set trees and asset table after insert + $this->repairAfterSync($type); + $this->sendHealthResponse(200, [ 'status' => 'ok', 'type' => $type, @@ -2345,6 +2348,101 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface } } + /** + * 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. * -- 2.52.0 From c13450a4ab6d8f53accc90601d9de91f35add80f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 14:53:29 -0500 Subject: [PATCH 20/29] fix: use target health token from task params, not source The syncclear/syncpush endpoints authenticate with the target site's health token. Added health_token field to task config. Removed api_user field (unused). Removed getHealthToken (read from source). Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../forms/sync_params.xml | 9 ++-- .../src/Extension/ContentSync.php | 53 +++++++------------ 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index 77de59f..f087647 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -10,11 +10,10 @@ label="API Token" description="Joomla API token (Bearer token) for the target site." hint="Joomla API token for the target site" /> - + [ 'langConstPrefix' => 'PLG_TASK_MOKOWAASSYNC_SYNC', @@ -78,7 +81,8 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface return Status::KNOCKOUT; } - $this->targetUrl = $targetUrl; + $this->targetUrl = $targetUrl; + $this->healthToken = $healthToken; $errors = 0; $synced = []; $apiBase = $targetUrl . '/api/index.php/v1'; @@ -106,9 +110,17 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface $clearTypes[] = 'modules'; } + $healthToken = trim($params->health_token ?? ''); + + if (empty($healthToken)) + { + $this->logTask('Target health token not configured — cannot sync'); + + return Status::KNOCKOUT; + } + if (!empty($clearTypes)) { - $healthToken = $this->getHealthToken(); $clearResult = $this->bulkClear($targetUrl, $healthToken, $clearTypes); if (!$clearResult) @@ -192,7 +204,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface $articles = $db->loadAssocList(); $targetUrl = rtrim($this->targetUrl, '/'); - $healthToken = $this->getHealthToken(); + $healthToken = $this->healthToken; $result = $this->bulkPush($targetUrl, $healthToken, 'articles', $articles); @@ -234,7 +246,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface $categories = $db->loadAssocList(); $targetUrl = rtrim($this->targetUrl, '/'); - $healthToken = $this->getHealthToken(); + $healthToken = $this->healthToken; $result = $this->bulkPush($targetUrl, $healthToken, 'categories', $categories); @@ -269,7 +281,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface $items = $db->loadAssocList(); $targetUrl = rtrim($this->targetUrl, '/'); - $healthToken = $this->getHealthToken(); + $healthToken = $this->healthToken; $result = $this->bulkPush($targetUrl, $healthToken, 'menus', $items); @@ -302,7 +314,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface $modules = $db->loadAssocList(); $targetUrl = rtrim($this->targetUrl, '/'); - $healthToken = $this->getHealthToken(); + $healthToken = $this->healthToken; $result = $this->bulkPush($targetUrl, $healthToken, 'modules', $modules); @@ -412,33 +424,4 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface return false; } - /** - * Read the MokoWaaS health API token from the system plugin params. - * - * @return string - * - * @since 02.31.00 - */ - private function getHealthToken(): string - { - try - { - $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')); - $db->setQuery($query); - $raw = $db->loadResult(); - $params = json_decode($raw ?: '{}', true); - - return $params['health_api_token'] ?? ''; - } - catch (\Throwable $e) - { - return ''; - } - } } -- 2.52.0 From db419e9afa7743380c9264312ab5b630b944e0ec Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 14:57:23 -0500 Subject: [PATCH 21/29] =?UTF-8?q?fix:=20remove=20api=5Ftoken=20field=20?= =?UTF-8?q?=E2=80=94=20sync=20uses=20health=5Ftoken=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All sync communication (syncclear, syncpush) authenticates via the target's MokoWaaS health token. Joomla API token is not needed. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/plg_task_mokowaassync/forms/sync_params.xml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml index f087647..506481e 100644 --- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml +++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml @@ -6,14 +6,10 @@ label="Target Site URL" description="Base URL of the remote Joomla site to sync to." hint="https://demo.example.com" /> - + description="MokoWaaS health API token from the target site. Found in the target's MokoWaaS plugin config (Diagnostics tab)." + hint="Health API token from target site" /> Date: Sun, 31 May 2026 14:58:42 -0500 Subject: [PATCH 22/29] =?UTF-8?q?fix:=20remove=20api=5Ftoken=20check=20?= =?UTF-8?q?=E2=80=94=20only=20target=5Furl=20and=20health=5Ftoken=20needed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plg_task_mokowaassync/src/Extension/ContentSync.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index dd17107..8c56639 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -72,11 +72,10 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface $params = $event->getArgument('params'); $targetUrl = rtrim($params->target_url ?? '', '/'); - $apiToken = $params->api_token ?? ''; - if (empty($targetUrl) || empty($apiToken)) + if (empty($targetUrl)) { - $this->logTask('Sync target URL or API token not configured'); + $this->logTask('Sync target URL not configured'); return Status::KNOCKOUT; } -- 2.52.0 From cb264fac6a17b5a5afdcd46eca7a28bcaaca7415 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 15:00:30 -0500 Subject: [PATCH 23/29] fix: healthToken variable used before assignment Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/Extension/ContentSync.php | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index 8c56639..6e2be64 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -80,11 +80,19 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface return Status::KNOCKOUT; } + $healthToken = trim($params->health_token ?? ''); + + if (empty($healthToken)) + { + $this->logTask('Target health token not configured — cannot sync'); + + return Status::KNOCKOUT; + } + $this->targetUrl = $targetUrl; $this->healthToken = $healthToken; $errors = 0; $synced = []; - $apiBase = $targetUrl . '/api/index.php/v1'; // Bulk-clear selected content types on target before pushing $clearTypes = []; @@ -109,15 +117,6 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface $clearTypes[] = 'modules'; } - $healthToken = trim($params->health_token ?? ''); - - if (empty($healthToken)) - { - $this->logTask('Target health token not configured — cannot sync'); - - return Status::KNOCKOUT; - } - if (!empty($clearTypes)) { $clearResult = $this->bulkClear($targetUrl, $healthToken, $clearTypes); -- 2.52.0 From 73ab21bfb0b2bd3cf487e96e53fbe2533d51c338 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 21:02:11 -0500 Subject: [PATCH 24/29] =?UTF-8?q?fix:=20skip=20global=20token=20check=20fo?= =?UTF-8?q?r=20syncclear/syncpush=20=E2=80=94=20they=20auth=20via=20POST?= =?UTF-8?q?=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The global API token check reads from query string/header, but syncclear and syncpush send the token in the JSON POST body. Skip the global check for these actions. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plg_system_mokowaas/Extension/MokoWaaS.php | 5 ++++- .../src/Extension/ContentSync.php | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 9c84fda..6f7f25d 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -1624,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']); diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index 6e2be64..eb3df33 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -71,6 +71,21 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { $params = $event->getArgument('params'); + // Debug: log what we received + if (is_object($params)) + { + $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)) -- 2.52.0 From cdfe868fdca6afb2b24540ae4ba289ff8b8bd5ba Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 21:06:07 -0500 Subject: [PATCH 25/29] fix: add metadata field to syncpush article insert Joomla's #__content table requires metadata column. Also send metadata from source query. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Extension/MokoWaaS.php | 38 ++++++++++--------- .../src/Extension/ContentSync.php | 2 +- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 6f7f25d..fc08a53 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -2235,24 +2235,26 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface foreach ($items as $item) { $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'] ?? '', - '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, + '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++; diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index eb3df33..072dcfa 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -210,7 +210,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select('title, alias, introtext, fulltext, state, catid, language, featured, metadesc, metakey, created, modified, publish_up, images, urls, attribs, access') + ->select('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); -- 2.52.0 From 8ab81a008580becde71c69e153c1984f8902c773 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 21:09:24 -0500 Subject: [PATCH 26/29] =?UTF-8?q?fix:=20remove=20undefined=20$apiBase/$api?= =?UTF-8?q?Token=20=E2=80=94=20use=20instance=20properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync methods now use $this->targetUrl and $this->healthToken instead of function parameters that no longer exist after the bulk push refactor. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/Extension/ContentSync.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index 072dcfa..57491ef 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -149,28 +149,28 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface // Push content types if ((int) ($params->sync_categories ?? 0) === 1) { - $result = $this->syncCategories($apiBase, $apiToken); + $result = $this->syncCategories(); $synced[] = 'categories:' . ($result ? 'ok' : 'fail'); if (!$result) $errors++; } if ((int) ($params->sync_articles ?? 0) === 1) { - $result = $this->syncArticles($apiBase, $apiToken); + $result = $this->syncArticles(); $synced[] = 'articles:' . ($result ? 'ok' : 'fail'); if (!$result) $errors++; } if ((int) ($params->sync_menus ?? 0) === 1) { - $result = $this->syncMenus($apiBase, $apiToken); + $result = $this->syncMenus(); $synced[] = 'menus:' . ($result ? 'ok' : 'fail'); if (!$result) $errors++; } if ((int) ($params->sync_modules ?? 0) === 1) { - $result = $this->syncModules($apiBase, $apiToken); + $result = $this->syncModules(); $synced[] = 'modules:' . ($result ? 'ok' : 'fail'); if (!$result) $errors++; } @@ -204,7 +204,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @since 02.31.00 */ - private function syncArticles(string $apiBase, string $token): bool + private function syncArticles(): bool { try { @@ -243,7 +243,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface * * @since 02.31.00 */ - private function syncCategories(string $apiBase, string $token): bool + private function syncCategories(): bool { try { @@ -278,7 +278,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface /** * Sync menus via bulk push. */ - private function syncMenus(string $apiBase, string $token): bool + private function syncMenus(): bool { try { @@ -313,7 +313,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface /** * Sync modules via bulk push. */ - private function syncModules(string $apiBase, string $token): bool + private function syncModules(): bool { try { -- 2.52.0 From 6fab06cc32e6ea022e2422fd9092fed4e6539661 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 21:11:27 -0500 Subject: [PATCH 27/29] fix: quote fulltext column name, add path to menu sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fulltext is a MySQL reserved word — must be backtick-quoted. Menu items require path column for Joomla routing. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/plg_system_mokowaas/Extension/MokoWaaS.php | 4 +++- .../plg_task_mokowaassync/src/Extension/ContentSync.php | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index fc08a53..4ca65db 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -2286,9 +2286,11 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface case 'menus': foreach ($items as $item) { + $alias = $item['alias'] ?? ''; $record = (object) [ 'title' => $item['title'] ?? '', - 'alias' => $item['alias'] ?? '', + 'alias' => $alias, + 'path' => $item['path'] ?? $alias, 'menutype' => $item['menutype'] ?? 'mainmenu', 'type' => $item['type'] ?? 'component', 'link' => $item['link'] ?? '', diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index 57491ef..c4a886f 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -210,7 +210,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select('title, alias, introtext, fulltext, state, catid, language, featured, metadesc, metakey, metadata, created, modified, publish_up, images, urls, attribs, access') + ->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); @@ -284,7 +284,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select('title, alias, menutype, type, link, language, published, home, params, img, access') + ->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') -- 2.52.0 From 93849ab7dd789be79145567ce3b8ceb4f70c1c7a Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 21:15:28 -0500 Subject: [PATCH 28/29] fix: skip duplicate inserts, add metadata to categories Wrap all syncpush inserts in try/catch to skip duplicates instead of failing the entire batch. Add metadata column to categories. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Extension/MokoWaaS.php | 181 ++++++++++-------- .../src/Extension/ContentSync.php | 2 +- 2 files changed, 106 insertions(+), 77 deletions(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 4ca65db..1b54b90 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -2234,100 +2234,129 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface case 'articles': foreach ($items as $item) { - $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++; + 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) { - $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'] ?? '{}', - 'parent_id' => 1, - 'level' => 1, - 'lft' => 0, - 'rgt' => 0, - ]; - $db->insertObject('#__categories', $record); - $inserted++; + 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) { - $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++; + 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) { - $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++; + 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; diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php index c4a886f..997b274 100644 --- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php +++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php @@ -249,7 +249,7 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface { $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select('title, alias, description, published, language, extension, access, params') + ->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') -- 2.52.0 From 6546592b1072704ad30fff46480d3b3c929f6917 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 21:17:08 -0500 Subject: [PATCH 29/29] chore: update CHANGELOG for 02.31.00 release Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c41bed6..9e03e79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ --> # Changelog -## [02.31.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,14 +27,19 @@ - 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 to use Joomla REST API with per-task configuration -- Sync task settings: target URL, API token, content type checkboxes (articles, categories, menus, modules, images/, files/, media/) -- Sync strategy: delete-then-push for articles and menus to avoid duplicates +- 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 & Monitoring is now the first config tab +- 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 @@ -44,6 +49,8 @@ - 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 -- 2.52.0