diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml
index 23419628..cf3a7684 100644
--- a/.mokogitea/workflows/issue-branch.yml
+++ b/.mokogitea/workflows/issue-branch.yml
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Automation
-# VERSION: 02.58.37
+# VERSION: 02.59.01
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index edc76ac1..223a9c8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,12 +3,13 @@
## [Unreleased]
### Fixed
+- Pre-update backup now fires for **every** extension update/uninstall, not just the first one in a 10-minute window. A coarse 600-second session throttle (armed after any pre-action backup — even after updating MokoSuiteBackup itself or a core Joomla update) was disabling the client-side interceptor **and** the server-side fallback for *all* extensions for 10 minutes, so distinct updates silently went un-backed-up (looked like "only the backup extension triggers it"). Replaced with **one backup per Update action**: a single batch/click still backs up once (deduped per-request), the client re-fire is skipped via a **one-shot** action-keyed flag (consumed once), and the core-update path uses its own dedicated key so it no longer suppresses extension backups.
- Pre-update backup on the **Joomla Update** page is now **seamless** (Akeeba-style): after the full-screen backup runs, the plugin auto-ticks Joomla's "I have taken a backup" checkbox and clicks Install, so the update continues automatically instead of stopping for a second manual click.
- Pre-update full-screen backup now actually fires on the **Joomla Update** page. The "Install the update" button posts to `option=com_joomlaupdate&layout=update` (Joomla's confirm/updating page) — carrying **only `layout=update`**, no `view`/`task` — so the previous match on `view=update`/`update.install` never triggered. The plugin now intercepts `layout=update` (as well as `view=update` and the legacy `update.install`) and returns the browser there flagged `is_backed_up=1`.
### Added
- **Snapshot transfer + master→slave injection (#237):** download a content snapshot as a portable **`.msbsnap`** file (zip of `manifest.json` + `snapshot.json` + a sha256 integrity check); **import** one on another site via a new Import button on the Snapshots page (materialises a local snapshot record you can then restore); and a **direct injection API** — `POST /api/index.php/v1/mokosuitebackup/snapshot/inject` — so a **master** site can push a snapshot straight into a **slave**'s Web Services API. Two conflict modes ship now: **overwrite** (replace matching items by ID — master wins) and **create** (skip existing, add only new), selectable per inject call or defaulted in **Options → Snapshot Transfer**. A receiving site must opt in via the new *Allow Snapshot Injection* setting. A third **duplicate** mode inserts everything as brand-new records with **remapped IDs** (articles, categories, modules — plus their tags, custom-field values and featured flags) using Joomla's Table API so assets/UCM/nested-sets stay valid; each item is inserted defensively (a bad item is skipped and logged, never fatal).
-- Full-screen backup now also fronts **extension updates and uninstalls** (Extensions → Update / Manage), not just core Joomla updates. Because `com_installer`'s `update.update` / `manage.remove` are POST actions (CSRF token + a checked `cid[]` list) that a server-side redirect can't cleanly resume, this is done client-side: the toolbar Update/Uninstall click is intercepted, the selection is captured, the full-screen backup runs, and on return the original selection is restored and the real POST form is re-submitted. Gated by `backup_before_update` / `backup_before_uninstall`, super-user only, and skipped if a backup already ran this session.
+- Full-screen backup now also fronts **extension updates and uninstalls** (Extensions → Update / Manage), not just core Joomla updates. Because `com_installer`'s `update.update` / `manage.remove` are POST actions (CSRF token + a checked `cid[]` list) that a server-side redirect can't cleanly resume, this is done client-side: the toolbar Update/Uninstall click is intercepted, the selection is captured, the full-screen backup runs, and on return the original selection is restored and the real POST form is re-submitted. Gated by `backup_before_update` / `backup_before_uninstall`, super-user only, and deduped to one backup per Update action.
- Manual **"Backup Now"** completion now offers a **View backup record** button (links straight to the record that was just created). It appears only for manual backups — the pre-update / pre-uninstall flow hands control back to Joomla instead.
- **Full-screen backup screen** (`view=runbackup`) for both pre-update and manual backups, modelled on Akeeba's Backup-on-Update — replaces the earlier popup-modal approach. Clicking Joomla's **Install the update** now redirects (server-side, Akeeba-style) to a dedicated full-page backup screen that runs the stepped backup with a real progress bar and then **automatically continues the update** via a validated `returnurl` (flagged `is_backed_up=1` so the backup isn't repeated). Crucially, **no backup runs synchronously inside the update request** — which is what white-screened large sites. The dashboard **Backup Now** now opens the same full-screen screen instead of an inline modal. (#196)
- Retention now prunes **remote** copies too: when a backup is pruned by age/count, its archive is deleted from every enabled remote destination (SFTP / FTP / S3 / Google Drive), not just the local copy. Each uploader gained an idempotent `delete()` method (already-absent file = success), and removal is best-effort — a failing destination is logged but never blocks local pruning. The shared standalone `restore.php` is intentionally left in place (every backup overwrites it, so newer backups still depend on it). (#229)
diff --git a/SECURITY.md b/SECURITY.md
index 9b01abfe..73deb7aa 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md
-VERSION: 02.58.37
+VERSION: 02.59.01
BRIEF: Security vulnerability reporting and handling policy
-->
diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml
index 6a134916..74e20570 100644
--- a/source/packages/com_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/com_mokosuitebackup/mokosuitebackup.xml
@@ -17,7 +17,7 @@
display label there.
-->
MokoSuiteBackup
- 02.58.37
+ 02.59.01
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.01.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.01.sql
new file mode 100644
index 00000000..73dfa00b
--- /dev/null
+++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.01.sql
@@ -0,0 +1 @@
+/* 02.59.01 — no schema changes */
diff --git a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php
index 0442b4bb..0afdce73 100644
--- a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php
+++ b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php
@@ -109,11 +109,21 @@ class AjaxController extends BaseController
return;
}
- // Same keys + semantics as plg_system_mokosuitebackup::runPreActionBackup().
+ /* Set a ONE-SHOT skip flag for the specific action the full-screen backup
+ was fronting. runPreActionBackup() consumes it on the re-fired
+ update/uninstall request (once), so the synchronous backup is skipped
+ that one time but the NEXT distinct action still backs up. Keyed by
+ action so an update ack never suppresses a later, unrelated uninstall.
+ For a core Joomla update (no action) neither flag is set — a core
+ update doesn't fire onExtensionBefore(Update|Uninstall). */
+ $action = $this->app->getInput()->getCmd('action', '');
$session = Factory::getSession();
- $now = time();
- $session->set('mokosuitebackup.preaction_backup_before_update', $now);
- $session->set('mokosuitebackup.preaction_backup_before_uninstall', $now);
+
+ if ($action === 'update') {
+ $session->set('mokosuitebackup.skip_preaction_backup_before_update', true);
+ } elseif ($action === 'uninstall') {
+ $session->set('mokosuitebackup.skip_preaction_backup_before_uninstall', true);
+ }
$this->sendJson(['error' => false, 'message' => 'Pre-action backup acknowledged']);
}
diff --git a/source/packages/com_mokosuitebackup/src/View/Runbackup/HtmlView.php b/source/packages/com_mokosuitebackup/src/View/Runbackup/HtmlView.php
index 39e79740..2f812eda 100644
--- a/source/packages/com_mokosuitebackup/src/View/Runbackup/HtmlView.php
+++ b/source/packages/com_mokosuitebackup/src/View/Runbackup/HtmlView.php
@@ -40,6 +40,9 @@ class HtmlView extends BaseHtmlView
public bool $autostart = true;
+ /** Which action the backup fronts ('update' | 'uninstall' | ''); drives the completion ack skip flag. */
+ public string $action = '';
+
public function display($tpl = null): void
{
$input = Factory::getApplication()->getInput();
@@ -48,6 +51,7 @@ class HtmlView extends BaseHtmlView
$this->description = $input->getString('description', '');
$this->returnUrl = $input->getRaw('returnurl', '');
$this->autostart = (bool) $input->getInt('autostart', 1);
+ $this->action = $input->getCmd('msb_action', '');
if ($this->profileId <= 0) {
$this->profileId = 1;
diff --git a/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php b/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php
index 5f408e55..235d1c04 100644
--- a/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php
+++ b/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php
@@ -71,6 +71,7 @@ $config = [
'profileId' => $this->profileId,
'description' => $this->description,
'returnUrl' => $safeReturnUrl,
+ 'action' => $this->action,
'dashboardUrl' => $dashboardUrl,
'recordUrl' => Route::_('index.php?option=com_mokosuitebackup&view=backup&id=__MSBID__', false),
'autostart' => $this->autostart,
@@ -227,10 +228,11 @@ $config = [
setBar(100, false);
el.title.textContent = L.complete || 'Backup complete';
- /* Arm the pre-action throttles so a following server-side onExtension
- Before(Update|Uninstall) backup is skipped (no duplicate backup).
+ /* Set the one-shot skip flag for THIS action so the following
+ server-side onExtensionBefore(Update|Uninstall) backup is skipped once
+ (no duplicate backup) without suppressing later, distinct actions.
Awaited so it lands before we hand back to the update/uninstall. */
- try { await post({ task: 'ajax.preupdateAck' }); } catch (e) {}
+ try { await post({ task: 'ajax.preupdateAck', action: CFG.action || '' }); } catch (e) {}
if (CFG.returnUrl) {
/* Pre-update / pre-uninstall flow: continue the original action.
diff --git a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml
index 89041781..1ca2fd08 100644
--- a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml
+++ b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml
@@ -8,7 +8,7 @@
-->
Module - MokoSuiteBackup - cPanel
- 02.58.37
+ 02.59.01
2026-06-23
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml
index 14b422a9..bbc3bb48 100644
--- a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Action Log - MokoSuiteBackup
- 02.58.37
+ 02.59.01
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml
index 3c178e47..3c42a189 100644
--- a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Console - MokoSuiteBackup
- 02.58.37
+ 02.59.01
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml
index a1baffc4..945ee740 100644
--- a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Content - MokoSuiteBackup
- 02.58.37
+ 02.59.01
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml
index a31a27a6..24b9c0c6 100644
--- a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml
@@ -1,7 +1,7 @@
Quick Icon - MokoSuiteBackup
- 02.58.37
+ 02.59.01
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_system_mokosuitebackup/media/js/installer-backup.js b/source/packages/plg_system_mokosuitebackup/media/js/installer-backup.js
index 477df90e..e3f5ac4a 100644
--- a/source/packages/plg_system_mokosuitebackup/media/js/installer-backup.js
+++ b/source/packages/plg_system_mokosuitebackup/media/js/installer-backup.js
@@ -73,6 +73,10 @@
var ret = window.location.href;
ret += (ret.indexOf('?') === -1 ? '?' : '&') + 'msb_resume=1';
+ /* Tell the backup screen which action it is fronting so its completion
+ ack sets the matching one-shot skip flag (and not the other). */
+ var action = (task === 'update.update') ? 'update' : 'uninstall';
+
/* Build the redirect BEFORE we swallow the click. btoa() throws on
non-Latin1 input (e.g. a UTF-8 filter value in the query string), so
encode as UTF-8 first; if it still fails, bail WITHOUT preventing the
@@ -81,6 +85,7 @@
try {
target = cfg.runbackupUrl
+ '&profile_id=' + encodeURIComponent(cfg.profileId)
+ + '&msb_action=' + action
+ '&returnurl=' + encodeURIComponent(btoa(unescape(encodeURIComponent(ret))));
} catch (x) {
return;
diff --git a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
index 9722a044..ea873c06 100644
--- a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
System - MokoSuiteBackup
- 02.58.37
+ 02.59.01
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php b/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php
index 2215b4e6..e55d19bd 100644
--- a/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php
+++ b/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php
@@ -28,6 +28,12 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
{
protected $autoloadLanguage = true;
+ /** @var array Per-request guard: at most one pre-action backup per request (dedupes a batch update). */
+ private array $preactionRan = [];
+
+ /** @var array Per-request cache of the one-shot "client already backed up" skip flag. */
+ private array $preactionSkip = [];
+
public static function getSubscribedEvents(): array
{
return [
@@ -223,23 +229,20 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
return;
}
- // Already backed up this session (throttle window)? Then the JS should
- // let the action proceed without a fresh backup.
- $session = Factory::getSession();
- $throttleK = $view === 'update'
- ? 'mokosuitebackup.preaction_backup_before_update'
- : 'mokosuitebackup.preaction_backup_before_uninstall';
- $lastRun = (int) $session->get($throttleK, 0);
- $recent = $lastRun > 0 && (time() - $lastRun) < 600;
-
$doc->getWebAssetManager()->useScript('core');
+ /* No time-based "recently backed up" suppression here: each distinct
+ Update/Uninstall click should get its own full-screen backup. The
+ re-fired click after a backup is guarded client-side by
+ window.__msbResuming, and the synchronous server-side fallback is
+ deduped per-request + by a one-shot skip flag (see runPreActionBackup),
+ so there is no double backup without blocking distinct actions. */
$doc->addScriptOptions('plg_system_mokosuitebackup.installer', [
'runbackupUrl' => Route::_('index.php?option=com_mokosuitebackup&view=runbackup&tmpl=component&autostart=1', false),
'profileId' => (int) $params->get('default_profile', 1),
'backupBeforeUpdate' => $view === 'update' && (bool) $beforeUpdate,
'backupBeforeUninstall' => $view === 'manage' && (bool) $beforeUninst,
- 'recentBackup' => $recent,
+ 'recentBackup' => false,
]);
$doc->addScript(Uri::root(true) . '/media/plg_system_mokosuitebackup/js/installer-backup.js', [], ['defer' => true]);
@@ -313,16 +316,20 @@ JS;
$session = Factory::getSession();
- // Returned from the backup screen: arm the throttle so the synchronous
- // onExtensionBeforeUpdate backup doesn't run again, then let it proceed.
+ // Returned from the backup screen: mark this core update backed up so we
+ // don't redirect to the backup screen again on a page revisit, then let
+ // it proceed. Uses a DEDICATED core-update key — a core update does not
+ // fire onExtensionBefore(Update|Uninstall), so it must not touch the
+ // extension pre-action flags (which would suppress extension backups).
if ((int) $input->getInt('is_backed_up', 0) === 1) {
- $session->set('mokosuitebackup.preaction_backup_before_update', time());
+ $session->set('mokosuitebackup.core_update_backed_up', time());
return;
}
- // Backed up recently already — let the update proceed.
- $lastRun = (int) $session->get('mokosuitebackup.preaction_backup_before_update', 0);
+ // Backed up this core update recently already — let the update proceed
+ // without redirecting to the backup screen again.
+ $lastRun = (int) $session->get('mokosuitebackup.core_update_backed_up', 0);
if ($lastRun > 0 && (time() - $lastRun) < 600) {
return;
@@ -525,17 +532,37 @@ JS;
return;
}
- // Throttle: only run once per 10 minutes to prevent duplicate
- // backups when multiple extensions are updated in a batch
- $session = Factory::getSession();
- $sessionKey = 'mokosuitebackup.preaction_' . $paramName;
- $lastRun = $session->get($sessionKey, 0);
+ /* One backup per Update action — NOT a 10-minute time throttle. The old
+ 600s window suppressed every extension's pre-action backup for 10
+ minutes after any single one ran (e.g. after updating this very
+ extension), so distinct updates silently went un-backed-up. Instead:
- if (time() - $lastRun < 600) {
+ (a) dedupe within a single request — a batch update fires this handler
+ once per extension, all in the same request, so back up only once;
+ (b) honour a one-shot session skip flag set by ajax.preupdateAck when
+ the client-side full-screen backup already ran and is re-firing the
+ real update/uninstall — consumed once, so the NEXT distinct action
+ backs up again. */
+ if (!empty($this->preactionRan[$paramName])) {
return;
}
- $session->set($sessionKey, time());
+ $session = Factory::getSession();
+ $skipKey = 'mokosuitebackup.skip_preaction_' . $paramName;
+
+ if (!array_key_exists($paramName, $this->preactionSkip)) {
+ $this->preactionSkip[$paramName] = (bool) $session->get($skipKey, false);
+ $session->set($skipKey, false);
+ }
+
+ if ($this->preactionSkip[$paramName]) {
+ // Client already backed up; skip this whole re-fired (possibly batch) request.
+ $this->preactionRan[$paramName] = true;
+
+ return;
+ }
+
+ $this->preactionRan[$paramName] = true;
$profileId = (int) $params->get('default_profile', 1);
diff --git a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml
index dd5c5f7d..5b6888ef 100644
--- a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Task - MokoSuiteBackup
- 02.58.37
+ 02.59.01
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml
index a194d45d..02fbeda4 100644
--- a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Web Services - MokoSuiteBackup
- 02.58.37
+ 02.59.01
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml
index 2863cdf5..e8ffce1f 100644
--- a/source/pkg_mokosuitebackup.xml
+++ b/source/pkg_mokosuitebackup.xml
@@ -8,7 +8,7 @@
Package - MokoSuiteBackup
mokosuitebackup
- 02.58.37
+ 02.59.01
2026-06-02
Moko Consulting
hello@mokoconsulting.tech