diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
index 7ce7c08..a7183e3 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.25.00
+ * VERSION: 02.25.03
* PATH: /src/Extension/MokoWaaS.php
* NOTE: Handles Joomla system events for rebranding functionality
*/
@@ -1104,13 +1104,38 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
$bgColor = htmlspecialchars($this->params->get('demo_banner_color', '#d9534f'), ENT_QUOTES, 'UTF-8');
$showCountdown = (int) $this->params->get('demo_banner_show_countdown', 0);
- // Use stored next-reset timestamp (calculated from cron schedule on save)
+ // Use stored next-reset timestamp, or calculate on the fly from cron schedule
$nextReset = $this->params->get('demo_next_reset', '');
$resetAtMs = 0;
- if ($showCountdown && !empty($nextReset))
+ if ($showCountdown)
{
- $resetAtMs = strtotime($nextReset) * 1000;
+ if (!empty($nextReset))
+ {
+ $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;
+ }
+ }
}
$countdownJs = '';
@@ -1709,16 +1734,33 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
{
require_once __DIR__ . '/../Service/DemoResetService.php';
- $tablesRaw = $this->params->get('demo_snapshot_tables', '');
- $tables = array_filter(
- array_map('trim', explode("\n", $tablesRaw))
- );
+ $tablesParam = $this->params->get('demo_snapshot_tables', '');
- $includeMedia = (bool) $this->params->get('demo_snapshot_include_media', 1);
+ // Handle both checkbox array and legacy newline-separated textarea
+ if (is_array($tablesParam))
+ {
+ $tables = array_filter($tablesParam);
+ }
+ else
+ {
+ $tables = array_filter(array_map('trim', explode("\n", $tablesParam)));
+ }
+
+ $mediaDirs = $this->params->get('demo_snapshot_include_media', ['images']);
+
+ // Handle legacy boolean value
+ if ($mediaDirs === '1' || $mediaDirs === true)
+ {
+ $mediaDirs = ['images'];
+ }
+ elseif ($mediaDirs === '0' || $mediaDirs === false)
+ {
+ $mediaDirs = [];
+ }
return new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService(
$tables,
- $includeMedia
+ (array) $mediaDirs
);
}
diff --git a/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php b/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php
index c08f41f..b9a83ac 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.25.00
+ * VERSION: 02.25.03
* 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
new file mode 100644
index 0000000..f035205
--- /dev/null
+++ b/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php
@@ -0,0 +1,63 @@
+value ?? '', ENT_QUOTES, 'UTF-8');
+ $id = $this->id;
+
+ if (empty($this->value))
+ {
+ return 'Token will be generated automatically on first save.
';
+ }
+
+ return <<
+
+
+
+HTML;
+ }
+}
diff --git a/src/packages/plg_system_mokowaas/Field/CurrentIpField.php b/src/packages/plg_system_mokowaas/Field/CurrentIpField.php
index 022b8b4..3f96e91 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.25.00
+ * VERSION: 02.25.03
* 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/SnapshotTablesField.php b/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php
new file mode 100644
index 0000000..7a4e891
--- /dev/null
+++ b/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php
@@ -0,0 +1,157 @@
+ ['content', 'categories', 'fields', 'tags', 'contentitem_tag_map', 'ucm_content', 'ucm_history'],
+ 'users' => ['users', 'user_usergroup_map', 'user_profiles', 'usergroups', 'user_keys', 'user_mfa'],
+ 'menus' => ['menu', 'menu_types'],
+ 'modules' => ['modules', 'modules_menu'],
+ 'assets' => ['assets'],
+ ];
+
+ protected function getOptions()
+ {
+ $db = Factory::getDbo();
+ $prefix = $db->getPrefix();
+ $tables = $db->getTableList();
+
+ $options = [];
+
+ foreach ($tables as $table)
+ {
+ // Only show tables with the site's prefix
+ if (strpos($table, $prefix) !== 0)
+ {
+ continue;
+ }
+
+ // Convert real table name to #__ notation
+ $logical = '#__' . substr($table, strlen($prefix));
+
+ // Determine group for display ordering
+ $group = 'Other';
+
+ foreach (self::TABLE_GROUPS as $groupName => $patterns)
+ {
+ $suffix = substr($table, strlen($prefix));
+
+ foreach ($patterns as $pattern)
+ {
+ if ($suffix === $pattern)
+ {
+ $group = ucfirst($groupName);
+ break 2;
+ }
+ }
+ }
+
+ $obj = (object) [
+ 'value' => $logical,
+ 'text' => $logical,
+ 'disable' => false,
+ 'class' => '',
+ 'onclick' => '',
+ ];
+
+ $options[$group][] = $obj;
+ }
+
+ // Flatten with group headers: content tables first, then alphabetical
+ $priority = ['Content', 'Users', 'Menus', 'Modules', 'Assets'];
+ $sorted = [];
+
+ foreach ($priority as $g)
+ {
+ if (isset($options[$g]))
+ {
+ $sorted = array_merge($sorted, $options[$g]);
+ unset($options[$g]);
+ }
+ }
+
+ // Remaining tables (Other)
+ if (isset($options['Other']))
+ {
+ sort($options['Other']);
+ $sorted = array_merge($sorted, $options['Other']);
+ }
+
+ return $sorted;
+ }
+
+ protected function getInput()
+ {
+ // If no value stored yet, use defaults
+ if ($this->value === null || $this->value === '')
+ {
+ $this->value = self::DEFAULT_TABLES;
+ }
+ elseif (is_string($this->value))
+ {
+ // Handle legacy textarea format (newline-separated)
+ $this->value = array_filter(array_map('trim', explode("\n", $this->value)));
+ }
+
+ return parent::getInput();
+ }
+}
diff --git a/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php b/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
index b8cd1af..03f7016 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.25.00
+ * VERSION: 02.25.03
* 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 f91afc3..1c25565 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.25.00
+ * VERSION: 02.25.03
* 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 cd3cea1..89ff461 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.25.00
+ * VERSION: 02.25.03
* BRIEF: Core snapshot/restore service for demo site reset
*/
@@ -91,27 +91,39 @@ class DemoResetService
private array $tables;
/**
- * Whether to include media files in snapshots.
+ * Directories to include in media snapshot (e.g. ['images', 'media']).
*
- * @var bool
- * @since 02.21.00
+ * @var array
+ * @since 02.25.00
*/
- private bool $includeMedia;
+ private array $mediaDirs;
/**
* Constructor.
*
- * @param array $tables Table names with #__ prefix
- * @param bool $includeMedia Include /images/ directory in snapshot
- * @param string $baseDir Override snapshot root (for testing)
+ * @param array $tables Table names with #__ prefix
+ * @param array|bool $mediaDirs Dirs to snapshot: ['images','media'], true (= images), false/[] (= none)
+ * @param string $baseDir Override snapshot root (for testing)
*
* @since 02.21.00
*/
- public function __construct(array $tables = [], bool $includeMedia = true, string $baseDir = '')
+ public function __construct(array $tables = [], $mediaDirs = ['images'], string $baseDir = '')
{
- $this->tables = !empty($tables) ? $tables : self::DEFAULT_TABLES;
- $this->includeMedia = $includeMedia;
- $this->snapshotDir = $baseDir ?: JPATH_ROOT . '/mokowaas-snapshots';
+ $this->tables = !empty($tables) ? $tables : self::DEFAULT_TABLES;
+ $this->snapshotDir = $baseDir ?: JPATH_ROOT . '/mokowaas-snapshots';
+
+ if ($mediaDirs === true)
+ {
+ $this->mediaDirs = ['images'];
+ }
+ elseif ($mediaDirs === false || empty($mediaDirs))
+ {
+ $this->mediaDirs = [];
+ }
+ else
+ {
+ $this->mediaDirs = (array) $mediaDirs;
+ }
}
/**
@@ -193,12 +205,22 @@ class DemoResetService
$dumped++;
}
- // Media snapshot
- $hasMedia = false;
+ // Media snapshot — one ZIP per directory
+ $mediaDirs = [];
- if ($this->includeMedia)
+ foreach ($this->mediaDirs as $dir)
{
- $hasMedia = $this->snapshotMedia($path);
+ $fullPath = JPATH_ROOT . '/' . $dir;
+
+ if (is_dir($fullPath))
+ {
+ $zipName = 'media_' . $dir . '.zip';
+
+ if ($this->snapshotDirectory($fullPath, $path . '/' . $zipName))
+ {
+ $mediaDirs[] = $dir;
+ }
+ }
}
// Write manifest
@@ -207,7 +229,8 @@ class DemoResetService
'created_at' => gmdate('Y-m-d\TH:i:s\Z'),
'tables' => $dumped,
'table_list' => $this->tables,
- 'has_media' => $hasMedia,
+ 'has_media' => !empty($mediaDirs),
+ 'media_dirs' => $mediaDirs,
'joomla_version' => JVERSION,
];
@@ -308,12 +331,41 @@ class DemoResetService
}
}
- // Restore media
+ // Restore media directories
$mediaRestored = false;
+ $restoredDirs = $manifest['media_dirs'] ?? [];
- if ($manifest['has_media'] ?? false)
+ // Legacy support: old manifests used has_media=true with a single media.zip for /images/
+ if (empty($restoredDirs) && ($manifest['has_media'] ?? false))
{
- $mediaRestored = $this->restoreMedia($path);
+ $restoredDirs = ['images'];
+ }
+
+ foreach ($restoredDirs as $dir)
+ {
+ $zipName = 'media_' . $dir . '.zip';
+ $zipPath = $path . '/' . $zipName;
+
+ // Legacy fallback: old snapshots used media.zip for images
+ if (!file_exists($zipPath) && $dir === 'images' && file_exists($path . '/media.zip'))
+ {
+ $zipPath = $path . '/media.zip';
+ }
+
+ if (file_exists($zipPath))
+ {
+ $targetDir = JPATH_ROOT . '/' . $dir;
+ $this->clearDirectory($targetDir);
+
+ $zip = new \ZipArchive();
+
+ if ($zip->open($zipPath) === true)
+ {
+ $zip->extractTo($targetDir);
+ $zip->close();
+ $mediaRestored = true;
+ }
+ }
}
Log::add(
@@ -495,25 +547,23 @@ class DemoResetService
}
/**
- * Create a ZIP archive of the /images/ directory.
+ * Create a ZIP archive of a directory.
*
- * @param string $snapshotDir Snapshot directory path
+ * @param string $sourceDir Full path to the directory to archive
+ * @param string $zipPath Full path for the output ZIP file
*
- * @return bool True if media was archived
+ * @return bool True if archived successfully
*
- * @since 02.21.00
+ * @since 02.25.00
*/
- private function snapshotMedia(string $snapshotDir): bool
+ private function snapshotDirectory(string $sourceDir, string $zipPath): bool
{
- $imagesDir = JPATH_ROOT . '/images';
-
- if (!is_dir($imagesDir))
+ if (!is_dir($sourceDir))
{
return false;
}
- $zipPath = $snapshotDir . '/media.zip';
- $zip = new \ZipArchive();
+ $zip = new \ZipArchive();
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true)
{
@@ -521,13 +571,13 @@ class DemoResetService
}
$iterator = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator($imagesDir, \RecursiveDirectoryIterator::SKIP_DOTS),
+ new \RecursiveDirectoryIterator($sourceDir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item)
{
- $relativePath = substr($item->getPathname(), strlen($imagesDir) + 1);
+ $relativePath = substr($item->getPathname(), strlen($sourceDir) + 1);
$relativePath = str_replace('\\', '/', $relativePath);
if ($item->isDir())
@@ -545,41 +595,6 @@ class DemoResetService
return true;
}
- /**
- * Restore media files from a snapshot ZIP.
- *
- * @param string $snapshotDir Snapshot directory path
- *
- * @return bool True if media was restored
- *
- * @since 02.21.00
- */
- private function restoreMedia(string $snapshotDir): bool
- {
- $zipPath = $snapshotDir . '/media.zip';
- $imagesDir = JPATH_ROOT . '/images';
-
- if (!file_exists($zipPath))
- {
- return false;
- }
-
- // Clear existing images directory contents (keep the directory itself)
- $this->clearDirectory($imagesDir);
-
- $zip = new \ZipArchive();
-
- if ($zip->open($zipPath) !== true)
- {
- return false;
- }
-
- $zip->extractTo($imagesDir);
- $zip->close();
-
- return true;
- }
-
/**
* Ensure the snapshot root directory exists with .htaccess protection.
*
diff --git a/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini b/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini
index 8bd0219..e9fc2f3 100644
--- a/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini
+++ b/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini
@@ -171,8 +171,8 @@ PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_LABEL="Next Scheduled Reset"
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_DESC="Calculated automatically from the reset schedule. The banner countdown uses this timestamp."
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
-PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Media Files"
-PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Include the /images/ directory in snapshots. Disabling this speeds up snapshot/restore for sites with large media libraries."
+PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Directories"
+PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Select which directories to include in the snapshot. Images contains uploaded media, Media contains extension assets."
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
diff --git a/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini b/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini
index 8bd0219..e9fc2f3 100644
--- a/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini
+++ b/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini
@@ -171,8 +171,8 @@ PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_LABEL="Next Scheduled Reset"
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_DESC="Calculated automatically from the reset schedule. The banner countdown uses this timestamp."
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
-PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Media Files"
-PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Include the /images/ directory in snapshots. Disabling this speeds up snapshot/restore for sites with large media libraries."
+PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Directories"
+PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Select which directories to include in the snapshot. Images contains uploaded media, Media contains extension assets."
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
diff --git a/src/packages/plg_system_mokowaas/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml
index 606a136..2dcecb8 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.25.00
+ 02.25.03-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
@@ -268,6 +268,7 @@