Finishes the deferred 'duplicate' conflict mode. Inserts snapshot content
as brand-new records with remapped IDs, leaving existing content
untouched — for master->slave 'push as new copies'.
- Uses Joomla's Table API (bootComponent MVCFactory) for Article,
Category and Module so assets, UCM entries, category/tag nested-sets
and alias uniqueness are handled by core rather than hand-rolled.
- Remaps category parent_id (parents first), article catid, module menu
assignments, and custom-field-value item_id; re-features articles.
- Tags are reused/created by title (resolveTagIds) and assigned via the
article table's newTags so UCM/tag-map stay correct.
- uniqueAlias() avoids alias collisions per scope.
- DEFENSIVE: every item is wrapped in try/catch — a bad item is skipped
and logged, never fatal — so an untested/partial import degrades
gracefully instead of corrupting content.
NOTE: still needs a real test pass on a dev site before relying on it.
Refs #237
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Adds the first increment of snapshot transfer:
- SnapshotPortable engine: package a snapshot into a portable .msbsnap
(zip of manifest.json + snapshot.json + sha256), and ingest one back
(from a zip upload or an inline payload) by materialising a local
snapshot record.
- Admin: per-row 'Download portable snapshot' + an Import modal
(upload .msbsnap) on the Snapshots page.
- API: POST /snapshot/inject — master pushes a snapshot payload into a
slave's Web Services API; gated by a new 'Allow Snapshot Injection'
option. Route added to the webservices plugin.
- Restore engine: new 'create' mode (skip existing, insert only new) and
mode-name normalisation (overwrite->replace, create, duplicate). The
'duplicate' (insert-as-new-with-remapped-IDs) mode returns a clear
'not yet available' — deferred to a dedicated, tested pass.
- Options: 'Snapshot Transfer' fieldset — default conflict mode +
allow-inject toggle. Language keys added (en-GB fallback).
Reuses the existing mokosuitebackup.snapshot.manage ACL. NOTE: untested
end-to-end (no dev build this session) — verify download/import/inject
on a dev build.
Refs #237
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
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=<record_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
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
The pre-update return URL is assigned to window.location.href, so a
javascript:/data: URI (empty host) would have passed the host-only check
and executed. Require an absolute http(s) same-host URL or a single-slash
root-relative path; reject javascript:/data:/vbscript: and protocol-relative
//host.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Replace the popup-modal pre-update approach with a dedicated full-page
backup screen, modelled on Akeeba Backup-on-Update, and fix the white
screen caused by running a backup synchronously inside the update request.
- New view=runbackup (View/Runbackup + tmpl/runbackup): full-screen
progress page that auto-starts the stepped backup (ajax.init -> loop
ajax.step), then redirects to a validated returnurl on completion or
shows a completion/retry panel. Auto-resolved by MVCFactory.
- System plugin: on com_joomlaupdate task=update.install (when
backup_before_update is on), redirect to the full-screen backup screen
with returnurl back to update.install&is_backed_up=1; on return, arm the
throttle so onExtensionBeforeUpdate does not run a duplicate backup. No
synchronous backup in the update request.
- Dashboard 'Backup Now' navigates to the full-screen screen instead of
the inline modal; the inline modal + stepped JS are removed.
- Retire the popup modal: delete media/js/update-backup.js, remove the
plugin <media> element and the preupdate_auto_intercept param.
- Dashboard: warn when Global Config live_site is empty (CLI/scheduled
backup naming + URL generation depend on it).
Refs #196
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
The pre-update/uninstall backup runs synchronously inside the extension
update request. Without raising limits, a large site exceeds
max_execution_time/memory_limit mid-backup and the update page
white-screens. Raise set_time_limit(0)/memory_limit/ignore_user_abort
like the web-cron path.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Joomla's console fills $_SERVER['HTTP_HOST'] with the reserved sentinel
'joomla.invalid', which the [HOST] placeholder resolver used verbatim
because its unusable-host check only knew about empty/localhost. Treat
'joomla.invalid' as unusable too, falling back to live_site then the
system hostname, so CLI/console/scheduled backups get a real name.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Opt-in param preupdate_auto_intercept (off by default). When enabled the
injected update-page script wraps window.Joomla.submitbutton so clicking
the toolbar Update button first pops the progress modal, runs the
pre-update backup, and re-issues the original update task on success.
- Matches only update.update / update.install (not Find Updates /
Clear Cache, which also start with 'update').
- Reuses the phase-1 overlay + ajax.preupdateAck throttle so the
subsequent server-side onExtensionBeforeUpdate backup is skipped.
- The manual 'Back up now' notice remains a fallback for flows that
don't route through submitbutton (e.g. some com_joomlaupdate paths).
Refs #196
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
The server-side onExtensionBeforeUpdate backup runs synchronously and
cannot drive a browser modal, so nothing ever 'popped'. Add a client
notice + on-demand live-progress modal on com_joomlaupdate and
com_installer(view=update):
- plg_system_mokosuitebackup gains onBeforeCompileHead, which injects a
'back up before you update' notice + Back up now button on the update
pages (gated by show_update_notice + backup_before_update), plus a new
media JS asset that runs the same stepped backup as the dashboard
(ajax.init then a loop of ajax.step) with a dependency-free progress
overlay.
- New AjaxController::preupdateAck() arms the same 10-min throttle key
runPreActionBackup() checks, so proceeding with Joomla's update after a
modal backup does not run a duplicate server-side backup.
- Plugin manifest registers the new media/js folder; language keys added.
Phase-1 baseline (notice + on-demand). Auto-intercepting the Update
button is the planned phase-2 follow-up (#196).
Refs #196
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Retention (RetentionManager and the plugin's hourly cleanup) only ever
deleted local files; remote archives grew unbounded. Add an idempotent
delete() to RemoteUploaderInterface + all four uploaders and have
RetentionManager remove each pruned archive from the profile's enabled
remotes (best-effort; failures logged). The shared restore.php is left
in place since every backup overwrites it. (#229)
Consolidate duplicated plumbing (#230):
- New RemoteUploaderFactory replaces the createUploaderFromParams copy
duplicated in BackupEngine and SteppedBackupEngine.
- RetentionManager becomes the single retention authority (global-default
fallback + pruneOrphans()); the system plugin delegates to it and its
duplicate doCleanup()/deleteBackupRecord() logic is removed.
- Backend controller, API controller and legacy cli/mokosuitebackup.php
now run through the shared BackupRunner instead of BackupEngine directly.
Refs #229#230
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
MokoRestore::generateStandaloneScript() rewrites the 'Backup Archive'
pre-check via str_replace(ORIG, REPL) but was missing the third
$php subject argument, so every standalone-mode backup fatally errored
with 'str_replace() expects at least 3 arguments, 2 given' at the
'Generating standalone restore.php...' step. The archive still finalized
and uploaded, but no restore.php was produced -- the underlying cause of
#226 (restore script missing on the remote).
Refs #226
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
The standalone restore script was uploaded to each remote with the result
discarded and nothing logged, so a failed second upload left the restore script
missing from the remote with no warning while the archive still recorded
'complete'. Capture the result: log success, and on failure set uploadFailed
(-> warning status) and log the reason.
Fixes#226
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Per feedback, wrap the destination modal content in a Bootstrap .container-fluid
and give that a 1rem frame in the WAM stylesheet, instead of a custom
.msbackup class on .modal-content. Inner header/body/footer drop their own
horizontal padding so the frame isn't doubled.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
- Add/Edit Destination modal: SFTP key can now be UPLOADED as a file (read into
the key textarea client-side via FileReader) instead of copy/paste; pasting
still works.
- Give .modal-content a "msbackup" class with a 1rem padding frame — Bootstrap's
.modal-content has no padding of its own, which made the modal feel cramped.
Styling lives in the WAM stylesheet (media/.../admin.css), scoped to .msbackup.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
runPreActionBackup() only enqueued a message when the backup failed, so a
successful pre-update backup produced no admin notification at all. Now it
surfaces every outcome via the BackupRunner status: complete -> success,
warning (archive made but remote upload failed) -> warning, fail -> error.
Refs #192
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
- Move component admin CSS to the Joomla Web Asset Manager: new
media/com_mokosuitebackup/css/admin.css + media/joomla.asset.json, a <media>
entry in the manifest, and useStyle('com_mokosuitebackup.admin') in the
profile edit view. Removes the inline <style> block (no more inline CSS).
- Remove the profile-level "Keep local copy" field/"Remote" fieldset from
forms/profile.xml — it is now per remote destination (modal remoteKeepLocal).
- Clean up the Add/Edit Destination modal padding/margins (in admin.css).
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i