fix(preupdate): time-bound the pre-action skip flag
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 7s
The one-shot "client already backed up" skip flag was a bare boolean with no expiry. If a user ran the full-screen pre-update backup then abandoned the flow (closed the tab before the re-fired update fired), the flag lingered for the whole session and could suppress a LATER, unrelated pre-action backup — a real update running with no backup. Store the unix time the flag was armed (AjaxController::armPreactionSkip) and honour the skip only within PREACTION_SKIP_TTL (300s), consuming it one-shot. A stale flag is now ignored and the server backs up as normal. Completes the pre-update double-backup hardening: server-side arming stops the "ack fails -> double backup" direction; the TTL stops the "abandoned flow -> skipped backup" direction. Fail-safe both ways. Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Pre-update / pre-uninstall skip flag is now **time-bounded**. It records the moment it was armed and is honoured only within a short TTL (5 min), always consumed one-shot. Previously a bare boolean: if a user ran the full-screen pre-update backup then abandoned the flow (closed the tab before the update re-fired), the flag lingered for the whole session and could silently suppress a later, unrelated pre-action backup — i.e. a real update running with **no** backup. Now a stale flag is ignored and the server backs up as normal.
|
||||
- Pre-update / pre-uninstall backup can no longer run **twice**. The one-shot "skip the synchronous server-side backup" flag is now armed **server-side** the moment the full-screen backup completes (in `AjaxController::step`, keyed by the `msb_action` it is fronting), instead of relying solely on the best-effort client `ajax.preupdateAck` POST (whose failure was silently swallowed in a `catch()`). If that ack ever fails to land, the re-fired extension update/uninstall no longer triggers a second backup behind the "update is running" overlay. The flag is armed **only** on genuine completion (`done`, no error), so a failed or cancelled backup still lets the server back up before the action — the failure mode stays "one extra backup", never "no backup".
|
||||
- Component **Options** page showed the raw `COM_MOKOSUITEBACKUP_CONFIGURATION` constant instead of a translated title. Joomla's `com_config` derives the Options-page title (and the component name) from the extension *element* (`com_mokosuitebackup`), but the language files defined those keys only under the legacy `COM_MOKOJOOMBACKUP_` stem. Added the element-derived `COM_MOKOSUITEBACKUP` and `COM_MOKOSUITEBACKUP_CONFIGURATION` keys to all four language files (en-GB/en-US, `.ini` and `.sys.ini`), matching the already-migrated `COM_MOKOSUITEBACKUP_ACTION_*` ACL keys. The 400+ explicitly-referenced `COM_MOKOJOOMBACKUP_*` keys are unaffected and left as-is. (#234)
|
||||
|
||||
|
||||
@@ -144,15 +144,19 @@ class AjaxController extends BaseController
|
||||
* Called from two places: ajax.step on genuine completion (authoritative) and
|
||||
* ajax.preupdateAck (best-effort client ack) — either one arming the flag is
|
||||
* enough, and re-arming it is harmless.
|
||||
*
|
||||
* Stores the unix time it was armed (not a bare bool) so the plugin can honour
|
||||
* the skip only within a short TTL and never let a stale flag from an abandoned
|
||||
* backup suppress a genuine, unrelated pre-action backup later in the session.
|
||||
*/
|
||||
private function armPreactionSkip(string $action): void
|
||||
{
|
||||
$session = Factory::getSession();
|
||||
|
||||
if ($action === 'update') {
|
||||
$session->set('mokosuitebackup.skip_preaction_backup_before_update', true);
|
||||
$session->set('mokosuitebackup.skip_preaction_backup_before_update', time());
|
||||
} elseif ($action === 'uninstall') {
|
||||
$session->set('mokosuitebackup.skip_preaction_backup_before_uninstall', true);
|
||||
$session->set('mokosuitebackup.skip_preaction_backup_before_uninstall', time());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,17 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
|
||||
/** @var array<string,bool> Per-request cache of the one-shot "client already backed up" skip flag. */
|
||||
private array $preactionSkip = [];
|
||||
|
||||
/**
|
||||
* Max age (seconds) of the client "already backed up" skip flag that this
|
||||
* plugin will honour. The flag stores the unix time it was armed (by the
|
||||
* full-screen backup's ajax.step completion / ajax.preupdateAck); the re-fired
|
||||
* update/uninstall follows within a second or two, so a small window is ample.
|
||||
* Bounding it means a backup that was run then ABANDONED (browser closed before
|
||||
* the update re-fired) cannot silently suppress a genuine, unrelated pre-action
|
||||
* backup minutes or hours later in the same session.
|
||||
*/
|
||||
private const PREACTION_SKIP_TTL = 300;
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
@@ -648,12 +659,16 @@ JS;
|
||||
$skipKey = 'mokosuitebackup.skip_preaction_' . $paramName;
|
||||
|
||||
if (!array_key_exists($paramName, $this->preactionSkip)) {
|
||||
$this->preactionSkip[$paramName] = (bool) $session->get($skipKey, false);
|
||||
$session->set($skipKey, false);
|
||||
/* The flag stores the unix time it was armed. Consume it one-shot (reset
|
||||
to 0) and honour the skip ONLY if it was armed within the TTL — a
|
||||
stale flag from an abandoned backup must not suppress a real one. */
|
||||
$armedAt = (int) $session->get($skipKey, 0);
|
||||
$session->set($skipKey, 0);
|
||||
$this->preactionSkip[$paramName] = ($armedAt > 0 && (time() - $armedAt) <= self::PREACTION_SKIP_TTL);
|
||||
}
|
||||
|
||||
if ($this->preactionSkip[$paramName]) {
|
||||
// Client already backed up; skip this whole re-fired (possibly batch) request.
|
||||
// Client already backed up (recently); skip this re-fired (possibly batch) request.
|
||||
$this->preactionRan[$paramName] = true;
|
||||
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user