diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 6ffb4cd4..a793e605 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.27.00 + 02.26.15 GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 4cba78d1..0a7e07b5 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: moko-platform.Automation -# VERSION: 02.27.00 +# VERSION: 02.26.15 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a02f67b..61478c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,14 +14,12 @@ INGROUP: MokoWaaS.Documentation REPO: https://github.com/mokoconsulting-tech/mokowaas PATH: ./CHANGELOG.md - VERSION: 02.27.00 + VERSION: 02.26.15 BRIEF: Version history using `Keep a Changelog` --> # Changelog ## [Unreleased] - -## [02.27.00] --- 2026-05-31 ### Added - API endpoint `POST /api/index.php/v1/mokowaas/install` — install extensions from a remote ZIP URL - Demo Mode with configurable warning banner on frontend when enabled @@ -51,3 +49,30 @@ All notable changes to the MokoWaaS plugin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [02.17.00] --- 2026-05-28 + +### Changed +- Migrated all workflow and template paths from `.github/` to `.mokogitea/` +- Template source paths updated: `templates/gitea/` to `templates/mokogitea/` +- HCL definition files removed -- Template repos are now the canonical source + +### Added +- `branch-cleanup.yml`: auto-delete merged feature branches after PR merge +- `plg_webservices_perfectpublisher`: REST API for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, feeds, and stats + +### Planned +- License/subscription check +- System email template branding (DB approach) + +### Added +- Trusted IPs: configurable repeatable rows of IP addresses, CIDR ranges, and wildcards that bypass admin session timeout +- Supports exact IPs (192.168.1.100), CIDR (10.0.0.0/24), and wildcards (192.168.1.*) +- Each entry has a label and enabled toggle for easy management +- Current IP display above trusted IPs table so admins can easily add their own IP + +### Fixed +- Trusted IP session bypass: moved from `onAfterInitialise` to `boot()` so Joomla's session lifetime is extended before the session handler validates it (was too late, Joomla expired the session first) +- updates.xml: removed stale pre-release entries pointing to non-existent dev artifacts, legacy plugin update entry that caused stable sites to attempt dev downloads +- Removed duplicate `` from inner plugin manifest — only the package-level manifest should register the update server +- Auto-cleanup of stale plugin-level update site entries on install/update (cleans `#__update_sites` and `#__update_sites_extensions`) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 57cd7447..6bb8a1a5 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.27.00 + VERSION: 02.26.15 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 e878bc8c..97c5834e 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.27.00 + VERSION: 02.26.15 PATH: /GOVERNANCE.md BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand --> diff --git a/LICENSE.md b/LICENSE.md index bbefeb1b..22bb6e94 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.27.00 + VERSION: 02.26.15 BRIEF: Project license (GPL-3.0-or-later) --> GNU GENERAL PUBLIC LICENSE diff --git a/README.md b/README.md index cc2330b8..47ca28fd 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.27.00 + VERSION: 02.26.15 PATH: /README.md BRIEF: MokoWaaS platform plugin for Joomla --> diff --git a/SECURITY.md b/SECURITY.md index e1f24387..f7094083 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.27.00 +VERSION: 02.26.15 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/docs/guides/build-guide.md b/docs/guides/build-guide.md index 248b64dc..1e9f1bca 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.27.00 + VERSION: 02.26.15 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.27.00) +# MokoWaaS Build Guide (VERSION: 02.26.15) ## 1. Purpose diff --git a/docs/guides/configuration-guide.md b/docs/guides/configuration-guide.md index 503bcae5..6db5596a 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.27.00 + VERSION: 02.26.15 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.27.00) +# MokoWaaS Configuration Guide (VERSION: 02.26.15) ## 1. Objective diff --git a/docs/guides/installation-guide.md b/docs/guides/installation-guide.md index bc5687b8..8d03411a 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.27.00 + VERSION: 02.26.15 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.27.00) +# MokoWaaS Installation Guide (VERSION: 02.26.15) ## Introduction diff --git a/docs/guides/operations-guide.md b/docs/guides/operations-guide.md index 97b84130..5d6565da 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.27.00 + VERSION: 02.26.15 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.27.00) +# MokoWaaS Operations Guide (VERSION: 02.26.15) ## Introduction diff --git a/docs/guides/rollback-and-recovery-guide.md b/docs/guides/rollback-and-recovery-guide.md index deaebaeb..9e5a416e 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.27.00 + VERSION: 02.26.15 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.27.00) +# MokoWaaS Rollback and Recovery Guide (VERSION: 02.26.15) ## Introduction diff --git a/docs/guides/testing-guide.md b/docs/guides/testing-guide.md index 55f3eebe..28d9ec8b 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.27.00 + VERSION: 02.26.15 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.27.00) +# MokoWaaS Testing Guide (VERSION: 02.26.15) ## 1. Prerequisites diff --git a/docs/guides/troubleshooting-guide.md b/docs/guides/troubleshooting-guide.md index d377cded..dd8b8469 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.27.00 + VERSION: 02.26.15 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.27.00) +# MokoWaaS Troubleshooting Guide (VERSION: 02.26.15) ## Introduction diff --git a/docs/guides/upgrade-and-versioning-guide.md b/docs/guides/upgrade-and-versioning-guide.md index f39d7864..f2f337c1 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.27.00 + VERSION: 02.26.15 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.27.00) +# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.26.15) ## Introduction diff --git a/docs/index.md b/docs/index.md index f5480db1..cf01826b 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.27.00 + VERSION: 02.26.15 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.27.00) +# MokoWaaS Documentation Index (VERSION: 02.26.15) ## Introduction diff --git a/docs/plugin-basic.md b/docs/plugin-basic.md index 93658500..765b0757 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.27.00 + VERSION: 02.26.15 BRIEF: Baseline documentation for the MokoWaaS system plugin NOTE: Foundational reference for internal and external stakeholders --> -# MokoWaaS Plugin Overview (VERSION: 02.27.00) +# MokoWaaS Plugin Overview (VERSION: 02.26.15) ## Introduction diff --git a/docs/update-server.md b/docs/update-server.md index a4c6be44..80612486 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.27.00 +VERSION: 02.26.15 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 eaa616c6..a7cd0c04 100644 --- a/src/packages/com_mokowaas/mokowaas.xml +++ b/src/packages/com_mokowaas/mokowaas.xml @@ -7,7 +7,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.27.00 + 02.26.15-dev 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 f71556e8..f23a5646 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.27.00 + * VERSION: 02.26.15 * PATH: /src/Extension/MokoWaaS.php * NOTE: Handles Joomla system events for rebranding functionality */ @@ -862,92 +862,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface ); } - // Demo Mode: Calculate next reset time and manage scheduled task - if ((int) $params->get('demo_mode_enabled', 0) === 1) - { - $schedule = $params->get('demo_reset_schedule', '0 0 * * *'); - $cron = ($schedule === 'custom') - ? $params->get('demo_reset_cron', '0 0 * * *') - : $schedule; - - $nextReset = $this->calculateNextCronRun($cron); - - if ($nextReset) - { - $params->set('demo_next_reset', $nextReset); - $changed = true; - } - - // Auto-create or update the scheduled task - $baseline = 'default'; - $this->ensureDemoResetTask($cron, $baseline); - } - else - { - if ($params->get('demo_next_reset', '') !== '') - { - $params->set('demo_next_reset', ''); - $changed = true; - } - - // Remove the scheduled task when demo mode is off - $this->removeDemoResetTask(); - } - - // Demo Mode: Take Snapshot Now - if ((int) $params->get('demo_take_snapshot_now', 0) === 1) - { - $params->set('demo_take_snapshot_now', '0'); - $changed = true; - - try - { - $this->params = $params; - $service = $this->createDemoResetService(); - $baseline = 'default'; - $result = $service->createSnapshot($baseline); - - $app->enqueueMessage( - sprintf('Demo snapshot created (%.1f MB database, media=%s).', $result['dump_size_mb'] ?? 0, ($result['has_media'] ?? false) ? 'yes' : 'no'), - 'message' - ); - } - catch (\Throwable $e) - { - $app->enqueueMessage( - 'Snapshot failed: ' . $e->getMessage(), - 'error' - ); - } - } - - // Demo Mode: Restore Baseline Now - if ((int) $params->get('demo_restore_now', 0) === 1) - { - $params->set('demo_restore_now', '0'); - $changed = true; - - try - { - $this->params = $params; - $service = $this->createDemoResetService(); - $baseline = 'default'; - $result = $service->restoreSnapshot($baseline); - - $app->enqueueMessage( - sprintf('Site restored to baseline (media=%s).', ($result['media_restored'] ?? false) ? 'yes' : 'no'), - 'message' - ); - } - catch (\Throwable $e) - { - $app->enqueueMessage( - 'Restore failed: ' . $e->getMessage(), - 'error' - ); - } - } - // Content Sync: Push Now if ((int) $params->get('sync_push_now', 0) === 1) { @@ -1080,10 +994,15 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface $this->injectAliasRobots($doc); } - // Demo mode banner (frontend only) - if ($this->app->isClient('site') && (int) $this->params->get('demo_mode_enabled', 0)) + // Demo mode banner (frontend only) — check if scheduled task is active + if ($this->app->isClient('site')) { - $this->injectDemoBanner($doc); + $demoTask = $this->getDemoTaskParams(); + + if ($demoTask && !empty($demoTask['banner_enabled'])) + { + $this->injectDemoBanner($doc, $demoTask); + } } if (!$this->app->isClient('administrator')) @@ -1113,43 +1032,23 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface * * @since 02.21.00 */ - protected function injectDemoBanner($doc) + protected function injectDemoBanner($doc, array $taskData) { - $message = htmlspecialchars($this->params->get('demo_banner_message', 'This is a demo site. All changes will be reset periodically.'), ENT_QUOTES, 'UTF-8'); - $bgColor = htmlspecialchars($this->params->get('demo_banner_color', '#d9534f'), ENT_QUOTES, 'UTF-8'); - $showCountdown = (int) $this->params->get('demo_banner_show_countdown', 0); + $message = htmlspecialchars($taskData['banner_message'] ?? 'This is a demo site. All changes will be reset periodically.', ENT_QUOTES, 'UTF-8'); + $bgColor = htmlspecialchars($taskData['banner_color'] ?? '#d9534f', ENT_QUOTES, 'UTF-8'); + $showCountdown = (int) ($taskData['show_countdown'] ?? 0); - // Use stored next-reset timestamp, or calculate on the fly from cron schedule - $nextReset = $this->params->get('demo_next_reset', ''); - $resetAtMs = 0; + // Get next_execution from the scheduled task + $resetAtMs = 0; + $nextExec = $taskData['next_execution'] ?? ''; - if ($showCountdown) + if ($showCountdown && !empty($nextExec)) { - if (!empty($nextReset)) + $ts = strtotime($nextExec . ' UTC'); + + if ($ts > time()) { - $ts = strtotime($nextReset); - - // If stored timestamp is in the past, recalculate - if ($ts > time()) - { - $resetAtMs = $ts * 1000; - } - } - - // Calculate on the fly if no valid stored timestamp - if ($resetAtMs === 0) - { - $schedule = $this->params->get('demo_reset_schedule', '0 0 * * *'); - $cron = ($schedule === 'custom') - ? $this->params->get('demo_reset_cron', '0 0 * * *') - : $schedule; - - $calculated = $this->calculateNextCronRun($cron); - - if ($calculated) - { - $resetAtMs = strtotime($calculated) * 1000; - } + $resetAtMs = $ts * 1000; } } @@ -1195,6 +1094,49 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface "); } + /** + * Get demo task params from #__scheduler_tasks if task is enabled. + * + * @return array|null Task params merged with task metadata, or null if no active task + * + * @since 02.29.00 + */ + protected function getDemoTaskParams(): ?array + { + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select([ + $db->quoteName('params'), + $db->quoteName('state'), + $db->quoteName('next_execution'), + $db->quoteName('last_execution'), + ]) + ->from($db->quoteName('#__scheduler_tasks')) + ->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset')) + ->where($db->quoteName('state') . ' = 1'); + + $db->setQuery($query); + $task = $db->loadAssoc(); + + if (!$task) + { + return null; + } + + $params = json_decode($task['params'] ?? '{}', true) ?: []; + $params['next_execution'] = $task['next_execution']; + $params['last_execution'] = $task['last_execution']; + + return $params; + } + catch (\Throwable $e) + { + return null; + } + } + /** * Hide MokoWaaS plugin and package from the extensions list via JS. * diff --git a/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php b/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php index 9cdeb0d0..17dbed43 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.27.00 + * VERSION: 02.26.15 * 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 6bd9bdb5..ca9ec10d 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.27.00 + * VERSION: 02.26.15 * 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 f8ab64d7..492f9af2 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.27.00 + * VERSION: 02.26.15 * 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 new file mode 100644 index 00000000..0e979880 --- /dev/null +++ b/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php @@ -0,0 +1,237 @@ +getQuery(true) + ->select('*') + ->from($db->quoteName('#__scheduler_tasks')) + ->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset')); + + $db->setQuery($query); + $task = $db->loadAssoc(); + } + catch (\Throwable $e) + { + $task = null; + } + + $newTaskLink = Route::_('index.php?option=com_scheduler&task=task.add'); + + if (!$task) + { + return '
' + . 'No demo reset task configured. ' + . 'Create a Scheduled Task ' + . 'and select MokoWaaS Demo Reset to enable demo mode.
'; + } + + $taskId = (int) $task['id']; + $state = (int) $task['state']; + $siteTimezone = Factory::getApplication()->get('offset', 'UTC'); + + // Parse schedule from execution_rules + $rules = json_decode($task['execution_rules'] ?? '{}', true); + $ruleType = $rules['rule-type'] ?? ''; + + switch ($ruleType) + { + case 'cron-expression': + $schedule = $rules['cron-expression'] ?? ''; + $friendlySchedule = $this->friendlySchedule($schedule); + break; + + case 'interval-minutes': + $mins = (int) ($rules['interval-minutes'] ?? 0); + + if ($mins >= 1440 && $mins % 1440 === 0) + { + $days = $mins / 1440; + $schedule = 'Every ' . $days . ' day' . ($days > 1 ? 's' : ''); + } + elseif ($mins >= 60 && $mins % 60 === 0) + { + $hours = $mins / 60; + $schedule = 'Every ' . $hours . ' hour' . ($hours > 1 ? 's' : ''); + } + else + { + $schedule = 'Every ' . $mins . ' minute' . ($mins !== 1 ? 's' : ''); + } + + $friendlySchedule = $schedule; + break; + + case 'interval-hours': + $hours = (int) ($rules['interval-hours'] ?? 0); + $schedule = 'Every ' . $hours . ' hour' . ($hours !== 1 ? 's' : ''); + $friendlySchedule = $schedule; + break; + + case 'interval-days': + $days = (int) ($rules['interval-days'] ?? 0); + $schedule = 'Every ' . $days . ' day' . ($days !== 1 ? 's' : ''); + $friendlySchedule = $schedule; + break; + + default: + $schedule = $ruleType ?: 'Not set'; + $friendlySchedule = 'Custom'; + } + + // Next execution + $nextExec = $task['next_execution'] ?? ''; + $nextFormatted = 'Not scheduled'; + $nextBadge = ''; + + if (!empty($nextExec) && $nextExec !== '0000-00-00 00:00:00') + { + try + { + $dt = new \DateTime($nextExec, new \DateTimeZone('UTC')); + $dt->setTimezone(new \DateTimeZone($siteTimezone)); + $nextFormatted = $dt->format('M j, Y g:i A T'); + } + catch (\Throwable $e) + { + $nextFormatted = $nextExec; + } + + $diff = strtotime($nextExec . ' UTC') - time(); + + if ($diff <= 0) + { + $nextBadge = 'DUE'; + } + elseif ($diff < 3600) + { + $nextBadge = 'in ' . (int) ceil($diff / 60) . ' min'; + } + elseif ($diff < 86400) + { + $nextBadge = 'in ' . round($diff / 3600, 1) . 'h'; + } + else + { + $nextBadge = 'in ' . round($diff / 86400, 1) . 'd'; + } + } + + // Last execution + $lastExec = $task['last_execution'] ?? ''; + $lastFormatted = 'Never'; + + if (!empty($lastExec) && $lastExec !== '0000-00-00 00:00:00') + { + try + { + $dt = new \DateTime($lastExec, new \DateTimeZone('UTC')); + $dt->setTimezone(new \DateTimeZone($siteTimezone)); + $lastFormatted = $dt->format('M j, Y g:i A T'); + } + catch (\Throwable $e) + { + $lastFormatted = $lastExec; + } + } + + // State badge + $stateBadge = $state === 1 + ? 'Enabled' + : 'Disabled'; + + // Link to edit the task + $editLink = Route::_('index.php?option=com_scheduler&task=task.edit&id=' . $taskId); + + // Task params + $taskParams = json_decode($task['params'] ?? '{}', true) ?: []; + $bannerOn = !empty($taskParams['banner_enabled']) && (int) $taskParams['banner_enabled'] === 1; + $mediaOn = !empty($taskParams['include_media']) && (int) $taskParams['include_media'] === 1; + $countdownOn = !empty($taskParams['show_countdown']) && (int) $taskParams['show_countdown'] === 1; + + // Check if snapshot exists + $snapshotExists = is_dir(JPATH_ROOT . '/mokowaas-snapshots/default'); + + // Build info card + return '
' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
Status' . $stateBadge . '
Schedule' . htmlspecialchars($friendlySchedule) . '
Next Reset' . htmlspecialchars($nextFormatted) . ' ' . $nextBadge . '
Last Reset' . htmlspecialchars($lastFormatted) . '
Runs' . (int) ($task['times_executed'] ?? 0) . ' executed, ' . (int) ($task['times_failed'] ?? 0) . ' failed
Baseline' . ($snapshotExists ? 'Saved' : 'Not taken yet') . '
Banner' . ($bannerOn ? 'On' : 'Off') . ($countdownOn ? ' + countdown' : '') . '
Images' . ($mediaOn ? 'Included' : 'Excluded') . '
' + . '' + . ' Manage Scheduled Task' + . '
'; + } + + protected function getLabel() + { + return ''; + } + + /** + * Convert a cron expression to a human-readable string. + * + * @param string $cron Cron expression + * + * @return string + */ + private function friendlySchedule(string $cron): string + { + $map = [ + '* * * * *' => 'Every minute', + '*/5 * * * *' => 'Every 5 minutes', + '*/15 * * * *' => 'Every 15 minutes', + '*/30 * * * *' => 'Every 30 minutes', + '0 */1 * * *' => 'Every hour', + '0 */4 * * *' => 'Every 4 hours', + '0 */6 * * *' => 'Every 6 hours', + '0 */12 * * *' => 'Every 12 hours', + '0 0 * * *' => 'Daily at midnight', + '0 6 * * *' => 'Daily at 6:00 AM', + '0 0 * * 0' => 'Weekly (Sunday)', + '0 0 1 * *' => 'Monthly (1st)', + ]; + + return $map[$cron] ?? 'Custom'; + } +} diff --git a/src/packages/plg_system_mokowaas/Field/NextResetField.php b/src/packages/plg_system_mokowaas/Field/NextResetField.php index 2eef389d..1f3cb28d 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.27.00 + * VERSION: 02.26.15 * 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 29b1a592..d2fbc3b7 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.27.00 + * VERSION: 02.26.15 * 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 45f6547f..b99992fc 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.27.00 + * VERSION: 02.26.15 * 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 6e7e93cb..89f83399 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.27.00 + * VERSION: 02.26.15 * 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 ea003c56..db1e1631 100644 --- a/src/packages/plg_system_mokowaas/Service/DemoResetService.php +++ b/src/packages/plg_system_mokowaas/Service/DemoResetService.php @@ -10,8 +10,8 @@ * INGROUP: MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php - * VERSION: 02.27.00 - * BRIEF: Full database snapshot/restore service for demo site reset + * VERSION: 02.26.15 + * BRIEF: Content-only snapshot/restore for demo site reset */ namespace Moko\Plugin\System\MokoWaaS\Service; @@ -22,47 +22,70 @@ use Joomla\CMS\Factory; use Joomla\CMS\Log\Log; /** - * Demo Reset Service — full database snapshot and restore. + * Demo Reset Service — content-only snapshot and restore. * - * Takes a complete mysqldump of the database and restores it wholesale. - * This avoids the complexity of selective table management and ensures - * the site returns to an exact known state. + * Only touches safe content tables (articles, categories, menus, modules, + * users, tags, fields). Never touches extensions, assets, sessions, + * schemas, update sites, or any system tables. * - * @since 02.28.00 + * @since 02.30.00 */ class DemoResetService { - /** - * Maximum snapshot name length. - * - * @var int - * @since 02.21.00 - */ private const MAX_NAME_LENGTH = 64; + private const BATCH_SIZE = 500; /** - * Root directory for all snapshots. - * - * @var string - * @since 02.21.00 + * Safe content tables to snapshot/restore. + * These can be wiped and restored without breaking the Joomla installation. + */ + private const SAFE_TABLES = [ + // Content + '#__content', + '#__content_frontpage', + '#__categories', + '#__fields', + '#__fields_values', + '#__fields_groups', + '#__tags', + '#__contentitem_tag_map', + '#__ucm_content', + + // Menus + '#__menu', + '#__menu_types', + + // Modules + '#__modules', + '#__modules_menu', + + // Users + '#__users', + '#__user_usergroup_map', + '#__user_profiles', + + // Contact + '#__contact_details', + + // Banners + '#__banners', + '#__banner_clients', + '#__banner_tracks', + ]; + + /** + * @var string */ private string $snapshotDir; /** - * Whether to include /images/ in snapshots. - * - * @var bool - * @since 02.21.00 + * @var bool */ private bool $includeMedia; /** - * Constructor. - * - * @param bool $includeMedia Include /images/ directory in snapshot - * @param string $baseDir Override snapshot root (for testing) - * - * @since 02.28.00 + * @param bool $includeMedia Include /images/ directory + * @param string $baseDir Override snapshot root */ public function __construct(bool $includeMedia = true, string $baseDir = '') { @@ -73,9 +96,7 @@ class DemoResetService /** * List all available snapshots. * - * @return array Array of manifest data keyed by snapshot name - * - * @since 02.21.00 + * @return array */ public function listSnapshots(): array { @@ -86,11 +107,9 @@ class DemoResetService return $snapshots; } - $dirs = glob($this->snapshotDir . '/*/manifest.json'); - - foreach ($dirs as $manifestPath) + foreach (glob($this->snapshotDir . '/*/manifest.json') as $path) { - $data = json_decode(file_get_contents($manifestPath), true); + $data = json_decode(file_get_contents($path), true); if ($data && isset($data['name'])) { @@ -102,16 +121,11 @@ class DemoResetService } /** - * Create a full database snapshot. + * Create a content snapshot. * * @param string $name Snapshot name * - * @return array Result payload - * - * @throws \InvalidArgumentException On invalid name - * @throws \RuntimeException On failure - * - * @since 02.28.00 + * @return array Result */ public function createSnapshot(string $name): array { @@ -125,212 +139,134 @@ class DemoResetService $this->removeDirectory($path); } - if (!mkdir($path, 0755, true)) + mkdir($path, 0755, true); + + $db = Factory::getDbo(); + $prefix = $db->getPrefix(); + $allTables = $db->getTableList(); + $dumped = 0; + + foreach (self::SAFE_TABLES as $logicalName) { - throw new \RuntimeException('Failed to create snapshot directory: ' . $path); + $realName = str_replace('#__', $prefix, $logicalName); + + if (!in_array($realName, $allTables)) + { + continue; + } + + $this->dumpTable($logicalName, $realName, $path, $db); + $dumped++; } - // Full database dump - $config = Factory::getConfig(); - $host = $config->get('host', 'localhost'); - $dbName = $config->get('db'); - $user = $config->get('user'); - $pass = $config->get('password'); - - $dumpFile = $path . '/database.sql'; - - // Use mysqldump for a complete, reliable dump - $cmd = sprintf( - 'mysqldump --host=%s --user=%s --password=%s --single-transaction --routines --triggers --add-drop-table %s > %s 2>&1', - escapeshellarg($host), - escapeshellarg($user), - escapeshellarg($pass), - escapeshellarg($dbName), - escapeshellarg($dumpFile) - ); - - exec($cmd, $output, $exitCode); - - if ($exitCode !== 0 || !file_exists($dumpFile) || filesize($dumpFile) === 0) - { - // Fallback: PHP-based dump if mysqldump is not available - $this->phpDatabaseDump($dumpFile); - } - - $dumpSizeMb = round(filesize($dumpFile) / 1048576, 1); - - // Media snapshot + // Media $hasMedia = false; - if ($this->includeMedia) + if ($this->includeMedia && is_dir(JPATH_ROOT . '/images')) { - $imagesDir = JPATH_ROOT . '/images'; - - if (is_dir($imagesDir)) - { - $hasMedia = $this->snapshotDirectory($imagesDir, $path . '/media.zip'); - } + $hasMedia = $this->zipDirectory(JPATH_ROOT . '/images', $path . '/media.zip'); } - // Write manifest $manifest = [ 'name' => $name, 'created_at' => gmdate('Y-m-d\TH:i:s\Z'), - 'type' => 'full-database', - 'dump_size_mb' => $dumpSizeMb, + 'type' => 'content-only', + 'tables' => $dumped, 'has_media' => $hasMedia, 'joomla_version' => JVERSION, ]; - file_put_contents( - $path . '/manifest.json', - json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) - ); + file_put_contents($path . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - Log::add( - sprintf('Demo snapshot "%s" created (full DB %.1f MB, media=%s)', $name, $dumpSizeMb, $hasMedia ? 'yes' : 'no'), - Log::INFO, - 'mokowaas' - ); + Log::add(sprintf('Demo snapshot "%s" created (%d tables, media=%s)', $name, $dumped, $hasMedia ? 'yes' : 'no'), Log::INFO, 'mokowaas'); return [ - 'status' => 'ok', - 'message' => 'Snapshot created', - 'name' => $name, - 'dump_size_mb' => $dumpSizeMb, - 'has_media' => $hasMedia, + 'status' => 'ok', + 'message' => 'Snapshot created', + 'name' => $name, + 'tables' => $dumped, + 'has_media' => $hasMedia, ]; } /** - * Restore the site to a named snapshot. + * Restore from a snapshot. * - * @param string $name Snapshot name to restore + * @param string $name Snapshot name * - * @return array Result payload - * - * @throws \InvalidArgumentException On invalid name - * @throws \RuntimeException On missing snapshot or restore failure - * - * @since 02.28.00 + * @return array Result */ public function restoreSnapshot(string $name): array { $this->validateSnapshotName($name); - $path = $this->getSnapshotPath($name); - $manifestFile = $path . '/manifest.json'; + $path = $this->getSnapshotPath($name); + $manifest = $path . '/manifest.json'; - if (!file_exists($manifestFile)) + if (!file_exists($manifest)) { throw new \RuntimeException('Snapshot not found: ' . $name); } - $manifest = json_decode(file_get_contents($manifestFile), true); + $manifestData = json_decode(file_get_contents($manifest), true); - if (!$manifest) + // Clear cache + try { Factory::getCache('')->clean(''); } catch (\Throwable $e) {} + + $db = Factory::getDbo(); + $prefix = $db->getPrefix(); + $restored = 0; + $sqlFiles = glob($path . '/*.sql'); + + foreach ($sqlFiles as $sqlFile) { - throw new \RuntimeException('Invalid manifest for snapshot: ' . $name); - } - - $dumpFile = $path . '/database.sql'; - - if (!file_exists($dumpFile)) - { - throw new \RuntimeException('Database dump not found in snapshot: ' . $name); - } - - // Clear Joomla cache - try - { - $cache = Factory::getCache(''); - $cache->clean(''); - } - catch (\Throwable $e) - { - // Best effort - } - - // Restore database - $config = Factory::getConfig(); - $host = $config->get('host', 'localhost'); - $dbName = $config->get('db'); - $user = $config->get('user'); - $pass = $config->get('password'); - - // Try mysql CLI first (fastest, handles large dumps) - $cmd = sprintf( - 'mysql --host=%s --user=%s --password=%s %s < %s 2>&1', - escapeshellarg($host), - escapeshellarg($user), - escapeshellarg($pass), - escapeshellarg($dbName), - escapeshellarg($dumpFile) - ); - - exec($cmd, $output, $exitCode); - - if ($exitCode !== 0) - { - // Fallback: PHP-based restore - $this->phpDatabaseRestore($dumpFile); + try + { + $this->restoreTable($sqlFile, $db, $prefix); + $restored++; + } + catch (\Throwable $e) + { + Log::add('Demo reset: failed to restore ' . basename($sqlFile) . ': ' . $e->getMessage(), Log::ERROR, 'mokowaas'); + } } // Restore /images/ $mediaRestored = false; - if ($manifest['has_media'] ?? false) + if (($manifestData['has_media'] ?? false) && file_exists($path . '/media.zip')) { - $zipPath = $path . '/media.zip'; - $imagesDir = JPATH_ROOT . '/images'; + $this->clearDirectory(JPATH_ROOT . '/images'); + $zip = new \ZipArchive(); - if (file_exists($zipPath)) + if ($zip->open($path . '/media.zip') === true) { - $this->clearDirectory($imagesDir); - - $zip = new \ZipArchive(); - - if ($zip->open($zipPath) === true) - { - $zip->extractTo($imagesDir); - $zip->close(); - $mediaRestored = true; - } + $zip->extractTo(JPATH_ROOT . '/images'); + $zip->close(); + $mediaRestored = true; } } - // After full DB restore, re-ensure the task plugin and scheduled task - // exist — the restore overwrote #__extensions and #__scheduler_tasks - $this->reRegisterAfterRestore(); + // Rebuild assets table to fix ACL after content restore + $this->rebuildAssets(); - Log::add( - sprintf('Demo site reset to baseline "%s" (full DB, media=%s)', $name, $mediaRestored ? 'yes' : 'no'), - Log::WARNING, - 'mokowaas' - ); + Log::add(sprintf('Demo site reset (%d tables, media=%s)', $restored, $mediaRestored ? 'yes' : 'no'), Log::WARNING, 'mokowaas'); return [ - 'status' => 'ok', - 'message' => 'Site restored to baseline: ' . $name, - 'baseline' => $name, - 'type' => 'full-database', - 'media_restored' => $mediaRestored, + 'status' => 'ok', + 'message' => 'Site content restored', + 'baseline' => $name, + 'restored_tables' => $restored, + 'media_restored' => $mediaRestored, ]; } /** - * Delete a named snapshot. - * - * @param string $name Snapshot name - * - * @return bool True on success - * - * @since 02.21.00 + * Delete a snapshot. */ public function deleteSnapshot(string $name): bool { $this->validateSnapshotName($name); - $path = $this->getSnapshotPath($name); if (!is_dir($path)) @@ -344,164 +280,241 @@ class DemoResetService } /** - * PHP fallback: dump entire database when mysqldump is unavailable. + * Rebuild the assets table after content restore. * - * @param string $dumpFile Output file path + * Deletes content-related asset entries (which now have stale IDs) + * and rebuilds them using Joomla's Table classes. Extension and + * component-level assets are left untouched. * * @return void - * - * @since 02.28.00 */ - private function phpDatabaseDump(string $dumpFile): void + private function rebuildAssets(): void { - $db = Factory::getDbo(); - $tables = $db->getTableList(); - $fp = fopen($dumpFile, 'w'); - - if ($fp === false) + try { - throw new \RuntimeException('Cannot write dump file'); + $db = Factory::getDbo(); + + // Delete content-level assets (articles, categories, modules, etc.) + // Keep component-level and root assets intact + $contentAssetPrefixes = [ + 'com_content.article.%', + 'com_content.category.%', + 'com_contact.contact.%', + 'com_banners.banner.%', + 'com_banners.category.%', + 'com_modules.module.%', + 'com_menus.menu.%', + 'com_users.user.%', + ]; + + foreach ($contentAssetPrefixes as $prefix) + { + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' LIKE ' . $db->quote($prefix)) + ); + $db->execute(); + } + + // Rebuild category tree (also fixes category assets) + $catTable = \Joomla\CMS\Table\Table::getInstance('Category'); + + if ($catTable) + { + $catTable->rebuild(); + } + + // Rebuild menu tree + $menuTable = \Joomla\CMS\Table\Table::getInstance('Menu'); + + if ($menuTable) + { + $menuTable->rebuild(); + } + + // Rebuild asset tree + $assetTable = \Joomla\CMS\Table\Table::getInstance('Asset'); + + if ($assetTable) + { + $assetTable->rebuild(); + } + + // Re-create assets for content items that lost theirs + $this->fixContentAssets($db); + } + catch (\Throwable $e) + { + Log::add('Asset rebuild warning: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); + } + } + + /** + * Re-create missing asset entries for content items. + * + * After deleting stale assets and restoring content, some items + * may reference asset_id=0. This creates new asset rows for them. + * + * @param \Joomla\Database\DatabaseInterface $db + * + * @return void + */ + private function fixContentAssets($db): void + { + // Fix articles with missing assets + $query = $db->getQuery(true) + ->select([$db->quoteName('id'), $db->quoteName('title'), $db->quoteName('alias')]) + ->from($db->quoteName('#__content')) + ->where($db->quoteName('asset_id') . ' = 0'); + + $db->setQuery($query); + $articles = $db->loadAssocList() ?: []; + + // Find the com_content component asset as parent + $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = ' . $db->quote('com_content')) + ); + $contentAssetId = (int) $db->loadResult(); + + foreach ($articles as $article) + { + $assetName = 'com_content.article.' . (int) $article['id']; + + $asset = (object) [ + 'parent_id' => $contentAssetId ?: 1, + 'lft' => 0, + 'rgt' => 0, + 'level' => 0, + 'name' => $assetName, + 'title' => $article['title'], + 'rules' => '{}', + ]; + + $db->insertObject('#__assets', $asset, 'id'); + + // Update content row with new asset_id + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__content')) + ->set($db->quoteName('asset_id') . ' = ' . (int) $asset->id) + ->where($db->quoteName('id') . ' = ' . (int) $article['id']) + ); + $db->execute(); } - fwrite($fp, "SET FOREIGN_KEY_CHECKS=0;\n\n"); - - foreach ($tables as $table) + // Rebuild asset tree again after inserts + try { - // CREATE TABLE statement - $db->setQuery('SHOW CREATE TABLE ' . $db->quoteName($table)); - $create = $db->loadAssoc(); - $createSql = $create['Create Table'] ?? $create['Create View'] ?? ''; + $assetTable = \Joomla\CMS\Table\Table::getInstance('Asset'); - if (empty($createSql)) + if ($assetTable) { - continue; + $assetTable->rebuild(); + } + } + catch (\Throwable $e) + { + // Best effort + } + } + + // ------------------------------------------------------------------ + // Private helpers + // ------------------------------------------------------------------ + + private function dumpTable(string $logicalName, string $realName, string $dir, $db): void + { + $safeFileName = str_replace('#__', 'jml__', $logicalName); + $fp = fopen($dir . '/' . $safeFileName . '.sql', 'w'); + + $columns = $db->getTableColumns($realName, false); + $colNames = array_keys($columns); + $quotedCols = array_map([$db, 'quoteName'], $colNames); + $colList = implode(', ', $quotedCols); + $offset = 0; + + while (true) + { + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName($realName)) + ->setLimit(self::BATCH_SIZE, $offset); + + $db->setQuery($query); + $rows = $db->loadAssocList(); + + if (empty($rows)) + { + break; } - fwrite($fp, "DROP TABLE IF EXISTS " . $db->quoteName($table) . ";\n"); - fwrite($fp, $createSql . ";\n\n"); + $values = []; - // Skip views for data dump - if (isset($create['Create View'])) + foreach ($rows as $row) { - continue; + $vals = []; + + foreach ($colNames as $col) + { + $vals[] = $row[$col] === null ? 'NULL' : $db->quote($row[$col]); + } + + $values[] = '(' . implode(', ', $vals) . ')'; } - // Dump data in batches - $offset = 0; + fwrite($fp, 'INSERT INTO ' . $db->quoteName($realName) . ' (' . $colList . ') VALUES ' . "\n" . implode(",\n", $values) . ";\n\n"); - while (true) + $offset += self::BATCH_SIZE; + + if (count($rows) < self::BATCH_SIZE) { - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName($table)) - ->setLimit(500, $offset); - - $db->setQuery($query); - $rows = $db->loadAssocList(); - - if (empty($rows)) - { - break; - } - - // Get column names - $columns = array_keys($rows[0]); - $quotedCols = array_map([$db, 'quoteName'], $columns); - $colList = implode(', ', $quotedCols); - - $values = []; - - foreach ($rows as $row) - { - $vals = []; - - foreach ($columns as $col) - { - $val = $row[$col]; - $vals[] = $val === null ? 'NULL' : $db->quote($val); - } - - $values[] = '(' . implode(', ', $vals) . ')'; - } - - fwrite($fp, 'INSERT INTO ' . $db->quoteName($table) - . ' (' . $colList . ') VALUES ' . "\n" - . implode(",\n", $values) . ";\n\n"); - - $offset += 500; - - if (count($rows) < 500) - { - break; - } + break; } } - fwrite($fp, "\nSET FOREIGN_KEY_CHECKS=1;\n"); fclose($fp); } - /** - * PHP fallback: restore database from SQL dump when mysql CLI is unavailable. - * - * @param string $dumpFile SQL dump file path - * - * @return void - * - * @since 02.28.00 - */ - private function phpDatabaseRestore(string $dumpFile): void + private function restoreTable(string $sqlFile, $db, string $prefix): void { - $db = Factory::getDbo(); - $sql = file_get_contents($dumpFile); + $baseName = basename($sqlFile, '.sql'); + $realTable = str_replace('jml__', $prefix, $baseName); - if (empty($sql)) + $db->setQuery('TRUNCATE TABLE ' . $db->quoteName($realTable)); + $db->execute(); + + $sql = file_get_contents($sqlFile); + + if (empty(trim($sql))) { - throw new \RuntimeException('Empty database dump file'); + return; } - // Split by semicolons followed by newlines (avoids splitting within values) - $statements = preg_split('/;\s*\n/', $sql); + $statements = array_filter( + array_map('trim', explode(";\n", $sql)), + function ($s) { return !empty($s) && $s !== ';'; } + ); foreach ($statements as $statement) { - $statement = trim($statement); + $statement = rtrim($statement, ';'); - if (empty($statement) || $statement === ';') + if (empty($statement)) { continue; } - try - { - $db->setQuery($statement); - $db->execute(); - } - catch (\Throwable $e) - { - // Log but continue — partial restore is better than aborting - Log::add('DB restore warning: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); - } + $db->setQuery($statement); + $db->execute(); } } - /** - * Create a ZIP archive of a directory. - * - * @param string $sourceDir Full path to directory - * @param string $zipPath Output ZIP path - * - * @return bool - * - * @since 02.25.00 - */ - private function snapshotDirectory(string $sourceDir, string $zipPath): bool + private function zipDirectory(string $sourceDir, string $zipPath): bool { - if (!is_dir($sourceDir)) - { - return false; - } - $zip = new \ZipArchive(); if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) @@ -516,17 +529,8 @@ class DemoResetService foreach ($iterator as $item) { - $relativePath = substr($item->getPathname(), strlen($sourceDir) + 1); - $relativePath = str_replace('\\', '/', $relativePath); - - if ($item->isDir()) - { - $zip->addEmptyDir($relativePath); - } - else - { - $zip->addFile($item->getPathname(), $relativePath); - } + $rel = str_replace('\\', '/', substr($item->getPathname(), strlen($sourceDir) + 1)); + $item->isDir() ? $zip->addEmptyDir($rel) : $zip->addFile($item->getPathname(), $rel); } $zip->close(); @@ -534,117 +538,16 @@ class DemoResetService return true; } - /** - * Ensure the snapshot root directory exists with .htaccess protection. - * - * @return void - * - * @since 02.21.00 - */ - private function reRegisterAfterRestore(): void - { - try - { - $db = Factory::getDbo(); - $prefix = $db->getPrefix(); - - // Re-enable all MokoWaaS extensions (may have been overwritten by old snapshot) - $elements = [ - $db->quote('pkg_mokowaas'), - $db->quote('mokowaas'), - $db->quote('com_mokowaas'), - $db->quote('mokowaasdemo'), - $db->quote('perfectpublisher'), - ]; - - $db->setQuery( - $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->set($db->quoteName('protected') . ' = 1') - ->set($db->quoteName('locked') . ' = 0') - ->where($db->quoteName('element') . ' IN (' . implode(',', $elements) . ')') - ); - $db->execute(); - - // If the task plugin isn't registered, add it - $db->setQuery( - $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaasdemo')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ); - - if ((int) $db->loadResult() === 0) - { - $obj = (object) [ - 'name' => 'Task - MokoWaaS Demo Reset', - 'type' => 'plugin', - 'element' => 'mokowaasdemo', - 'folder' => 'task', - 'client_id' => 0, - 'enabled' => 1, - 'protected' => 1, - 'locked' => 0, - 'access' => 1, - 'params' => '{}', - ]; - $db->insertObject('#__extensions', $obj); - } - - // Ensure the scheduled task exists and is enabled - $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__scheduler_tasks')) - ->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset')) - ); - $taskId = (int) $db->loadResult(); - - if ($taskId > 0) - { - // Re-enable and update next_execution to now so it stays active - $db->setQuery( - $db->getQuery(true) - ->update($db->quoteName('#__scheduler_tasks')) - ->set($db->quoteName('state') . ' = 1') - ->where($db->quoteName('id') . ' = ' . $taskId) - ); - $db->execute(); - } - - // Re-enable the update server - $db->setQuery( - $db->getQuery(true) - ->update($db->quoteName('#__update_sites')) - ->set($db->quoteName('enabled') . ' = 1') - ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoWaaS%') - . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoWaaS%') . ')') - ); - $db->execute(); - } - catch (\Throwable $e) - { - // Best effort — don't let registration failure break the restore - } - } - private function ensureSnapshotDir(): void { if (!is_dir($this->snapshotDir)) { - if (!mkdir($this->snapshotDir, 0755, true)) - { - throw new \RuntimeException('Cannot create snapshot directory'); - } + mkdir($this->snapshotDir, 0755, true); } - $htaccess = $this->snapshotDir . '/.htaccess'; - - if (!file_exists($htaccess)) + if (!file_exists($this->snapshotDir . '/.htaccess')) { - file_put_contents($htaccess, "Deny from all\n"); + file_put_contents($this->snapshotDir . '/.htaccess', "Deny from all\n"); } } @@ -655,14 +558,9 @@ class DemoResetService private function validateSnapshotName(string $name): void { - if ($name === '' || strlen($name) > self::MAX_NAME_LENGTH) + if ($name === '' || strlen($name) > self::MAX_NAME_LENGTH || !preg_match('/^[a-zA-Z0-9_-]+$/', $name)) { - throw new \InvalidArgumentException('Snapshot name must be 1-' . self::MAX_NAME_LENGTH . ' characters'); - } - - if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) - { - throw new \InvalidArgumentException('Snapshot name must contain only letters, numbers, hyphens, and underscores'); + throw new \InvalidArgumentException('Invalid snapshot name'); } } @@ -683,10 +581,7 @@ class DemoResetService private function clearDirectory(string $dir): void { - if (!is_dir($dir)) - { - return; - } + if (!is_dir($dir)) return; $items = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), diff --git a/src/packages/plg_system_mokowaas/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml index 900de196..2ffdf905 100644 --- a/src/packages/plg_system_mokowaas/mokowaas.xml +++ b/src/packages/plg_system_mokowaas/mokowaas.xml @@ -30,7 +30,7 @@ GNU General Public License version 3 or later; see LICENSE.md hello@mokoconsulting.tech https://mokoconsulting.tech - 02.27.00 + 02.26.15-dev 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 @@ -270,75 +270,9 @@ description="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC" addfieldprefix="Moko\Plugin\System\MokoWaaS\Field" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
+
+ + + + + + + + + + + + + + + + + +
diff --git a/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml b/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml index a82b7640..3612d707 100644 --- a/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml +++ b/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml @@ -12,7 +12,7 @@ GNU General Public License version 3 or later; see LICENSE hello@mokoconsulting.tech https://mokoconsulting.tech - 02.27.00 + 02.26.15-dev PLG_TASK_MOKOWAASDEMO_DESC Moko\Plugin\Task\MokoWaaSDemo diff --git a/src/packages/plg_task_mokowaasdemo/src/Extension/DemoReset.php b/src/packages/plg_task_mokowaasdemo/src/Extension/DemoReset.php index 820cfa07..d157d1fc 100644 --- a/src/packages/plg_task_mokowaasdemo/src/Extension/DemoReset.php +++ b/src/packages/plg_task_mokowaasdemo/src/Extension/DemoReset.php @@ -10,33 +10,26 @@ namespace Moko\Plugin\Task\MokoWaaSDemo\Extension; defined('_JEXEC') or die; +use Joomla\CMS\Factory; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\CMS\Plugin\PluginHelper; use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent; use Joomla\Component\Scheduler\Administrator\Task\Status; use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait; -use Joomla\Event\DispatcherInterface; use Joomla\Event\SubscriberInterface; -use Joomla\Registry\Registry; /** * MokoWaaS Demo Reset — Joomla Scheduled Task Plugin. * - * Registers a task routine that restores the site to a named baseline - * snapshot on a configurable schedule (e.g. every 24 hours). + * All demo settings live here in the task form: snapshot, media, + * banner message, color, countdown. The system plugin reads these + * settings from the task to render the banner. * - * @since 02.21.00 + * @since 02.29.00 */ final class DemoReset extends CMSPlugin implements SubscriberInterface { use TaskPluginTrait; - /** - * Task routine map. - * - * @var array - * @since 02.21.00 - */ protected const TASKS_MAP = [ 'mokowaas.demo.reset' => [ 'langConstPrefix' => 'PLG_TASK_MOKOWAASDEMO_RESET', @@ -45,46 +38,96 @@ final class DemoReset extends CMSPlugin implements SubscriberInterface ], ]; - /** - * @return array - * - * @since 02.21.00 - */ public static function getSubscribedEvents(): array { return [ 'onTaskOptionsList' => 'advertiseRoutines', 'onExecuteTask' => 'standardRoutineHandler', 'onContentPrepareForm' => 'enhanceTaskItemForm', + 'onContentAfterSave' => 'onContentAfterSave', ]; } /** - * Reset the demo site to a baseline snapshot. + * Handle snapshot-on-save when the task is saved with take_snapshot_on_save=1. + * + * @param string $context The save context + * @param object $table The table object + * @param bool $isNew Whether this is a new record + * + * @return void + * + * @since 02.29.00 + */ + public function onContentAfterSave($event): void + { + // Joomla 6 passes a single event object; Joomla 5 passes individual args + if (is_object($event) && method_exists($event, 'getArgument')) + { + $context = $event->getArgument('context', $event->getArgument(0, '')); + $table = $event->getArgument('subject', $event->getArgument(1, null)); + } + else + { + $context = $event; + $table = func_get_arg(1); + } + + if ($context !== 'com_scheduler.task') + { + return; + } + + if (!is_object($table) || ($table->type ?? '') !== 'mokowaas.demo.reset') + { + return; + } + + $params = json_decode($table->params ?? '{}', true) ?: []; + + if (!empty($params['take_snapshot_on_save']) && (int) $params['take_snapshot_on_save'] === 1) + { + $serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/DemoResetService.php'; + + if (file_exists($serviceFile)) + { + require_once $serviceFile; + + $media = !empty($params['include_media']) && (int) $params['include_media'] === 1; + $service = new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService($media); + + try + { + $result = $service->createSnapshot('default'); + Factory::getApplication()->enqueueMessage( + sprintf('Demo snapshot created (%.1f MB database, media=%s).', $result['dump_size_mb'] ?? 0, ($result['has_media'] ?? false) ? 'yes' : 'no'), + 'message' + ); + } + catch (\Throwable $e) + { + Factory::getApplication()->enqueueMessage('Snapshot failed: ' . $e->getMessage(), 'error'); + } + } + + // Reset the flag + $this->clearSnapshotFlag(); + } + } + + /** + * Reset the demo site to the baseline snapshot. * * @param ExecuteTaskEvent $event The task event * * @return int Status::OK or Status::KNOCKOUT * - * @since 02.21.00 + * @since 02.29.00 */ private function resetDemoSite(ExecuteTaskEvent $event): int { $params = $event->getArgument('params'); - // Load system plugin params for table list and media setting - $sysPlugin = PluginHelper::getPlugin('system', 'mokowaas'); - - if (!$sysPlugin) - { - $this->logTask('MokoWaaS system plugin not enabled — cannot reset'); - - return Status::KNOCKOUT; - } - - $sysParams = new Registry($sysPlugin->params); - - // Load the service $serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/DemoResetService.php'; if (!file_exists($serviceFile)) @@ -96,8 +139,7 @@ final class DemoReset extends CMSPlugin implements SubscriberInterface require_once $serviceFile; - $media = (bool) $sysParams->get('demo_snapshot_include_media', 1); - + $media = !empty($params->include_media) && (int) $params->include_media === 1; $service = new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService($media); try @@ -114,4 +156,69 @@ final class DemoReset extends CMSPlugin implements SubscriberInterface return Status::KNOCKOUT; } } + + /** + * Take a baseline snapshot. + * + * @param object $params Task params + * + * @return void + * + * @since 02.29.00 + */ + private function takeSnapshot(object $params): void + { + $serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/DemoResetService.php'; + + if (!file_exists($serviceFile)) + { + return; + } + + require_once $serviceFile; + + $media = !empty($params->include_media) && (int) $params->include_media === 1; + $service = new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService($media); + $service->createSnapshot('default'); + } + + /** + * Reset the take_snapshot_on_save flag back to 0 after snapshot. + * + * @return void + * + * @since 02.29.00 + */ + private function clearSnapshotFlag(): void + { + try + { + $db = Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName(['id', 'params'])) + ->from($db->quoteName('#__scheduler_tasks')) + ->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset')) + ); + $task = $db->loadAssoc(); + + if ($task) + { + $p = json_decode($task['params'], true) ?: []; + $p['take_snapshot_on_save'] = '0'; + + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__scheduler_tasks')) + ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($p))) + ->where($db->quoteName('id') . ' = ' . (int) $task['id']) + ); + $db->execute(); + } + } + catch (\Throwable $e) + { + // Best effort + } + } } diff --git a/src/packages/plg_webservices_mokowaas/mokowaas.xml b/src/packages/plg_webservices_mokowaas/mokowaas.xml index 6bfa5dbd..4a6e3d86 100644 --- a/src/packages/plg_webservices_mokowaas/mokowaas.xml +++ b/src/packages/plg_webservices_mokowaas/mokowaas.xml @@ -7,7 +7,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.27.00 + 02.26.15-dev 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 8039a973..aed2936f 100644 --- a/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml +++ b/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml @@ -7,7 +7,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.27.00 + 02.26.15-dev 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 16d5291f..f7154ab1 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.27.00 + * VERSION: 02.26.15 * 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 8b8e2d1e..63e855bf 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.27.00 + * VERSION: 02.26.15 * BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet) */ diff --git a/src/pkg_mokowaas.xml b/src/pkg_mokowaas.xml index 96a55744..fd7a6aca 100644 --- a/src/pkg_mokowaas.xml +++ b/src/pkg_mokowaas.xml @@ -2,7 +2,7 @@ Package - MokoWaaS mokowaas - 02.27.00 + 02.26.15-dev 2026-05-23 Moko Consulting hello@mokoconsulting.tech diff --git a/updates.xml b/updates.xml index 24a325b0..9b5cfb04 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -11,13 +11,13 @@ pkg_mokowaas package site - 02.26.14-dev + 02.26.15-dev 2026-05-31 https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.26.14-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.26.15-dev.zip - ae35f14628602e49528654436663ec86398cff7335dd10c8c401710eacbea06a + 10216c269624df358ce02672382c75419f1ee16914357f923053b6cdbb432b36 dev https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md Moko Consulting