From e56a1fd8f6222f7cb9d90d20baad7c386670cbfa Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 9 Jul 2026 18:10:01 -0500 Subject: [PATCH] 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');