diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 1b6dc702..4b03a2fd 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.59.00 +# VERSION: 02.59.09 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index d7c84e80..f42503af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,13 @@ ## [02.59.00] --- 2026-07-12 ### 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 503fa346..fa1ba912 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.59.00 +VERSION: 02.59.09 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/source/packages/com_mokosuitebackup/media/css/runbackup.css b/source/packages/com_mokosuitebackup/media/css/runbackup.css new file mode 100644 index 00000000..f29dea3a --- /dev/null +++ b/source/packages/com_mokosuitebackup/media/css/runbackup.css @@ -0,0 +1,84 @@ +/* + * @package MokoSuiteBackup + * @subpackage com_mokosuitebackup + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * + * Full-screen backup progress screen (view=runbackup). Loaded via the Joomla + * Web Asset Manager (asset com_mokosuitebackup.runbackup). + */ + +/* Full-screen backup runs on a black backdrop. */ +html, +body { + background-color: #000 !important; +} + +/* Colours are pulled from the Atum admin template's own CSS custom properties + so the screen matches whatever theme the site runs, with Bootstrap and then + a literal as progressive fallbacks. */ +.msb-runbackup { + --msb-accent: var(--template-link-color, var(--bs-link-color, #2a69b8)); + --msb-accent2: var(--template-special-color, var(--msb-accent)); + --msb-success: var(--template-success-color, var(--bs-success, #2f7d32)); + --msb-danger: var(--template-danger-color, var(--bs-danger, #c52128)); + --msb-track: var(--template-bg-light, var(--bs-gray-200, #edf0f5)); + + padding: 0 1rem; +} + +/* overflow:hidden clips the full-bleed bar to the card's rounded corners. */ +.msb-runbackup .card { + overflow: hidden; +} + +.msb-rb-progress { + height: 2.5rem; + background: var(--msb-track); + overflow: hidden; +} + +.msb-rb-bar { + position: relative; + height: 100%; + width: 0; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-weight: 600; + font-size: .95rem; + white-space: nowrap; + background-color: var(--msb-accent); + background-image: linear-gradient(90deg, var(--msb-accent), var(--msb-accent2)); + transition: width .35s ease; +} + +.msb-rb-bar.is-animated::after { + content: ""; + position: absolute; + inset: 0; + background-image: linear-gradient(45deg, rgba(255, 255, 255, .18) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .18) 50%, rgba(255, 255, 255, .18) 75%, transparent 75%, transparent); + background-size: 1rem 1rem; + animation: msb-rb-stripes 1s linear infinite; +} + +.msb-rb-bar.is-success { + background-image: none; + background-color: var(--msb-success); +} + +.msb-rb-bar.is-danger { + background-image: none; + background-color: var(--msb-danger); +} + +@keyframes msb-rb-stripes { + from { + background-position: 1rem 0; + } + + to { + background-position: 0 0; + } +} diff --git a/source/packages/com_mokosuitebackup/media/joomla.asset.json b/source/packages/com_mokosuitebackup/media/joomla.asset.json index f967399c..12194cba 100644 --- a/source/packages/com_mokosuitebackup/media/joomla.asset.json +++ b/source/packages/com_mokosuitebackup/media/joomla.asset.json @@ -9,6 +9,11 @@ "name": "com_mokosuitebackup.admin", "type": "style", "uri": "com_mokosuitebackup/css/admin.css" + }, + { + "name": "com_mokosuitebackup.runbackup", + "type": "style", + "uri": "com_mokosuitebackup/css/runbackup.css" } ] } diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml index c3b5782f..f3bf61fb 100644 --- a/source/packages/com_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/com_mokosuitebackup/mokosuitebackup.xml @@ -17,7 +17,7 @@ display label there. --> MokoSuiteBackup - 02.59.00 + 02.59.09 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/sql/updates/mysql/02.59.02.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.02.sql new file mode 100644 index 00000000..7534a1bb --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.02.sql @@ -0,0 +1 @@ +/* 02.59.02 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.03.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.03.sql new file mode 100644 index 00000000..1617153b --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.03.sql @@ -0,0 +1 @@ +/* 02.59.03 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.04.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.04.sql new file mode 100644 index 00000000..9dcc5f80 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.04.sql @@ -0,0 +1 @@ +/* 02.59.04 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.05.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.05.sql new file mode 100644 index 00000000..0105d7ae --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.05.sql @@ -0,0 +1 @@ +/* 02.59.05 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.06.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.06.sql new file mode 100644 index 00000000..d0f46de4 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.06.sql @@ -0,0 +1 @@ +/* 02.59.06 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.07.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.07.sql new file mode 100644 index 00000000..82a7f33c --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.07.sql @@ -0,0 +1 @@ +/* 02.59.07 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.08.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.08.sql new file mode 100644 index 00000000..fa653e10 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.08.sql @@ -0,0 +1 @@ +/* 02.59.08 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.09.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.09.sql new file mode 100644 index 00000000..5842668c --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.59.09.sql @@ -0,0 +1 @@ +/* 02.59.09 — 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..af2b2695 100644 --- a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php @@ -89,11 +89,11 @@ class AjaxController extends BaseController * * Called by the full-screen backup screen after a successful backup so the * imminent server-side onExtensionBeforeUpdate / onExtensionBeforeUninstall - * backup is skipped — it arms the same 10-minute throttle keys the system - * plugin checks, preventing a duplicate backup when the update / uninstall - * then proceeds. Arms BOTH keys so a single ack covers whichever action the - * full-screen backup was fronting. - * POST: task=ajax.preupdateAck + * backup is skipped once — it sets a ONE-SHOT skip flag (keyed by the + * `action` the backup was fronting) that plg_system_mokosuitebackup + * consumes on the re-fired update/uninstall request, so exactly that one + * action is not double-backed-up while later, distinct actions still are. + * POST: task=ajax.preupdateAck&action=update|uninstall */ public function preupdateAck(): void { @@ -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..0238f35f 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, @@ -87,53 +88,11 @@ $config = [ 'leaveWarn' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING'), ], ]; + +// All screen CSS loads via the Joomla Web Asset Manager (media/joomla.asset.json), +// not an inline
@@ -164,7 +123,7 @@ $config = [
-
0%
+
0%
@@ -227,10 +186,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 871837df..43ab0dcf 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.59.00 + 02.59.09 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 13bd9ca6..e4a4fc90 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.59.00 + 02.59.09 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 a4309175..5399bd90 100644 --- a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Console - MokoSuiteBackup - 02.59.00 + 02.59.09 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 b85db656..5fc6df8e 100644 --- a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Content - MokoSuiteBackup - 02.59.00 + 02.59.09 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 f559b1f7..4fc20091 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.59.00 + 02.59.09 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 f8b68b13..ecd94713 100644 --- a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> System - MokoSuiteBackup - 02.59.00 + 02.59.09 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 2dcc1986..8be123b1 100644 --- a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Task - MokoSuiteBackup - 02.59.00 + 02.59.09 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 ad3452ad..367abcdc 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.59.00 + 02.59.09 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml index 853c37a1..33c8c9ba 100644 --- a/source/pkg_mokosuitebackup.xml +++ b/source/pkg_mokosuitebackup.xml @@ -8,7 +8,7 @@ Package - MokoSuiteBackup mokosuitebackup - 02.59.00 + 02.59.09 2026-06-02 Moko Consulting hello@mokoconsulting.tech