From e56a1fd8f6222f7cb9d90d20baad7c386670cbfa Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 9 Jul 2026 18:10:01 -0500 Subject: [PATCH 1/4] fix: pre-update full-screen backup fires on Joomla 4/5/6 (view=update) The redirect only matched task=update.install, which Joomla 4/5/6 do not use. Their 'Install the update' flow server-side-redirects to the updating page (view=update), which then extracts the downloaded package from JavaScript (AJAX to com_joomlaupdate/extract.php). So the view=update page LOAD is the last server-side moment before any files change. Intercept that page load (in onAfterRoute, before com_joomlaupdate renders it), redirect to the full-screen backup with a returnurl back to view=update&is_backed_up=1, then the browser returns and the JS extraction runs. update.finalise is deliberately NOT intercepted -- by then the files are already extracted, too late for a pre-update backup. Legacy task=update.install (Joomla 3) still handled. Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i --- CHANGELOG.md | 1 + .../src/Extension/MokoSuiteBackup.php | 43 +++++++++++++------ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94209868..79a998ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Fixed - Package installer is now **honest about success**: the postflight no longer prints "installed successfully" / next-steps when the install actually failed or only partially completed. Joomla logs a failed child extension but still runs the package postflight, so the postflight now (a) verifies every bundled child declared in the manifest actually registered in `#__extensions` (matched by element + type, and folder/group for plugins) and (b) verifies the component's schema tables — derived dynamically from the installed `install.mysql.sql` — actually exist. If anything is missing it enqueues an error listing what's missing and stops before the success message. Fail-open: any manifest/DB/IO glitch is treated as "nothing missing" so a transient error never turns a good install into a false failure. Unconditional housekeeping (e.g. download-key restore) still runs regardless. +- Pre-update full-screen backup screen now actually triggers on **Joomla 6** (and 4/5). The redirect matched only the legacy `update.install` task, which Joomla 4/5/6 don't use — they server-side-redirect to the **updating page `view=update`**, which then extracts the downloaded package from JavaScript. The plugin now intercepts the `view=update` page **load** (the last point before any files change) and returns the browser there flagged `is_backed_up=1` so the extraction proceeds after the backup. (`update.finalise` is intentionally not intercepted — by then the files are already extracted.) - Pre-update/uninstall backup no longer **white-screens** the update on large sites. The synchronous backup that runs inside the extension update/uninstall request now raises `max_execution_time`/`memory_limit` (and `ignore_user_abort`) like the web-cron path, so it can't exhaust the request's default limits mid-backup. (Core Joomla updates additionally get a full-screen backup screen — see below.) - Archive names for **CLI/console-triggered backups** no longer come out as `joomla.invalid_…`. The `[HOST]` placeholder took `$_SERVER['HTTP_HOST']` verbatim, but Joomla's console fills that with the reserved sentinel host `joomla.invalid`; the resolver now treats that (like empty/`localhost`) as unusable and falls back to the configured `live_site` host, then the system hostname. (Set the site's *live_site* to get the exact domain in CLI-built names.) - Standalone restore script generation no longer aborts backups with `str_replace() expects at least 3 arguments, 2 given`. `MokoRestore::generateStandaloneScript()` had a `str_replace()` call (the "Backup Archive" pre-check rewrite) that was missing its `$php` subject argument, so **every** standalone-mode backup fatally errored while "Generating standalone restore.php…" — the archive still finalized and uploaded, but no `restore.php` was ever produced (the true root cause behind #226). (#226) diff --git a/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php b/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php index 41ae8765..a7babc7c 100644 --- a/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php +++ b/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php @@ -146,16 +146,23 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface } /** - * Send a Joomla core-update install through the full-screen backup page - * BEFORE the update runs (Akeeba-style), so no backup runs synchronously - * inside the update request (which is what white-screened large sites). + * Send a Joomla core update through the full-screen backup page BEFORE the + * update applies (Akeeba-style), so no backup runs synchronously inside the + * update request (which is what white-screened large sites). * - * Flow: user clicks "Install the update" (com_joomlaupdate&task=update.install) - * → we redirect to com_mokosuitebackup&view=runbackup (full screen) with a - * returnurl back to update.install&is_backed_up=1 → the backup runs on its - * own page with real progress → on completion the browser returns to - * update.install, we arm the throttle so onExtensionBeforeUpdate doesn't - * duplicate the backup, and the update proceeds. + * Interception point by Joomla version: on Joomla 4/5/6 the "Install the + * update" flow server-side-redirects to the updating page `view=update`, + * which *then* runs the file extraction from JavaScript (an AJAX call to + * com_joomlaupdate/extract.php). So the `view=update` page LOAD is the last + * moment before any files change — we intercept that. (Older releases used + * `task=update.install`.) In onAfterRoute — before com_joomlaupdate renders + * the page — we redirect to view=runbackup with a returnurl back to the same + * page flagged `is_backed_up=1`; the backup runs on its own full-screen page, + * then the browser returns to `view=update`, whose JS extraction then runs. + * On return we arm the throttle so onExtensionBeforeUpdate won't duplicate it. + * + * Note: `update.finalise` is deliberately NOT intercepted — by then the files + * are already extracted, so it is too late for a pre-update backup. */ private function maybeRedirectForUpdate(): void { @@ -167,9 +174,13 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface $app = $this->getApplication(); $input = $app->getInput(); + $view = $input->getCmd('view', ''); + $task = $input->getCmd('task', ''); + // Joomla 4/5/6 updating page (before its JS extracts), or the legacy + // Joomla 3 install task. if ($input->getCmd('option', '') !== 'com_joomlaupdate' - || $input->getCmd('task', '') !== 'update.install') { + || ($view !== 'update' && $task !== 'update.install')) { return; } @@ -200,10 +211,18 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface $token = Session::getFormToken(); $profileId = (int) $params->get('default_profile', 1); - // Where to send the browser after the backup: back to the update install. + // Where to send the browser after the backup: back to the same update + // entry point that fired (the updating page, or the legacy install task), + // flagged so this doesn't loop. $returnUri = new Uri(Uri::base() . 'index.php'); $returnUri->setVar('option', 'com_joomlaupdate'); - $returnUri->setVar('task', 'update.install'); + + if ($view === 'update') { + $returnUri->setVar('view', 'update'); + } else { + $returnUri->setVar('task', $task); + } + $returnUri->setVar('is_backed_up', '1'); $returnUri->setVar($token, '1'); From a63b87d97c9f403321833b49067974c1d216ac68 Mon Sep 17 00:00:00 2001 From: "mokogitea-actions[bot]" Date: Thu, 9 Jul 2026 23:23:12 +0000 Subject: [PATCH 2/4] chore(version): pre-release bump to 02.58.19-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- SECURITY.md | 2 +- source/packages/com_mokosuitebackup/mokosuitebackup.xml | 2 +- .../packages/com_mokosuitebackup/sql/updates/mysql/02.58.19.sql | 1 + .../mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml | 2 +- .../packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml | 2 +- .../packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml | 2 +- .../plg_webservices_mokosuitebackup/mokosuitebackup.xml | 2 +- source/pkg_mokosuitebackup.xml | 2 +- 13 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 source/packages/com_mokosuitebackup/sql/updates/mysql/02.58.19.sql diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index cc754b16..f70a189d 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.17 +# VERSION: 02.58.19 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/SECURITY.md b/SECURITY.md index 7e1087cb..9e0aa406 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.17 +VERSION: 02.58.19 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml index 233d8464..3d15dc7e 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.17 + 02.58.19 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.58.19.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.58.19.sql new file mode 100644 index 00000000..ef0ad5d8 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.58.19.sql @@ -0,0 +1 @@ +/* 02.58.19 — no schema changes */ diff --git a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml index 98be043e..24a0764a 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.17 + 02.58.19 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 14ac64c0..5c43ab09 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.17 + 02.58.19 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 230da13f..edd7e98b 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.17 + 02.58.19 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 99402c16..e95fb7f9 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.17 + 02.58.19 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 056d88da..3bca7be4 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.17 + 02.58.19 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml index fb4be78e..518ae801 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.17 + 02.58.19 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml index 8db1a5dd..d6f0b141 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.17 + 02.58.19 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 7578fe4b..a80405c6 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.17 + 02.58.19 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml index 3e355230..f6b972f0 100644 --- a/source/pkg_mokosuitebackup.xml +++ b/source/pkg_mokosuitebackup.xml @@ -8,7 +8,7 @@ Package - MokoSuiteBackup mokosuitebackup - 02.58.17 + 02.58.19 2026-06-02 Moko Consulting hello@mokoconsulting.tech From 7a0636606e3e33833909e0c0920380f759c9e506 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 10 Jul 2026 10:52:59 -0500 Subject: [PATCH 3/4] feat: full-screen backup for extension update/uninstall + view-record button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the full-screen backup screen beyond core Joomla updates: - Extension updates/uninstalls (com_installer): update.update and manage.remove are POST + checkToken with a checked cid[] list, so a server-side GET redirect can't resume them. New client-side interceptor (installer-backup.js, injected via onBeforeCompileHead on the update and manage views) wraps Joomla.submitbutton, stores the selection, sends the browser to view=runbackup, and on return restores the selection and re-submits the real POST form. Gated by backup_before_update / backup_before_uninstall, super-user only, skipped within the throttle window. - runbackup: on manual 'Backup Now' completion, show a 'View backup record' button (links to view=backup&id=); not shown for the pre-update/uninstall flow (which hands back to Joomla). SteppedBackup Engine now returns record_id in its step result. - runbackup arms the pre-action throttles on success (ajax.preupdateAck, now arming both update+uninstall keys) so the following server-side onExtensionBefore(Update|Uninstall) backup is skipped (no duplicate). - Plugin manifest re-registers media/js. NOTE: untested end-to-end (no dev build available this session) — verify the com_installer resume flow on a dev build before relying on it. Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i --- CHANGELOG.md | 2 + .../language/en-GB/com_mokosuitebackup.ini | 1 + .../language/en-US/com_mokosuitebackup.ini | 1 + .../src/Controller/AjaxController.php | 21 ++- .../src/Engine/SteppedBackupEngine.php | 2 + .../tmpl/runbackup/default.php | 25 ++- .../media/js/installer-backup.js | 142 ++++++++++++++++++ .../mokosuitebackup.xml | 4 + .../src/Extension/MokoSuiteBackup.php | 81 ++++++++++ 9 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 source/packages/plg_system_mokosuitebackup/media/js/installer-backup.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a998ae..4fec5ae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## [Unreleased] ### Added +- 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. +- 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/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini index b6793949..d6cb00b8 100644 --- a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini @@ -347,6 +347,7 @@ COM_MOKOJOOMBACKUP_RUNBACKUP_FAILED="Backup failed" COM_MOKOJOOMBACKUP_RUNBACKUP_RETRY="Retry backup" COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_ANYWAY="Continue without backup" COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD="Back to dashboard" +COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD="View backup record" COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING="A backup is in progress. Leaving now will cancel it." COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults" 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 7789c61e..d344d799 100644 --- a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini @@ -69,6 +69,7 @@ COM_MOKOJOOMBACKUP_RUNBACKUP_FAILED="Backup failed" COM_MOKOJOOMBACKUP_RUNBACKUP_RETRY="Retry backup" COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_ANYWAY="Continue without backup" COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD="Back to dashboard" +COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD="View backup record" COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING="A backup is in progress. Leaving now will cancel it." COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults" diff --git a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php index cda1f601..0442b4bb 100644 --- a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php @@ -85,12 +85,14 @@ class AjaxController extends BaseController } /** - * Mark the pre-update backup as satisfied for this session. + * Mark the pre-action backup as satisfied for this session. * - * Called by the update-page modal after a successful on-demand backup so the - * imminent server-side onExtensionBeforeUpdate backup is skipped — it sets - * the same 10-minute throttle key the system plugin checks, preventing a - * duplicate backup when the admin then clicks Joomla's Update button. + * 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 */ public function preupdateAck(): void @@ -107,10 +109,13 @@ class AjaxController extends BaseController return; } - // Same key + semantics as plg_system_mokosuitebackup::runPreActionBackup(). - Factory::getSession()->set('mokosuitebackup.preaction_backup_before_update', time()); + // Same keys + semantics as plg_system_mokosuitebackup::runPreActionBackup(). + $session = Factory::getSession(); + $now = time(); + $session->set('mokosuitebackup.preaction_backup_before_update', $now); + $session->set('mokosuitebackup.preaction_backup_before_uninstall', $now); - $this->sendJson(['error' => false, 'message' => 'Pre-update backup acknowledged']); + $this->sendJson(['error' => false, 'message' => 'Pre-action backup acknowledged']); } /** diff --git a/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php b/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php index 811a8464..6ced4f5a 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php @@ -217,6 +217,7 @@ class SteppedBackupEngine 'progress' => 100, 'message' => 'Backup complete: ' . $session->archiveName, 'done' => true, + 'record_id' => $session->recordId, ]; } @@ -228,6 +229,7 @@ class SteppedBackupEngine 'progress' => $session->getProgress(), 'message' => $session->statusMessage, 'done' => $session->phase === 'complete', + 'record_id' => $session->recordId, ]; } catch (\Throwable $e) { $session->log('FATAL: ' . $e->getMessage()); diff --git a/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php b/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php index dab4defb..0faeb92d 100644 --- a/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php +++ b/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php @@ -64,6 +64,7 @@ $config = [ 'description' => $this->description, 'returnUrl' => $safeReturnUrl, 'dashboardUrl' => $dashboardUrl, + 'recordUrl' => Route::_('index.php?option=com_mokosuitebackup&view=backup&id=__MSBID__', false), 'autostart' => $this->autostart, 'labels' => [ 'starting' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_STARTING'), @@ -74,6 +75,7 @@ $config = [ 'retry' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_RETRY'), 'continue' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_ANYWAY'), 'dashboard' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD'), + 'viewRecord' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD'), 'leaveWarn' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING'), ], ]; @@ -105,6 +107,7 @@ $config = [
+
@@ -119,6 +122,7 @@ $config = [ var CFG = ; var L = CFG.labels || {}; var running = false; + var lastRecordId = 0; var el = { bar: document.getElementById('msb-rb-bar'), @@ -127,6 +131,7 @@ $config = [ title: document.getElementById('msb-rb-title'), actions: document.getElementById('msb-rb-actions'), retry: document.getElementById('msb-rb-retry'), + record: document.getElementById('msb-rb-record'), continue: document.getElementById('msb-rb-continue'), dashboard: document.getElementById('msb-rb-dashboard') }; @@ -161,18 +166,32 @@ $config = [ node.classList.remove('d-none'); } - function onComplete() { + async function onComplete() { el.bar.classList.remove('progress-bar-striped', 'progress-bar-animated'); el.bar.classList.add('bg-success'); 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). + Awaited so it lands before we hand back to the update/uninstall. */ + try { await post({ task: 'ajax.preupdateAck' }); } catch (e) {} + if (CFG.returnUrl) { + /* Pre-update / pre-uninstall flow: continue the original action. + No "view record" button here — we're handing back to Joomla. */ setStatus(L.continuing || 'Continuing…'); window.setTimeout(function () { window.location.href = CFG.returnUrl; }, 1200); } else { + /* Manual "Backup Now": offer to view the record that was just made. */ setStatus(L.complete || 'Backup complete'); showActions(); + + if (lastRecordId > 0 && CFG.recordUrl) { + showBtn(el.record, L.viewRecord || 'View backup record', + CFG.recordUrl.replace('__MSBID__', String(lastRecordId))); + } + showBtn(el.dashboard, L.dashboard || 'Back to dashboard', CFG.dashboardUrl); } } @@ -219,6 +238,7 @@ $config = [ setBar(init.progress, true); setPhase(init.phase || ''); setStatus(init.message || (L.running || 'Backing up…')); + if (init.record_id) { lastRecordId = init.record_id; } var done = false; while (!done) { @@ -231,11 +251,12 @@ $config = [ setBar(step.progress, true); setPhase(step.phase || ''); setStatus(step.message || ''); + if (step.record_id) { lastRecordId = step.record_id; } done = step.done || false; } running = false; - onComplete(); + await onComplete(); } catch (err) { onError(err && err.message ? err.message : String(err)); } diff --git a/source/packages/plg_system_mokosuitebackup/media/js/installer-backup.js b/source/packages/plg_system_mokosuitebackup/media/js/installer-backup.js new file mode 100644 index 00000000..1153e59a --- /dev/null +++ b/source/packages/plg_system_mokosuitebackup/media/js/installer-backup.js @@ -0,0 +1,142 @@ +/** + * MokoSuiteBackup — full-screen backup before an EXTENSION update / uninstall. + * + * Injected by plg_system_mokosuitebackup on the Extensions → Update and + * Extensions → Manage pages. Unlike the core Joomla update (which has a + * server-side page load we can redirect), com_installer's update.update and + * manage.remove are POST actions with a CSRF token and a checked cid[] list — + * so we intercept CLIENT-SIDE: capture the selection, go to the full-screen + * backup page, and on return restore the selection and re-submit the real POST + * form (fresh token) so the update/uninstall proceeds. + * + * The full-screen page arms the pre-action throttle on success (ajax.preupdate + * Ack), so the server-side onExtensionBefore(Update|Uninstall) backup is then + * skipped — no duplicate backup. + * + * Config: Joomla.getOptions('plg_system_mokosuitebackup.installer'). + */ +(function () { + 'use strict'; + + var cfg = (window.Joomla && Joomla.getOptions) + ? Joomla.getOptions('plg_system_mokosuitebackup.installer', null) + : null; + + if (!cfg || !cfg.runbackupUrl) { + return; + } + + var STORAGE_KEY = 'msbInstallerBackup'; + + /* Toolbar tasks to intercept, gated by which feature is enabled. */ + var TASKS = {}; + if (cfg.backupBeforeUpdate) { TASKS['update.update'] = true; } + if (cfg.backupBeforeUninstall) { TASKS['manage.remove'] = true; } + + function checkedCids() { + var form = document.getElementById('adminForm'); + var cids = []; + + if (form) { + form.querySelectorAll('input[name="cid[]"]:checked').forEach(function (b) { + cids.push(b.value); + }); + } + + return cids; + } + + /* On return from the backup screen: restore the selection and re-submit. */ + function resumeIfNeeded() { + var params = new URLSearchParams(window.location.search); + + if (params.get('msb_resume') !== '1') { + return; + } + + var raw = null; + try { raw = sessionStorage.getItem(STORAGE_KEY); } catch (e) {} + try { sessionStorage.removeItem(STORAGE_KEY); } catch (e) {} + + if (!raw) { + return; + } + + var data; + try { data = JSON.parse(raw); } catch (e) { data = null; } + + if (!data || !data.task) { + return; + } + + var form = document.getElementById('adminForm'); + + if (!form) { + return; + } + + (data.cids || []).forEach(function (id) { + var box = form.querySelector('input[name="cid[]"][value="' + id + '"]'); + if (box) { box.checked = true; } + }); + + /* Re-run the original toolbar action; the wrapper lets it through + because the resume flag is set. */ + window.__msbResuming = true; + + if (window.Joomla && typeof Joomla.submitbutton === 'function') { + Joomla.submitbutton(data.task); + } else if (typeof window.submitbutton === 'function') { + window.submitbutton(data.task); + } + } + + /* Wrap Joomla.submitbutton to intercept the update/uninstall tasks. */ + function wrapSubmit() { + var J = window.Joomla; + + if (!J || typeof J.submitbutton !== 'function' || J.submitbutton.__msbWrapped) { + return; + } + + var original = J.submitbutton; + + var wrapped = function (task) { + /* Skip when resuming after a backup, when a backup already ran this + session (throttle window), or for tasks we don't handle. */ + if (window.__msbResuming || cfg.recentBackup || !TASKS[task]) { + return original.apply(J, arguments); + } + + var cids = checkedCids(); + + /* Nothing selected — let Joomla show its own "please select" notice + and don't run a pointless backup. */ + if (!cids.length) { + return original.apply(J, arguments); + } + + try { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ task: task, cids: cids })); + } catch (e) {} + + var ret = window.location.href; + ret += (ret.indexOf('?') === -1 ? '?' : '&') + 'msb_resume=1'; + + window.location.href = cfg.runbackupUrl + + '&profile_id=' + encodeURIComponent(cfg.profileId) + + '&returnurl=' + encodeURIComponent(btoa(ret)); + }; + + wrapped.__msbWrapped = true; + J.submitbutton = wrapped; + } + + /* Wrap as early as possible, and again once the DOM/core JS is ready. */ + wrapSubmit(); + + document.addEventListener('DOMContentLoaded', function () { + wrapSubmit(); + resumeIfNeeded(); + }); +})(); diff --git a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml index 518ae801..115b64eb 100644 --- a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml @@ -29,6 +29,10 @@ language/en-GB/plg_system_mokosuitebackup.sys.ini + + js + +
diff --git a/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php b/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php index a7babc7c..0782c17a 100644 --- a/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php +++ b/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php @@ -13,8 +13,10 @@ namespace Joomla\Plugin\System\MokoSuiteBackup\Extension; defined('_JEXEC') or die; use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Document\HtmlDocument; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Router\Route; use Joomla\CMS\Session\Session; use Joomla\CMS\Uri\Uri; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\RetentionManager; @@ -31,6 +33,7 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface return [ 'onAfterInitialise' => 'onAfterInitialise', 'onAfterRoute' => 'onAfterRoute', + 'onBeforeCompileHead' => 'onBeforeCompileHead', 'onExtensionBeforeUpdate' => 'onExtensionBeforeUpdate', 'onExtensionBeforeUninstall' => 'onExtensionBeforeUninstall', ]; @@ -145,6 +148,84 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface $this->cleanupOldSnapshots(); } + /** + * Inject the client-side interceptor on the Extensions → Update and + * Extensions → Manage pages, so an EXTENSION update / uninstall also runs + * through the full-screen backup page. + * + * Core Joomla updates are handled server-side (see maybeRedirectForUpdate), + * but com_installer's update.update / manage.remove are POST actions with a + * CSRF token and a checked cid[] list — a server-side GET redirect would drop + * the selection and fail the token check. So the JS wraps Joomla.submitbutton, + * stores the selection, sends the browser to view=runbackup, and on return + * restores the selection and re-submits the real POST form. + */ + public function onBeforeCompileHead(Event $event): void + { + $app = $this->getApplication(); + + if (!$app->isClient('administrator')) { + return; + } + + $input = $app->getInput(); + + if ($input->getCmd('option', '') !== 'com_installer') { + return; + } + + $view = $input->getCmd('view', ''); + + if ($view !== 'update' && $view !== 'manage') { + return; + } + + $params = ComponentHelper::getParams('com_mokosuitebackup'); + $beforeUpdate = (int) $params->get('backup_before_update', 0); + $beforeUninst = (int) $params->get('backup_before_uninstall', 0); + + // Relevant feature for this view must be on. + $active = ($view === 'update' && $beforeUpdate) || ($view === 'manage' && $beforeUninst); + + if (!$active) { + return; + } + + // Super Users only. + $user = $app->getIdentity(); + + if (!$user || $user->guest || !$user->authorise('core.admin')) { + return; + } + + $doc = $app->getDocument(); + + if (!$doc instanceof HtmlDocument) { + 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'); + + $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, + ]); + + $doc->addScript(Uri::root(true) . '/media/plg_system_mokosuitebackup/js/installer-backup.js', [], ['defer' => true]); + } + /** * Send a Joomla core update through the full-screen backup page BEFORE the * update applies (Akeeba-style), so no backup runs synchronously inside the From 4e5bf99f61639730d9534956ced1eabaf4117e69 Mon Sep 17 00:00:00 2001 From: "mokogitea-actions[bot]" Date: Fri, 10 Jul 2026 15:54:02 +0000 Subject: [PATCH 4/4] chore(version): pre-release bump to 02.58.20-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- SECURITY.md | 2 +- source/packages/com_mokosuitebackup/mokosuitebackup.xml | 2 +- .../packages/com_mokosuitebackup/sql/updates/mysql/02.58.20.sql | 1 + .../mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml | 2 +- .../packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml | 2 +- .../packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml | 2 +- .../plg_webservices_mokosuitebackup/mokosuitebackup.xml | 2 +- source/pkg_mokosuitebackup.xml | 2 +- 13 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 source/packages/com_mokosuitebackup/sql/updates/mysql/02.58.20.sql diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index f70a189d..fd83b23a 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.19 +# VERSION: 02.58.20 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/SECURITY.md b/SECURITY.md index 9e0aa406..d46214dc 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.19 +VERSION: 02.58.20 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml index 3d15dc7e..546dac09 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.19 + 02.58.20 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.58.20.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.58.20.sql new file mode 100644 index 00000000..5fdbdc3b --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.58.20.sql @@ -0,0 +1 @@ +/* 02.58.20 — no schema changes */ diff --git a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml index 24a0764a..a5b2f370 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.19 + 02.58.20 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 5c43ab09..47ea83a8 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.19 + 02.58.20 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 edd7e98b..3aa816f5 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.19 + 02.58.20 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 e95fb7f9..a438b8aa 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.19 + 02.58.20 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 3bca7be4..0b68be93 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.19 + 02.58.20 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml index 115b64eb..401a85fe 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.19 + 02.58.20 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml index d6f0b141..8876032b 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.19 + 02.58.20 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 a80405c6..66391a7c 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.19 + 02.58.20 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml index f6b972f0..13df5848 100644 --- a/source/pkg_mokosuitebackup.xml +++ b/source/pkg_mokosuitebackup.xml @@ -8,7 +8,7 @@ Package - MokoSuiteBackup mokosuitebackup - 02.58.19 + 02.58.20 2026-06-02 Moko Consulting hello@mokoconsulting.tech