If the pre-update/uninstall backup fails, the update must not run. The
completion screen no longer offers "Continue without backup" on error —
the only options are Retry (re-run the backup) or Back to dashboard (bail
out without updating). The status message makes clear the update was not
started.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Adds a "Cancel backup" button on the backup progress screen (visible while
the backup runs). Cancelling requires TWO confirmations; on confirm it
stops the stepped-backup loop, best-effort cancels the record server-side
(ajax.cancelBackup), and returns to the dashboard WITHOUT continuing any
pending update. The button is hidden once the backup completes or fails.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
- Backup progress screen shows a prominent warning not to close or switch
windows until the backup finishes (hidden once it completes/errors).
- After the backup hands back to an extension update, a full-screen
"update is running — please wait" overlay is shown while the re-fired
update POST processes, so the user knows it is working (not stuck).
Adds language strings (component + plugin) and a cfg.updateRunningText option.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
fix(installer): extension update now auto-continues after pre-update backup
Two bugs in the client-side resume path made the extension update return
to the selection screen (requiring a manual re-select + re-submit) instead
of continuing after the backup:
1. The re-fire clicked the OUTER <joomla-toolbar-button> (which carries the
task attr), but the web component binds its click handler to the INNER
<button>. Clicking the wrapper did nothing. Now clicks the inner element.
2. list-selection toolbar buttons start DISABLED and only enable when
boxchecked fires a `change` event. We set boxchecked.value but never
dispatched change, so the button stayed disabled and executeTask() no-opd.
feat(runbackup): open record link in new window; harden leave warning
- The "View backup record" link on the pre-update completion screen now
opens in a new window (target=_blank rel=noopener), so viewing the
record does not leave the "Continue the update" flow.
- beforeunload leave-warning now also returns the message (broader browser
support) so closing the window mid-backup reliably prompts a warning.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
feat(runbackup): opt-in auto-continue + manual continue on pre-update screen
The pre-update/uninstall full-screen backup now shows an "Automatically
continue the update when the backup finishes" checkbox (ticked by
default, so the seamless flow is unchanged). If unticked, the screen
stops on completion and offers a link to the new backup record plus a
"Continue the update" button, so the user can hand back to Joomla
manually — a reliable fallback when auto-continue does not fire.
Also drop the hard-to-read `small` text class from the progress screen.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
chore: back-merge main into dev (reconcile release version stamps)
Absorbs main (previous release) into dev ahead of the next release.
All conflicts were VERSION-stamp comments/tags in manifests, SECURITY.md
and issue-branch.yml — kept dev's current line. No functional changes.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
style(runbackup): gray card via bg-body-secondary utility class
Move the card background off a custom CSS rule and onto a Bootstrap
utility class (bg-body-secondary) on the card element itself, so the gray
panel background is declared in the markup. Supersedes the .card
background-color rule added in the previous change.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
style(runbackup): gray card panel on the black backdrop
Set the backup card (.msb-runbackup > .card) to a gray background so it
reads as a panel against the black full-screen backdrop instead of stark
white.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
fix(css): reliably load component stylesheets (progress bar was unstyled)
The full-screen backup progress bar lost all styling after the CSS moved
to the Web Asset Manager: this component's WAM style assets do not emit a
<link> on the admin document. The asset resolves without error (Joomla
lazily registers media/com_mokosuitebackup/joomla.asset.json, so useStyle
does not throw) yet no stylesheet tag is rendered — and the same was
already silently true of com_mokosuitebackup.admin (it was just cosmetic,
so unnoticed). Confirmed live: neither runbackup.css nor admin.css loads,
body stays white, the bar has no track/fill.
Attach the external media stylesheets directly to the document
(addStyleSheet), which renders reliably via <jdoc:include type="styles">.
The joomla.asset.json + useStyle calls are kept so the WAM path resumes
automatically if/when it starts emitting. Fixes both runbackup and the
profile edit screen.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
docs(ajax): correct preupdateAck docblock to one-shot skip semantics
The docblock still described the removed 10-minute throttle ("arms BOTH
keys"). It now sets a one-shot, action-keyed skip flag consumed once by
the system plugin. Comment-only; no behaviour change.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
chore: back-merge main into dev (release 02.59.00 version stamps)
Reconciles main (released 02.59.00) into dev ahead of the next release.
All conflicts were VERSION-stamp comments/tags in manifests, SECURITY.md
and issue-branch.yml — kept dev's current 02.59.05 line. No functional
changes. Clears the dev -> main merge conflict for the release PR.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
refactor(runbackup): move screen CSS into the Web Asset Manager
All full-screen backup CSS now loads through Joomla WAM instead of an
inline <style> block (and the redundant inline style="width:0"):
- New media/css/runbackup.css holds the whole stylesheet.
- Registered as asset com_mokosuitebackup.runbackup in joomla.asset.json.
- tmpl/runbackup/default.php loads it via
getDocument()->getWebAssetManager()->useStyle(com_mokosuitebackup.runbackup),
matching the existing com_mokosuitebackup.admin pattern.
The css folder already ships via the manifest <folder>css</folder>, so no
manifest change is needed.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
style(runbackup): black backdrop on the full-screen backup screen
Set html/body background to #000 so the full-screen pre-update / Backup
Now progress screen runs on a black backdrop (the card + Atum-coloured
progress bar sit on top).
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
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
@
chore: back-merge main into dev (absorb workflow template syncs)
Reconciles the 5 [skip ci] Template-Generic workflow syncs that landed on
main with dev ahead of the release. Only conflict was the VERSION comment
stamp in issue-branch.yml (kept dev's 02.58.37 over the template
placeholder 01.00.00); no functional workflow changes. Clears the
dev -> main merge conflict for the release PR.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
fix(review): open-redirect, btoa UTF-8 safety, honest duplicate-mode txn
Pre-release review findings on dev -> main:
- Return-URL open-redirect (MED): a base64 returnurl like "/\evil.com"
passed the root-relative allow-list (raw[1] is "\", not "/"), and a
browser normalises leading "/\" to "//", yielding a protocol-relative
redirect off-site. Reject any backslash or control char in the decoded
return URL outright — legitimate Joomla admin URLs never contain them.
- installer-backup.js btoa() (LOW): btoa() throws on non-Latin1 input
(e.g. a UTF-8 filter value in the list query string), and the throw was
uncaught after preventDefault(), leaving the toolbar button dead. Build
the redirect (UTF-8-safe base64) BEFORE swallowing the click; if it
still fails, bail without preventing so Joomla proceeds normally.
- Snapshot duplicate-mode transaction (HIGH): restore() wrapped duplicate
mode in transactionStart()/Rollback(), but duplicate mode writes via
Joomla Table API whose Nested tables (categories/tags) issue LOCK
TABLES, which implicitly COMMITS the open transaction in MySQL — so the
rollback promise was illusory and a mid-inject failure left a slave
half-updated. Run only the raw-DB modes (replace/create/merge) inside
the transaction; duplicate mode runs outside it, matching its existing
per-item defensive design (bad item skipped + logged, never fatal).
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
| python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "")
-d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges only if conflict-free; otherwise left open for manual resolution."}')
PR=$(printf '%s' "$RESP" | jqnum number)
if [ -z "$PR" ]; then
echo "::warning::Could not open cascade PR: $RESP"; exit 0
fi
echo "Opened cascade PR #${PR}"
if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then
# dev exists -> force it to match main (dev is a mirror of main)
git push --force origin FETCH_HEAD:refs/heads/dev
echo "dev reset to main" >> "$GITHUB_STEP_SUMMARY"
else
echo "Reusing open cascade PR #${PR}"
fi
# 3. wait for MokoGitea to compute mergeability (conflict detection)
-d '{"body":"<!-- wiki-reminder -->\n\n**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_"}' >/dev/null 2>&1 || true
- 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`.
## [02.64.00] --- 2026-07-14
### 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.
- 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)
## [02.64.00] --- 2026-07-14
### Changed
- Consolidated backup plumbing into shared helpers (#230):
- New `RemoteUploaderFactory` replaces the `createUploaderFromParams()` copy that was duplicated in `BackupEngine` and `SteppedBackupEngine`.
-`RetentionManager` is now the single retention authority — it takes the global `max_age_days`/`max_backups` fallback and gained `pruneOrphans()`; the system plugin's hourly cleanup delegates to it and its duplicate `deleteBackupRecord()` logic is removed.
- The backend controller, Web Services API controller, and legacy `cli/mokosuitebackup.php` now run backups through the shared `BackupRunner` (gaining the normalized complete/warning/fail status) instead of instantiating `BackupEngine` directly.
## [02.63.00] --- 2026-07-13
### 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)
- Remote upload: the standalone restore script upload is no longer silent — its result is now checked and logged, and a failed restore-script upload marks the backup as `warning` (previously the result was discarded, so a missing restore script on the remote went unreported while the archive still showed success). (#226)
## [02.62.00] --- 2026-07-13
## [02.58.00] --- 2026-07-06
## [02.61.00] --- 2026-07-13
### Added
- SFTP destinations: **upload an SSH private key file** in the Add/Edit Destination modal instead of pasting it (reads the file into the key field client-side; pasting still works).
### Changed
- Add/Edit Destination modal: wrap the content in a Bootstrap `.container-fluid` and give it a 1rem padding frame via the WAM stylesheet (Bootstrap's `.modal-content` has no padding of its own).
### Fixed
- Pre-update backup now shows an admin notification on **every** outcome (success / warning / failure) — previously a *successful* pre-update backup fired nothing, so it looked like the notification wasn't working. (#192)
### Changed
- Component admin CSS now loads via the Joomla **Web Asset Manager** (`media/com_mokosuitebackup/` + `joomla.asset.json`) instead of an inline `<style>` block.
- "Keep local copy" is configured **per remote destination** (in the Add/Edit Destination modal); the redundant profile-level field and its "Remote" tab are removed.
- Add/Edit Destination modal: cleaned up the cramped padding/margins.
### Fixed
- Admin menu / duplicate-component fallout: (a) the orphaned `com_component-mokosuitebackup` is now removed in **preflight** too, so its "Backup" menu no longer collides on install ("The alias backup is already being used"); (b) `ensureSubmenuItems()`**recreates the top-level "Backup" menu** if it was deleted, instead of giving up — so Backup no longer disappears from the admin menu (and the cPanel module / MokoSuiteClient can find it again); (c) the component menu label now uses the short constant `COM_MOKOJOOMBACKUP_SHORT`. (#213 fallout)
- Installer no longer aborts on MySQL 8 with "This command is not supported in the prepared statement protocol yet" (error 1295). The legacy remote-column purge migration (`02.56.01.sql`) used `PREPARE`/`EXECUTE`/`DEALLOCATE`, which Joomla's installer rejects; the drop now runs in the package installer script via an INFORMATION_SCHEMA-gated plain `ALTER` (portable across MariaDB and MySQL 8). (#213 update path)
## [02.57.00] --- 2026-07-05
### Added
-`BackupRunner` service — a single synchronous entry point for backups that unifies final status and notification gating across the pre-update, web-cron, pre-install, scheduled-task and CLI triggers (delegates to `BackupEngine`; no double-notify). Returns a normalized result including the real complete/warning/fail status. Dashboard "Backup Now" (AJAX) unchanged; installer progress popup deferred to #196. (#214)
### Fixed
- Release automation: the changelog-promote step is now idempotent — it only inserts a `## [version]` header when that version isn't already present, preventing the empty, duplicated version headers that repeated/same-version builds were producing. (workflow)
## [02.56.11] --- 2026-07-05
### Fixed
- Package `postflight` now removes the orphaned `com_component-mokosuitebackup` registration (stray extension record, admin menu items, schema rows, and `administrator/` folder) left behind by the old "Type - Name" component `<name>` — self-healing on update so affected sites reconcile to the real `com_mokosuitebackup`. (#213 follow-up)
## [02.56.08] --- 2026-07-05
### Fixed
- Component manifest `<name>` reverted to the element-safe `MokoSuiteBackup`. The "Type - Name" convention derived element `com_component-mokosuitebackup`, registering a duplicate component and leaving the real `com_mokosuitebackup` orphaned on the old version. (#213)
## [02.56.07] --- 2026-07-05
### Security
- Mask the FTP password in `AjaxController::maskSecrets()`/`mergeExistingSecrets()` — stored FTP remotes no longer leak their password via `listRemotes`. (#169)
- Stop the API single-item view (`Backups/JsonapiView`) leaking the server `absolute_path`. (#187)
- SFTP uploader uses `StrictHostKeyChecking=accept-new` instead of `no`. (#182)
### Fixed
-`DatabaseDumper` writes a real newline after `DROP TABLE` (was a literal `\n`). (#179)
- Profile-picker forms order by `id` instead of the dropped `ordering` column. (#180)
-`uninstall.mysql.sql` now drops the snapshots table. (#181)
/* Pre-update/uninstall flow: the backup failed, so the update must NOT
run. We deliberately do NOT offer a "continue without backup" option
here — the only ways forward are to retry the backup or go back. */
setStatus(CFG.returnUrl ? (L.failedNoUpdate || 'Backup failed — the update was not started. Retry the backup, or go back without updating.') : (message || 'Backup failed'));
hideCancel();
showActions();
el.retry.textContent = L.retry || 'Retry';
el.retry.classList.remove('d-none');
el.retry.onclick = function () {
el.retry.classList.add('d-none');
el.continue.classList.add('d-none');
el.bar.classList.remove('is-danger');
el.bar.classList.add('is-animated');
run();
};
if (CFG.returnUrl) {
showBtn(el.continue, L.continue || 'Continue without backup', CFG.returnUrl);
}
showBtn(el.dashboard, L.dashboard || 'Back to dashboard', CFG.dashboardUrl);
}
async function run() {
if (running) { return; }
running = true;
cancelled = false;
el.actions.classList.add('d-none');
if (el.cancelWrap) { el.cancelWrap.classList.remove('d-none'); }
if (el.cancel) { el.cancel.disabled = false; }
setBar(0, true);
setPhase('');
setStatus(L.starting || 'Starting backup…');
@@ -288,7 +348,7 @@ $config = [
if (init.record_id) { lastRecordId = init.record_id; }
var done = false;
while (!done) {
while (!done && !cancelled) {
var step = await post({ task: 'ajax.step', session_id: init.session_id });
if (!step || step.error) {
@@ -302,16 +362,27 @@ $config = [
done = step.done || false;
}
/* User cancelled mid-flight — the click handler already stops the
loop and navigates away, so do not fall through to completion. */
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.