Files
MokoSuiteBackup/CHANGELOG.md
T
jmiller 3eb2562abd
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
@
2026-07-12 17:48:09 -05:00

13 KiB

Changelog

[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 APIPOST /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 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)

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.

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.58.00] --- 2026-07-06

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)
  • CLI snapshot delete uses the data_file column (was non-existent file_path). (#184)
  • Remove the duplicate pre-update backup (content plugin no longer subscribes to onExtensionBeforeUpdate). (#186)
  • DatabaseImporter collects non-fatal statement errors (getErrors()/hasErrors()). (#188)