@
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
fix: pre-action backup fires for every extension, not one per 10 min Root cause of "pre-update backup only fires on the backup extension": a coarse 600s session throttle. Whenever ANY pre-action backup ran (including updating MokoSuiteBackup itself, or a core Joomla update), the key mokosuitebackup.preaction_backup_before_update was armed for 10 minutes. onBeforeCompileHead read it as `recentBackup` and DISABLED the client-side interceptor for all extensions, and runPreActionBackup was throttled by the same window. So after updating one extension, every other extension update silently skipped its backup for 10 minutes. Replace the time window with "one backup per Update action": - runPreActionBackup dedupes per-request (a batch update fires the event once per extension in a single request -> back up once) via instance flags, and consumes a ONE-SHOT session skip flag for the re-fired client update/uninstall (skip once, next distinct action backs up). - ajax.preupdateAck sets the one-shot flag for the SPECIFIC action (update|uninstall), passed from installer-backup.js via &msb_action and surfaced by Runbackup HtmlView -> CFG.action. A core update passes no action, so it never suppresses an extension backup. - onBeforeCompileHead drops the recentBackup time-suppression (the re-fire loop is already guarded client-side by window.__msbResuming). - The core Joomla update path uses its own key (mokosuitebackup.core_update_backed_up), decoupled from extensions. Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i @
This commit is contained in:
+2
-1
@@ -3,12 +3,13 @@
|
||||
## [Unreleased]
|
||||
|
||||
### 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)
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -227,10 +228,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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,6 +28,12 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
protected $autoloadLanguage = true;
|
||||
|
||||
/** @var array<string,bool> Per-request guard: at most one pre-action backup per request (dedupes a batch update). */
|
||||
private array $preactionRan = [];
|
||||
|
||||
/** @var array<string,bool> 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user