feat: pre-update modal phase 2 — auto-run backup on Update click (#196)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 4s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 9s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 31s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled

Opt-in param preupdate_auto_intercept (off by default). When enabled the
injected update-page script wraps window.Joomla.submitbutton so clicking
the toolbar Update button first pops the progress modal, runs the
pre-update backup, and re-issues the original update task on success.

- Matches only update.update / update.install (not Find Updates /
  Clear Cache, which also start with 'update').
- Reuses the phase-1 overlay + ajax.preupdateAck throttle so the
  subsequent server-side onExtensionBeforeUpdate backup is skipped.
- The manual 'Back up now' notice remains a fallback for flows that
  don't route through submitbutton (e.g. some com_joomlaupdate paths).

Refs #196

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
This commit is contained in:
2026-07-05 21:13:35 -05:00
parent 6060979ce8
commit c0ecf6ce73
6 changed files with 81 additions and 4 deletions
+2 -1
View File
@@ -3,7 +3,8 @@
## [Unreleased]
### Added
- Pre-update backup **notice + live-progress modal** on the admin update pages (Joomla Update and Extensions → Update). Because the server-side `onExtensionBeforeUpdate` backup runs synchronously and can't drive a browser modal, the system plugin now injects a "back up before you update" notice with a **Back up now** button that runs the same stepped backup as the dashboard and shows a live progress bar. On success it pings a new `ajax.preupdateAck` endpoint, which arms the same throttle the server-side hook checks — so clicking Joomla's Update afterwards won't run a duplicate backup. Gated by the existing `show_update_notice` + `backup_before_update` params. (Auto-intercepting the Update button is the planned phase-2 follow-up — see #196.) (#196)
- Pre-update backup **notice + live-progress modal** on the admin update pages (Joomla Update and Extensions → Update). Because the server-side `onExtensionBeforeUpdate` backup runs synchronously and can't drive a browser modal, the system plugin now injects a "back up before you update" notice with a **Back up now** button that runs the same stepped backup as the dashboard and shows a live progress bar. On success it pings a new `ajax.preupdateAck` endpoint, which arms the same throttle the server-side hook checks — so clicking Joomla's Update afterwards won't run a duplicate backup. Gated by the existing `show_update_notice` + `backup_before_update` params. (#196)
- Pre-update modal **phase 2 — "Auto-run on Update click"** (new opt-in param `preupdate_auto_intercept`, off by default). When enabled, clicking the toolbar **Update** button on the Joomla Update / Extensions-update page first pops the backup progress modal, runs the pre-update backup, then automatically continues the update. Implemented by wrapping `Joomla.submitbutton` for the `update.update` / `update.install` tasks only (Find Updates / Clear Cache are untouched); the manual "Back up now" notice remains as a fallback for any flow that doesn't route through `submitbutton`. (#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)
### Changed
@@ -151,6 +151,18 @@
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="preupdate_auto_intercept"
type="radio"
label="COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT"
description="COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT_DESC"
default="0"
class="btn-group"
showon="backup_before_update:1"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="backup_before_uninstall"
type="radio"
@@ -331,6 +331,8 @@ COM_MOKOJOOMBACKUP_CONFIG_SHOW_UPDATE_NOTICE_DESC="Display the update site confi
COM_MOKOJOOMBACKUP_CONFIG_PREACTION="Pre-action Backups"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE="Backup Before Extension Update"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE_DESC="Automatically run a full backup before any extension is updated. Uses the default profile. Throttled to once per 10 minutes to prevent duplicate backups during batch updates."
COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT="Auto-run on Update click"
COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT_DESC="When enabled, clicking the Update button on the Joomla Update or Extensions update page first pops the backup progress modal, runs the pre-update backup, then automatically continues the update. When disabled, a notice with a manual 'Back up now' button is shown instead."
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL="Backup Before Extension Uninstall"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL_DESC="Automatically run a full backup before any extension is uninstalled. Uses the default profile. Throttled to once per 10 minutes."
@@ -53,6 +53,8 @@ COM_MOKOJOOMBACKUP_CONFIG_SHOW_UPDATE_NOTICE_DESC="Display the update site confi
COM_MOKOJOOMBACKUP_CONFIG_PREACTION="Pre-action Backups"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE="Backup Before Extension Update"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE_DESC="Automatically run a full backup before any extension is updated. Uses the default profile. Throttled to once per 10 minutes to prevent duplicate backups during batch updates."
COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT="Auto-run on Update click"
COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT_DESC="When enabled, clicking the Update button on the Joomla Update or Extensions update page first pops the backup progress modal, runs the pre-update backup, then automatically continues the update. When disabled, a notice with a manual 'Back up now' button is shown instead."
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL="Backup Before Extension Uninstall"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL_DESC="Automatically run a full backup before any extension is uninstalled. Uses the default profile. Throttled to once per 10 minutes."
@@ -26,11 +26,65 @@
var L = cfg.labels || {};
var running = false;
var backedUp = false;
document.addEventListener('DOMContentLoaded', function () {
injectNotice();
});
if (cfg.autoIntercept) {
setupInterception();
}
/* ── Phase 2: auto-run on the Update click ──────────────────────────────
* Wrap window.Joomla.submitbutton so clicking the toolbar Update button
* first runs the backup modal, then continues the original update once the
* backup succeeds. submitbutton is synchronous, so we can't pause it — we
* swallow the update task, run the async backup, and re-issue the original
* task ourselves on success. The notice's "Back up now" button remains a
* fallback for any flow that doesn't route through submitbutton. */
function setupInterception() {
if (!wrapSubmit()) {
document.addEventListener('DOMContentLoaded', wrapSubmit);
}
}
function wrapSubmit() {
var J = window.Joomla;
if (!J || typeof J.submitbutton !== 'function' || J.submitbutton.__msbWrapped) {
return !!(J && J.submitbutton && J.submitbutton.__msbWrapped);
}
var original = J.submitbutton;
var wrapped = function (task) {
if (!backedUp && !running && isUpdateTask(task)) {
var args = arguments;
runBackup().then(function (ok) {
if (ok) {
original.apply(window.Joomla, args);
}
});
return;
}
return original.apply(window.Joomla, arguments);
};
wrapped.__msbWrapped = true;
J.submitbutton = wrapped;
return true;
}
/* Only the actual "apply the update" tasks — never Find Updates / Clear
* Cache (update.find / update.purge) which also start with "update". */
function isUpdateTask(task) {
return task === 'update.update' || task === 'update.install';
}
function injectNotice() {
var host = document.querySelector('main') || document.querySelector('#content') || document.body;
@@ -175,6 +229,7 @@
synchronous onExtensionBeforeUpdate backup is skipped. */
try { await post({ task: 'ajax.preupdateAck' }); } catch (ignore) {}
backedUp = true;
ui.title.textContent = L.complete || 'Backup complete — safe to update.';
setProgress(ui, 100, '');
@@ -187,6 +242,8 @@
}
setTimeout(function () { ui.overlay.remove(); }, 1500);
return true;
} catch (err) {
ui.title.textContent = (L.failed || 'Backup failed') + ': ' + err.message;
ui.bar.style.background = '#dc3545';
@@ -194,6 +251,8 @@
if (runBtn) {
runBtn.disabled = false;
}
return false;
} finally {
running = false;
window.removeEventListener('beforeunload', guard);
@@ -198,9 +198,10 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
$doc->getWebAssetManager()->useScript('core');
$doc->addScriptOptions('plg_system_mokosuitebackup.preupdate', [
'ajaxUrl' => Route::_('index.php?option=com_mokosuitebackup&format=json', false),
'token' => Session::getFormToken(),
'profileId' => (int) $params->get('default_profile', 1),
'ajaxUrl' => Route::_('index.php?option=com_mokosuitebackup&format=json', false),
'token' => Session::getFormToken(),
'profileId' => (int) $params->get('default_profile', 1),
'autoIntercept' => (bool) (int) $params->get('preupdate_auto_intercept', 0),
'labels' => [
'notice' => Text::_('PLG_SYSTEM_MOKOJOOMBACKUP_UPDATE_NOTICE'),
'backupNow' => Text::_('PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_NOW'),