From 257003fb74d0c6a800431f5111fbca3ab3120276 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 14 Jul 2026 22:33:07 -0500 Subject: [PATCH] feat: lock the admin UI while a backup is actively running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New "Lock admin during backup" option (on by default). While a stepped backup is running, every admin page load except the full-screen backup screen and the login page is replaced by a "backup in progress" interstitial that auto-refreshes, so no other window can navigate or change anything mid-backup. Detection uses the stepped-backup session file heartbeat (JPATH_ROOT/tmp/mokosuitebackup-sessions/mb_*.json is rewritten every step and deleted on completion) — a file with an mtime within ~45s means a backup is live. A crashed/abandoned backup goes stale and releases the lock on its own, so the admin is never permanently locked. Implemented in the system plugin's onAfterRoute (admin, HTML page loads only; AJAX/assets and the backup's own step requests are unaffected). Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i --- .../packages/com_mokosuitebackup/config.xml | 11 +++ .../language/en-GB/com_mokosuitebackup.ini | 2 + .../language/en-US/com_mokosuitebackup.ini | 2 + .../en-GB/plg_system_mokosuitebackup.ini | 2 + .../en-US/plg_system_mokosuitebackup.ini | 2 + .../src/Extension/MokoSuiteBackup.php | 95 +++++++++++++++++++ 6 files changed, 114 insertions(+) diff --git a/source/packages/com_mokosuitebackup/config.xml b/source/packages/com_mokosuitebackup/config.xml index f0ce6bda..bd73a14d 100644 --- a/source/packages/com_mokosuitebackup/config.xml +++ b/source/packages/com_mokosuitebackup/config.xml @@ -162,6 +162,17 @@ + + + +
diff --git a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini index fa663c7f..30d012bd 100644 --- a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini @@ -335,6 +335,8 @@ 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." +COM_MOKOJOOMBACKUP_CONFIG_LOCK_ADMIN="Lock admin during backup" +COM_MOKOJOOMBACKUP_CONFIG_LOCK_ADMIN_DESC="While a backup is actively running, show a \"backup in progress\" screen on all other admin pages until it finishes, so nothing changes mid-backup. The backup screen itself and the login page stay accessible. Auto-releases if a backup stops responding." ; Full-screen backup progress view (runbackup) COM_MOKOJOOMBACKUP_RUNBACKUP_TITLE="Backing Up" diff --git a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini index 61f2219d..cd24d53b 100644 --- a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini @@ -57,6 +57,8 @@ 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." +COM_MOKOJOOMBACKUP_CONFIG_LOCK_ADMIN="Lock admin during backup" +COM_MOKOJOOMBACKUP_CONFIG_LOCK_ADMIN_DESC="While a backup is actively running, show a \"backup in progress\" screen on all other admin pages until it finishes, so nothing changes mid-backup. The backup screen itself and the login page stay accessible. Auto-releases if a backup stops responding." ; Full-screen backup progress view (runbackup) COM_MOKOJOOMBACKUP_RUNBACKUP_TITLE="Backing Up" diff --git a/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.ini b/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.ini index 7cc78952..e3085701 100644 --- a/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.ini +++ b/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.ini @@ -15,3 +15,5 @@ PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup complete — safe to update." PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_FAILED="Backup failed" PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_STARTING="Starting backup…" PLG_SYSTEM_MOKOSUITEBACKUP_UPDATE_RUNNING="Backup complete. The update is now running — please wait and do not close this window…" +PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_TITLE="Backup in progress" +PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_MESSAGE="A MokoSuiteBackup backup is currently running in another window. The administrator area is locked until it finishes — this page will refresh automatically. Please do not make changes while the backup runs." diff --git a/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.ini b/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.ini index 12552aea..2f6c301e 100644 --- a/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.ini +++ b/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.ini @@ -15,3 +15,5 @@ PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup complete — safe to update." PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_FAILED="Backup failed" PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_STARTING="Starting backup…" PLG_SYSTEM_MOKOSUITEBACKUP_UPDATE_RUNNING="Backup complete. The update is now running — please wait and do not close this window…" +PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_TITLE="Backup in progress" +PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_MESSAGE="A MokoSuiteBackup backup is currently running in another window. The administrator area is locked until it finishes — this page will refresh automatically. Please do not make changes while the backup runs." diff --git a/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php b/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php index ea4626e1..c6f869da 100644 --- a/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php +++ b/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php @@ -133,6 +133,10 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface return; } + // While a backup is actively running, lock the admin UI so no other + // window/page can navigate or change anything mid-backup. May close(). + $this->maybeLockAdminDuringBackup(); + // Pre-update: send Joomla core-update installs through the full-screen // backup page BEFORE the update runs (may not return from here). $this->maybeRedirectForUpdate(); @@ -155,6 +159,97 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface $this->cleanupOldSnapshots(); } + /** + * Detect whether a stepped backup is actively running right now. + * + * The stepped-backup engine rewrites its session JSON file + * (JPATH_ROOT/tmp/mokosuitebackup-sessions/mb_*.json) on every step and + * deletes it on completion, so a file whose mtime is within the last ~45s + * is a reliable, cross-session heartbeat. A crashed/abandoned backup goes + * stale and stops locking on its own — the admin is never permanently locked. + */ + private function backupIsActive(): bool + { + $dir = JPATH_ROOT . '/tmp/mokosuitebackup-sessions'; + + if (!is_dir($dir)) { + return false; + } + + $cutoff = time() - 45; + + foreach (glob($dir . '/mb_*.json') ?: [] as $file) { + if (@filemtime($file) >= $cutoff) { + return true; + } + } + + return false; + } + + /** + * While a backup is actively running, lock the admin UI: every admin page + * load (except the full-screen backup screen and the login/logout page) + * gets a "backup in progress" interstitial that auto-refreshes, so no other + * window can navigate or change anything mid-backup. Gated by the + * lock_admin_during_backup option; auto-released when the backup ends. + */ + private function maybeLockAdminDuringBackup(): void + { + if (!(int) ComponentHelper::getParams('com_mokosuitebackup')->get('lock_admin_during_backup', 1)) { + return; + } + + $app = $this->getApplication(); + $input = $app->getInput(); + $option = $input->getCmd('option', ''); + $view = $input->getCmd('view', ''); + + // Never lock the full-screen backup screen (it IS the backup) nor login/logout. + if (($option === 'com_mokosuitebackup' && $view === 'runbackup') || $option === 'com_login') { + return; + } + + if (!$this->backupIsActive()) { + return; + } + + $this->renderBackupLockPage(); + $app->close(); + } + + /** + * Emit a self-contained "backup in progress" interstitial that refreshes + * itself (meta refresh) until the backup finishes and the lock releases. + */ + private function renderBackupLockPage(): void + { + $app = $this->getApplication(); + $title = Text::_('PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_TITLE'); + $message = Text::_('PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_MESSAGE'); + $t = htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + $m = htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); + + $html = '' + . '' + . '' + . '' . $t . '
' + . '

' . $t . '

' . $m . '

'; + + $app->setHeader('Content-Type', 'text/html; charset=utf-8', true); + $app->sendHeaders(); + echo $html; + } + /** * Inject the client-side interceptor on the Extensions → Update and * Extensions → Manage pages, so an EXTENSION update / uninstall also runs -- 2.52.0