Joomla logs a failed child extension but still runs the package
postflight, so the 'installed successfully' message could appear on a
broken install. In postflight, before the success/next-steps messages:
- verify every bundled child declared in the package manifest actually
registered in #__extensions (match element+type, and folder=group for
plugins), and
- verify the component's schema tables (parsed dynamically from the
installed install.mysql.sql) exist in the DB.
If anything is missing, enqueue an error listing it and RETURN before the
success message. Fail-open (try/catch -> nothing missing) so a transient
manifest/DB/IO glitch can't fail a good install. Unconditional
housekeeping (download-key restore, menu fixes) still runs regardless.
Names escaped with htmlspecialchars; table/child lists derived
dynamically, never hard-coded.
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
Fallout of the duplicate-component element bug:
- Preflight now runs removeOrphanedComponent() BEFORE the component install
recreates its menu, so the orphan's "Backup" menu (alias "backup") no longer
collides ("The alias backup is already being used by the Backup menu item").
- ensureSubmenuItems() recreates the top-level "Backup" menu when it is missing
(via new createTopMenu()) instead of bailing out — so a failed install that
deleted the parent menu no longer leaves Backup gone from the admin menu (which
also broke the cPanel module and MokoSuiteClient component detection).
- The top-level menu label now uses the short constant COM_MOKOJOOMBACKUP_SHORT
(manifest <menu> + enforced/normalized in the script).
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Non-workflow docs/config only (CLAUDE.md, README, branch-protection, ISSUE_TEMPLATE/config.yml,
ci-issue-reporter). Workflow files, manifest schema, and Makefile install paths left for the
forge-rebrand gate. Authored-by: Moko Consulting
02.56.01.sql used PREPARE/EXECUTE/DEALLOCATE to conditionally drop the 26 legacy
remote_storage/ftp_*/sftp_*/gdrive_*/s3_* columns portably. Joomla's installer
rejects those statements on MySQL 8 (error 1295: "This command is not supported
in the prepared statement protocol yet"), aborting the whole component install
when updating from an old schema.
Neutralize 02.56.01.sql (now a no-op comment) and move the purge into
Pkg_MokoSuiteBackupInstallerScript::dropLegacyRemoteColumns(): gate on
INFORMATION_SCHEMA, then issue one plain ALTER dropping only the columns that
still exist — portable on MariaDB and MySQL 8, idempotent, non-fatal.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Every non-interactive backup trigger used a slightly different path, so
final status and notification gating behaved inconsistently depending on
what started the backup (this is why pre-update backups did not gate
notifications the way "Backup Now" does).
Add src/Service/BackupRunner.php as the single synchronous entry point.
It delegates to BackupEngine (which already sends notifications gated by
the profile's notify_on_success/notify_on_failure and applies retention —
so there is exactly ONE notification layer), and normalises the result,
including the real complete/warning/fail status the engine records but did
not return. BackupRunner self-loads BackupEngine so callers require one file.
Routed through BackupRunner:
- plg_system runPreActionBackup() (preaction) + web-cron handler (webcron)
- plg_content triggerAutoBackup() (pre-install)
- plg_task scheduled task run
- plg_console RunCommand (cli)
The dashboard AJAX/stepped "Backup Now" flow is unchanged (it is the
interactive equivalent). The installer progress popup is deferred — the
installer request cannot drive the AJAX modal (TODO(#196) noted in code).
php -l clean on all changed files.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Guard the promote so it only inserts a version header when absent (stops the
empty duplicate headers each build was appending), and remove the duplicate
02.56.11 header the non-idempotent promote just created.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
The old "Type - Name" component <name> made Joomla register a duplicate
component under element com_component-mokosuitebackup (its own extension row,
admin menu, schema rows and administrator/ folder), orphaning the real
com_mokosuitebackup on the old version. The manifest fix (#213) prevents new
occurrences; this reconciles already-affected sites automatically.
Package postflight now removes any component whose element matches
%component%mokosuitebackup% (excluding com_mokosuitebackup): deletes its
#__menu, #__schemas and #__update_sites_extensions rows, the #__extensions
record, and the stray admin folder. Idempotent and non-fatal.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
The "Type - Name" convention (commit d155958) set the component <name> to
"Component - MokoSuiteBackup". Joomla derives a component's element from its
<name> (com_ + sanitized name), so this produced element
`com_component-mokosuitebackup` instead of `com_mokosuitebackup`. On update,
Joomla registered a NEW component (new element + folder) at the current
version and orphaned the real `com_mokosuitebackup` at its old version — which
is what `?option=com_mokosuitebackup` and the admin menu still load. Net
effect: the component appears "stuck" on the old version with the old UI.
Revert the component <name> to "MokoSuiteBackup" (-> com_mokosuitebackup) and
document why the display convention must not be applied to a component's
<name>. Packages are exempt (they use <packagename>); plugins/modules are
exempt (<name> is only a display label there).
Refs #171-area remote tab reports; fixes the duplicate-component registration.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Security (P0):
- #169 mask FTP password in AjaxController maskSecrets()/mergeExistingSecrets()
- #187 stop leaking absolute_path in API item view (JsonapiView)
- #182 SftpUploader StrictHostKeyChecking=no -> accept-new (MITM)
Data integrity / correctness (P1):
- #179 DatabaseDumper: real newline after DROP TABLE (was literal backslash-n)
- #180 profile-picker forms ORDER BY id (the 'ordering' column is dropped on
upgraded sites, breaking the query) — run_profile.xml + plg_content xml
- #181 uninstall.mysql.sql: add missing DROP for snapshots table
- #184 CLI snapshot delete uses data_file column (file_path does not exist,
so snapshot files were never deleted)
- #186 remove duplicate pre-update backup: content plugin no longer subscribes
to onExtensionBeforeUpdate (owned by the system plugin); keeps pre-install
- #188 DatabaseImporter now collects non-fatal statement errors
(getErrors()/hasErrors()) so restores are no longer silent and callers can
surface a warning status
Also folds in the .mokogitea/CLAUDE.md mokoplatform -> mokocli fix (stale repo
name after the rename).
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
02.52.25.sql used `DROP COLUMN IF EXISTS` (MariaDB-only), which errors on
Oracle MySQL 8.x — so on MySQL 8 installs the 26 legacy remote_storage/ftp_*/
sftp_*/gdrive_*/s3_* columns were never dropped, yet Joomla recorded the schema
as applied (confirmed on suite.dev: MySQL 8.0.41, all 26 columns still present,
0 profiles with legacy remote data).
New 02.56.01 migration removes them portably: an INFORMATION_SCHEMA-gated
prepared-statement ALTER that drops all 26 columns where present (plain
DROP COLUMN, valid on MySQL 8 + MariaDB) and is a no-op where already gone
(so it's safe on already-migrated installs too). Validated non-destructively
against suite.dev (gate=1, ALTER PREPAREs cleanly).
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
The remote destinations table stayed stuck at "Loading…" and the "Add
Destination" button did nothing. Root cause: the profile-edit DOMContentLoaded
handler instantiated the Bootstrap modal eagerly at the top
(`bootstrap.Modal.getOrCreateInstance`). In Joomla 6, Bootstrap loads as a
deferred ES module, so `bootstrap` is undefined at DOMContentLoaded — the
reference threw a ReferenceError that aborted the entire handler, so
loadRemotes() never ran and the Add button was never bound. (The purge and
stepped-backup modals worked because they resolve the modal lazily inside
click handlers.)
- Resolve the modal lazily via getModal() at click-time (matches the working modals)
- Explicitly load the bootstrap.modal web asset so window.bootstrap.Modal is registered
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
The canonical webservices provider lives in
source/packages/plg_webservices_mokosuitebackup/services/provider.php.
This root-level copy is an orphaned duplicate — nothing references it and
it is not part of any packaged extension. Recovered from a stash of
previously-set-aside cleanup.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Adopts the Template-Joomla package-script pattern for download-key handling
and post-install messaging, while preserving all MokoSuiteBackup-specific
postflight steps (plugins, webcron secret, scheduled task, submenus,
client_id fix, icon sync, backup_dir migration).
- preflight (update): backupDownloadKey() caches the bare dlid value
- postflight (update): restoreDownloadKey() re-writes extra_query as dlid=<key>
- postflight (install): installSuccessful() + warnMissingLicenseKey()
- postflight (update): installSuccessful()
- License nag now runs on install only (a fresh install never has a key),
removing the previous always-run "is a key present?" guard
- Licensing methods log via Joomla\CMS\Log\Log instead of error_log
Two related backup-management fixes.
Retention (records/days to keep):
- The retention fieldset was defined in profile.xml but never rendered
in the profile editor, so retention_days/retention_count were invisible.
Render the retention fieldset on the Archive tab.
- retention_days/retention_count were read by nothing, so they pruned no
backups. Add RetentionManager::prune(), called from completeRecord() in
both BackupEngine and SteppedBackupEngine after a backup finishes.
Policy: delete a completed/warning backup when EITHER it is older than
retention_days OR it falls outside the newest retention_count copies
(0 = unlimited for that rule). Deleting a record also removes its
archive and log file.
- Correct misleading language/schema text ("use global default" — no such
global backup-retention setting exists) to "0 = unlimited".
Purge Old Backups button:
- The modal only opened via a fragile selector match on the toolbar
button's inline onclick, which Joomla 6's Atum toolbar does not render,
so the button did nothing. Wrap Joomla.submitbutton to open the modal
for the backups.purgeModal task, keeping the selector as a fallback.
Per the package-XML convention, manifests must not use language strings for
titles/descriptions, and <name> must follow "Type - Name".
- Component <name> "MokoSuiteBackup" → "Component - MokoSuiteBackup"
- Module <name> "mod_mokosuitebackup_cpanel" → "Module - MokoSuiteBackup - cPanel"
- Hardcode every <description> (pkg, com, mod, all 7 plugins) with the literal
text previously stored in the *_DESCRIPTION language constants
- Hardcode the component admin submenu titles (Dashboard, Backup Records,
Content Snapshots, Backup Profiles)
Plugin <name> values already followed the Group - Name format and are
unchanged. All manifests validated as well-formed XML.
Follow-up to the legacy remote-storage removal — three consumers still
referenced columns this branch drops:
- 02.52.25.sql: use plain DROP COLUMN instead of DROP COLUMN IF EXISTS.
IF EXISTS on DROP COLUMN is a MariaDB-only extension and errors on
Oracle MySQL 8.x (which Joomla also supports); the columns always exist
here, so the guard is unnecessary and the migration is now portable.
- AkeebaImporter::mapToMokoProfile(): stop inserting the 19 dropped
remote_storage/ftp_*/gdrive_*/s3_* columns (would fatal with "Unknown
column" on Akeeba import). Remote settings now live in the remotes
table and are re-added on the profile Remote tab after import.
- AjaxController::browseSftpDir() + SftpPathField: remove. These were the
legacy single-SFTP path picker, orphaned when the SftpPath form field
was removed; they read now-dropped sftp_* columns.
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
- Add COM_MOKOJOOMBACKUP_SHORT="Backup" to the component .ini and .sys.ini
(en-GB + en-US) so both view titles and the admin sidebar menu resolve it.
- Prefix every admin view's toolbar title with the short brand via the
constant (e.g. "Backup: Dashboard", "Backup: Records"), and tighten the
view title strings that redundantly led with "Backup"/"MokoSuiteBackup".
- Admin sidebar top-level menu now shows the short name. Hardcoded as
"Backup" in the manifest per the package-xml convention (no language
strings in package XML).
Drops the per-profile remote_storage column and all legacy FTP/SFTP/S3/
Google Drive credential columns. Remote destinations are now sourced
exclusively from #__mokosuitebackup_remotes (multi-remote), which is
created at install time — so the backward-compat fallback branches in
BackupEngine, SteppedBackupEngine and loadRemoteDestinations are removed.
- sql: drop 26 legacy columns (install.mysql.sql + 02.52.25.sql migration)
- forms/profile.xml: remove legacy remote fields and ftp/gdrive/s3 fieldsets
- tmpl/profile/edit.php: drop legacy UI, add save-first prompt, use
getOrCreateInstance for the modal, read item.params (was item.config)
- PreflightCheck: validate credentials from the remotes table; curl
warning now applies to ntfy only
- SteppedSession: drop remoteStorage property
- language: add backup-record delete-count strings
- script.php: simplify postflight license-key prompt
Previously a successful backup with a failed remote upload was marked
as "complete", hiding the upload failure. Now these records get a
"warning" status with a yellow badge so operators can see at a glance
which backups didn't reach their remote destination.
Warning-status records are treated as usable backups throughout:
- Downloadable, browsable, and restorable (the archive is intact)
- Counted in dashboard stats, storage totals, and success streaks
- Included in purge operations and differential base lookups
- Shown with yellow "warning" badge in list, detail, and cpanel module
- Filterable via the status dropdown on Backup Records
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
PreflightCheck now auto-cancels "running" backup records that have
exceeded 30 minutes, treating them as stalled. Partial archive files
are cleaned up. The auto-cancelled records are surfaced as warnings
so the user knows what happened.
Records younger than 30 minutes are assumed to be legitimately running
and still block new backups for the same profile.
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
Backups stuck in "running" status block all future backups for the same
profile via the preflight check. Previously the only fix was a manual
DB update.
Adds a toolbar button and AJAX endpoint to cancel stalled backups:
- New ACL permission: mokosuitebackup.backup.cancel
- BackupsController::cancelStalled() for toolbar (multi-select)
- AjaxController::cancelBackup() for AJAX/API use
- Sets status to "fail", cleans up partial archive files
- Updated preflight error message to mention the cancel action
- Language keys for en-GB and en-US
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
Every action handler now logs entry, key parameters, outcomes, and
failures to PHP error_log. Security file creation logs directory
permissions, PHP user, and the specific error when file_put_contents
fails. Database import logs SQL file size, statement counts, and
individual errors. Cleanup logs each file removal success/failure.
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
The security file was only written inside the code-generation block
(first page load). If the file was deleted or failed to write, it
was never recreated because the session already held the code. Now
file writing is a separate check that runs whenever verification is
pending and the file is missing.
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
Remote uploaders (SFTP, FTP, S3, Google Drive) expect type-prefixed
property names (sftp_host, ftp_port, etc.) but createUploaderFromParams
passes unprefixed keys from the remotes table params JSON. Add prefix
mapping in createUploaderFromParams to bridge the naming gap.
Rename .mokorestore-security.php to mokorestore-security.php (no leading
dot) so the file is visible in file managers and not blocked by web
server dotfile rules. Also clean it up in actionCleanup.
Closes#13
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
The ordering column was unused in the profiles UI — profiles sort by ID.
Drops the column via migration, removes all references from model,
config query, importer, and install SQL.
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
The CI checkout step was missing submodules: recursive, causing
MokoSuiteClient to be an empty gitlink during builds. This resulted
in broken MokoSuiteClient.zip and "Install path does not exist" errors.
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
CI version_bump was creating duplicate <version> lines in all
sub-extension manifests. Also AjaxController still referenced the old
`config` column and removed `keep_local` column on the remotes table.
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
The CI version_bump wrote 't' instead of a tab before <version> in
pkg_mokosuitebackup.xml, and appended a duplicate <version> line in
mokosuitebackup.xml instead of replacing the existing one.
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
- Add MokoSuiteClient as git submodule under source/packages/
- Add pkg_mokosuiteclient entry to pkg_mokosuitebackup.xml
- Fix duplicate <version> tag in package manifest
- install.mysql.sql: rename `config` → `params` and drop `keep_local` from remotes
table to match update file 01.41.00 and RemoteTable.php code (fixes Joomla
database maintenance "one problem")
- install.mysql.sql: fix idx_enabled index to use composite (profile_id, enabled)
- install.mysql.sql: add restore_script_name column to profiles table
- 01.43.22.sql: ALTER TABLE to add restore_script_name for existing installs
- DashboardModel: order profile dropdown by ID instead of ordering column
- SteppedBackupEngine: add stack trace logging around MokoRestore standalone
generation to debug str_replace FATAL on SFTP profiles
- SshKeyField: detect base64-encoded keys from DB so the "Key loaded"
badge displays correctly after initial upload
- Add COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED language keys for
Joomla's AdminController delete feedback message
- Remove Run Backup / Backup Now buttons from profiles list, profile edit toolbar, and backup records view
- Move download, browse archive, and view log from backup list rows into individual backup record detail view
- Add download button to backup detail toolbar
- Link profile column in backup records list to profile edit
- Complete restore script filename customization across BackupEngine, SteppedBackupEngine, and MokoRestore
- Remove ordering field from profiles, default sort by ID ascending
- Fix untranslated JFIELD language keys
- Bump all manifests to 01.43.11-dev
- Preflight now detects existing Joomla installation (configuration.php / Version.php)
and shows a yellow warning — does not block, but alerts the user
- Standalone mode: backup archive check scans for all ZIPs instead of hardcoded name
- Multi-zip selector integrated into extract step with radio buttons
- Selected backup file passed through to extract action
- Added warn-style CSS class (yellow) for preflight warnings
- Convert 10 inline CSS modals to Bootstrap 5 (backups: 7, snapshots: 3)
- Replace style.display show/hide with Bootstrap Modal API
- Fix JFIELD_ORDERING_LABEL_ASC → JFIELD_ORDERING_ASC in profile filter
- Add COM_MOKOJOOMBACKUP_CONFIGURATION key for Options page title
- Change ntfy default server to ntfy.mokoconsulting.tech
- Add profile ID to dropdown labels across backups, dashboard, cpanel module
- Add error handling to MokoRestore post() and runPreflight() to prevent UI stalling
- Remove outdated SSH auth pattern references from field descriptions
Joomla's module installer requires a <filename module="..."> element
in the manifest's <files> section. Without it, installation fails with
"No module file specified." Added the stub PHP file and manifest entry.
- Language: "encrypted" → "base64-encoded" for SSH key description
- CHANGELOG: added 3 missing bug fix entries (fields_values scope, CSRF
token on Run Backup, SFTP showon/required)
- [HOST] placeholder: resolve domain from Joomla live_site config when
HTTP_HOST is unavailable (CLI), instead of falling back to system
hostname (joomla.invalid). Applied to both PlaceholderResolver and
FolderPickerField.
Consolidated all fragmented changelog entries from the session into
a single clean v01.41.00 release entry organized by feature area.
Covers: multi-remote, snapshots, SFTP, MokoRestore, sanitization,
engine improvements, admin UI, CLI/API, notifications, security.
New #__mokosuitebackup_remotes table stores remote destinations with
JSON params per type (SFTP/S3/GDrive/FTP). Each profile can have
multiple enabled destinations — the engine uploads to all of them.
Database:
- New table with profile_id FK, type, enabled, params JSON, ordering
- Migration auto-converts existing profile remote columns to new table
- RemoteTable, RemoteModel, RemotesModel classes
Engine:
- BackupEngine: loadRemoteDestinations() + createUploaderFromParams()
iterates all enabled remotes, falls back to legacy columns
- SteppedBackupEngine: one upload step per remote destination, persisted
via session.remoteDestinations + remoteIndex
- Local copy only deleted when ALL uploads succeed
UI:
- Profile edit: "Remote Destinations" linked table with AJAX CRUD
- Add/edit modal with type selector showing dynamic fields
- Toggle enabled/disabled, delete with confirmation
- Legacy fields hidden when remotes configured, shown as fallback
- Secrets masked in responses, merged from DB on save
Closes#97
config.xml:
- Defaults fieldset: archive format, MokoRestore mode, sanitization
defaults (passwords, emails, sessions), log retention days
- Global ntfy fieldset: server, topic, token (fallback for profiles)
access.xml:
- mokosuitebackup.backup.purge — bulk delete old backups
- mokosuitebackup.backup.compare — compare two backups
- mokosuitebackup.backup.browse — browse archive file listings
30+ new language strings for all fields and ACL actions.
Partial #137 (ACL enforcement audit in separate commit)
12 conflict markers from a stale stash pop were accidentally
committed to main in PR #135. Resolved all by keeping the
"Updated upstream" blocks which include both error_log() and
user-facing enqueueMessage() calls.
#131: Post-restore actions step in MokoRestore wizard:
- Reset all passwords to temporary "changeme" with requireReset flag
- Reset article hit counters to zero
- Clear content versions (#__history)
- Clear sessions (#__session)
- Clear cache tables and filesystem cache
- Auto-detect sanitized password hashes and prompt for reset
#132: Per-table conflict resolution during database import:
- New "Tables" step shows all tables from database.sql
- Per-table dropdown: Replace / Skip / Merge / Data Only
- Preset buttons: All Replace, All Skip, Everything except users
- Skip mode skips all statements for that table
- Merge mode uses INSERT IGNORE instead of INSERT INTO
- Data Only skips DROP/CREATE, inserts data only
Wizard now has 9 steps: Pre-check → Extract → Tables → Database →
Config → Admin → Post-Restore → Provisioning → Complete
Closes#131, closes#132
New "Data Sanitization" fieldset on profile form with four options:
- Sanitize User Passwords: replaces all bcrypt hashes with invalid sentinel
- Preserve Super Admin: keeps Super Users group passwords intact
- Sanitize User Emails: replaces with user123@sanitized.example.com
- Clear Session Data: excludes #__session table data (default: on)
DatabaseDumper sanitizes rows inline during dump — both in-memory
and file-streaming paths. Super admin detection uses group_id=8
from #__user_usergroup_map with static caching.
Use cases: sharing backups, creating demo/staging sites, GDPR compliance.
Partial #129 (Part 2 — restore script password reset — tracked separately)
Expanded the help modal with:
- Absolute paths: starts with / or drive letter, used as-is
- Relative paths: ./backups, ../backups, ../../backups with table
showing URL-style conventions and resolved examples
- Placeholder paths: detailed descriptions of each placeholder
with current server values
- Recommended configurations table: single site, multi-site,
date-organized, per-profile, shared hosting
- Security warnings for web-root-accessible paths
- Help button uses JS click handler with Bootstrap 5 fallback
(fixes non-working tooltip icon)
All placeholders changed from lowercase to UPPERCASE:
[host] → [HOST], [site_name] → [SITE_NAME], [date] → [DATE],
[datetime] → [DATETIME], [profile_id] → [PROFILE_ID], etc.
[HOME] and [DEFAULT_DIR] were already uppercase — now consistent.
SQL migration 01.39.01 updates existing profile data in the database.
Resolution display prefixed with "EXAMPLE:" to clarify these are
example values resolved at backup time.
13 files updated across engines, fields, forms, templates, and SQL.
BackupDirectory::resolve() only handles [HOME] and [DEFAULT_DIR].
The checkDir AJAX endpoint now uses PlaceholderResolver to also
resolve [site_name], [host], [profile_id], [date], etc. before
checking if the directory exists. This makes the "Resolves to"
display accurate for all placeholder types.
FolderPickerField: shows resolved placeholder values below input as
badges (e.g. [HOME]=/home/user, [host]=example.com), plus full
resolved path. Updates live as user types.
BackupsController::start(): accept CSRF token from both GET and POST
so the "Run Backup Now" link button on profile edit works without
triggering "security token did not match" error.
The column was TINYINT(1) which can only store 0/1. The new
'standalone' mode value causes MySQL to truncate the string to 0,
breaking profile save. Changed to VARCHAR(20) to support all three
modes: '0' (none), '1' (wrapped), 'standalone'.
New MokoRestore mode: 'standalone' generates restore.php as a separate
file that scans its directory for ZIP backup archives and lets the user
choose which one to restore. Unlike 'wrapped' mode which bundles
restore.php inside the backup ZIP, standalone mode keeps both files
separate — ideal for remote servers where you SCP the backup.
Changes:
- MokoRestore::generateStandalone() — writes restore.php with ZIP scanner
- Profile form: include_mokorestore now a dropdown (none/wrapped/standalone)
- BackupEngine: standalone mode writes restore.php + uploads to remote
- Restore script uses safe DOM methods (no innerHTML with user data)
Closes#107
#100: Run Backup button on profiles list (per-row) and edit toolbar,
backup count badge linking to filtered backups view, View Backups
toolbar button on profile edit.
#101: Profile → filtered backup list link (included in #100).
#104: Snapshot browse modal now shows tabbed view (Articles,
Categories, Modules) with item counts. AjaxController returns
all content types. Categories show indented hierarchy.
#108: "Do not navigate away or close this window" warning banner
added to both backup and restore progress modals.
#110: Joomla Action Logs integration — RestoreEngine, SnapshotEngine,
and SnapshotRestoreEngine now dispatch events that the actionlog
plugin logs to #__action_logs.
Closes#100, closes#101, closes#104, closes#108, closes#110
Both Backup Directory and Archive Name Format fields now show clickable
placeholder tags below the input. Clicking a tag inserts the placeholder
at the current cursor position using selectionStart/End.
- FolderPickerField: pills for [HOME], [DEFAULT_DIR], [host], [site_name],
[date], [profile_id], [profile_name], [type]
- PlaceholderTextField: new custom field type used by archive_name_format,
configurable placeholders via XML attribute
- Cursor position preserved after insert, input event dispatched for
live status updates
Joomla validates required fields server-side regardless of showon
visibility. SFTP fields with required="true" block saving when
remote_storage is set to None or S3/GDrive because the hidden
fields submit empty values. Validation should be done in
ProfileTable::check() conditionally instead.
SFTP UX improvements:
- SshKeyField: file upload button (FileReader → base64 → hidden field),
key never displayed as readable text, __KEEP_EXISTING__ sentinel
preserves DB value on re-save without re-uploading
- Auth type dropdown: password / key file / key file + passphrase
with conditional field visibility via showon
- Required field markers on host, username, path, password
- Remove insecure FTP option from remote storage dropdown
Security:
- Private key stored base64-encoded in database
- SftpUploader decodes base64 before writing temp file
- ProfileTable::store() handles sentinel to prevent key leakage
- Key content never rendered in HTML form output
Joomla's showon JS only finds the controlling field (remote_storage)
within the same rendered fieldset/tab. SFTP fields were in a separate
fieldset so they were never shown. Moved into the remote fieldset
alongside the dropdown.
Add three new dashboard widgets:
- Snapshot widget: latest snapshot info, type badges, item counts,
link to snapshots view, total count
- Backup trend: CSS bar chart showing daily backup sizes over 30 days,
red bars for days with failures, tooltips with details
- Storage breakdown: horizontal bars showing space used per profile
with color coding and backup counts
Closes#61
AJAX-powered file browser in backups list and detail views:
- AjaxController::browseArchive() reads ZIP/tar.gz entries
- Browse button on each backup row + detail view
- Modal shows file list with names and sizes (first 500 entries)
Closes#59
The build pipeline strips newlines from PHP files during packaging.
Single-line comments (//) then eat everything after them on the
concatenated line, causing a FatalError on install. Block comments
(/* */) survive minification.
Send notifications when site restores and snapshot create/restore
complete. Uses sendRestoreNotification() with type-specific subjects.
All calls wrapped in try-catch to never break the actual operation.
Closes#60
Add mokosuitebackup.snapshot task type for com_scheduler with params
for content_types and description_format ([date]/[datetime] placeholders).
Closes#56
Add five endpoints matching the existing backup API pattern:
- GET /snapshots — list with pagination
- POST /snapshot — create (content_types, description)
- POST /snapshot/:id/restore — restore (mode, content_types)
- DELETE /snapshot/:id — delete record + file
- GET /snapshot/:id/download — stream JSON file
ACL: mokosuitebackup.snapshot.manage for write ops, core.manage for read.
Routes registered in webservices plugin alongside backup routes.
Closes#54
After archive is created and checksum computed, automatically verify:
- Archive opens without error
- Contains at least one entry
- database.sql present when backup type includes database
- First entry is readable (spot-check)
Applied to both BackupEngine and SteppedBackupEngine. Throws
RuntimeException on verification failure (backup marked as failed).
Closes#65
The fields_values table is shared across all Joomla extensions.
Previously, dump captured ALL field values and restore deleted ALL
field values, destroying data for contacts, users, and other
extensions. Now scoped via subquery on field_id WHERE context =
'com_content.article'.
Remote upload failure (S3/FTP/GDrive) no longer marks the entire
backup as failed. The local archive is preserved with status
'complete' and the upload failure is logged as a warning. Applies
to both BackupEngine and SteppedBackupEngine.
Closes#66
When articles are included in a snapshot, now also captures:
- #__tags (tag definitions)
- #__fields (custom field definitions for com_content.article)
- #__fields_values (custom field values)
- #__fields_categories (field-to-category mappings)
Restore correctly scopes deletes to avoid touching non-content fields.
Closes#57
Add retention settings for content snapshots (max count, max age days)
in component options. System plugin runs cleanupOldSnapshots() alongside
existing backup cleanup, deleting JSON files and DB records.
Closes#63
The root mokosuitebackup.xml and mokosuitebackup.php are stale
copies from before the code was restructured into source/packages/.
The webservices plugin lives at source/packages/plg_webservices_mokosuitebackup/
with its own manifest and src/ directory.
The root manifest still referenced <folder>src</folder> but that
directory was removed in PR #82, causing Joomla installer to fail
with "File does not exist [ROOT][TMP]/.../mokosuitebackup/src".
#68: Add backup type filter dropdown to backups list view
- filter_backups.xml: full/database/files/differential options
- BackupsModel: backup_type filter in getListQuery()
- Language string: COM_MOKOJOOMBACKUP_FILTER_TYPE_ALL
#72: Path traversal protection in RestoreEngine and MokoRestore
- RestoreEngine::extractArchive(): validate ZIP entries before extractTo()
- RestoreEngine::extractTarGz(): validate PharData entries before extractTo()
- MokoRestore standalone script: same validation in generated PHP code
- Rejects entries containing ../ or starting with / or \
Closes#68, closes#72
CRITICAL:
- #73: S3Uploader now streams file via CURLOPT_PUT/INFILE instead of
loading entire file into RAM with file_get_contents
- #74: DatabaseDumper gains dumpToFile() that streams SQL to disk;
BackupEngine uses addFile() instead of addFromString() to avoid
holding the entire dump in memory
- #75: AkeebaImporter removes unserialize() — only uses json_decode,
skips legacy serialized filter data to prevent object injection
MEDIUM (also fixed):
- BackupEngine: $archiveName initialized before try block (prevents
undefined variable in catch)
- BackupEngine: plaintext archive deleted on encryption failure
- BackupEngine: temp SQL file cleaned up in both success and failure
- BackupEngine: createArchiver() throws on unknown format instead of
silently falling back to ZIP
- TarGzArchiver: intermediate .tar cleaned up in finally block
Closes#73, closes#74, closes#75
Ref #81
Fixes all critical and high severity issues from the codebase audit:
CRITICAL:
- #71: RestoreCommand passed wrong args to RestoreEngine (filepath
instead of record ID) — CLI restore was completely broken
- #72: JpaUnarchiver path traversal — added traversal rejection and
realpath boundary check to prevent writes outside staging dir
- #77: RestoreEngine staging path sanitized — $record->tag stripped
of non-alphanumeric characters
HIGH:
- #75: (noted, AkeebaImporter unserialize needs separate refactor)
- #76: BackupTable now deletes DB row before file — prevents data
loss if DB delete fails
- #78: API profiles endpoint now masks sensitive fields (passwords,
keys, tokens) with '***'
- #79: Webcron handler adds return after sendJsonResponse — prevents
execution falling through on non-terminal close()
- #80: BackupModel/ProfileModel loadFormData() now casts array to
object — prevents TypeError on PHP 8.x form state restore
PREFLIGHT HARDENING:
- PreflightCheck::run() wrapped in try-catch for DB exceptions
- mkdir() failure now includes actual error reason
- Unresolved placeholders generate a warning instead of silent return
Closes#71, closes#76, closes#77, closes#78, closes#79, closes#80
Ref #72, ref #81
- Remove dead checkRequiredExtensions() method (superseded by PreflightCheck)
- Add 'warnings' key to ALL return paths in BackupEngine::run() and
SteppedBackupEngine::init() to prevent undefined key access on PHP 8.x
- Include preflight warnings in success, failure, and early-exit returns
Validate backup prerequisites before creating any record, catching
common issues early with clear messages instead of failing mid-backup.
Pre-flight checks:
- Required PHP extensions (zip, pdo, pdo_mysql, mbstring, curl)
- Backup directory exists and is writable
- Sufficient disk space (last backup size + 20% buffer, skipped if
no previous backup exists)
- No other backup already running for this profile
- Excluded tables exist in database (warns on missing)
- Remote storage credentials minimally configured (FTP/S3/GDrive)
Errors block the backup; warnings are logged and displayed but allow
the backup to proceed. Integrated into both BackupEngine::run() and
SteppedBackupEngine::init() before any record is inserted.
UI: AJAX init response includes warnings array, displayed in the
stepped backup progress modal.
Closes#67
Fixes from code review and silent failure audit:
- SnapshotRestoreEngine: catch only duplicate key errors (MySQL 1062)
in merge mode, re-throw all other exceptions instead of swallowing
- SnapshotRestoreEngine: add json_last_error() check for better error
messages on corrupt snapshot files
- SnapshotRestoreEngine: log warnings when set_time_limit/ini_set fail
- SnapshotEngine: use strlen($json) instead of filesize() to avoid
race conditions; catch \Exception instead of \Throwable
- SnapshotsController: remove @unlink suppression, add try-catch
around delete loop with partial failure reporting
- script.php: add user-facing warning when webcron secret generation
fails (was silently swallowed, inconsistent with other catch blocks)
The truncateFiltered() method ran unfiltered DELETE FROM #__modules
in replace mode, which would wipe ALL site modules (admin toolbar,
login, menus) — not just the ones in the snapshot. Now scoped to
only delete modules whose IDs exist in the snapshot data.
Also scopes #__modules_menu delete to snapshot module IDs, and adds
defense-in-depth validation of restore_mode in the controller.
Add content snapshot system for lightweight article/category/module
versioning independent of full backups. Snapshots store as JSON files
with replace or merge restore modes, wrapped in DB transactions.
- SnapshotEngine: dumps articles, categories, modules + related tables
(workflow_associations, tag maps, frontpage) to JSON
- SnapshotRestoreEngine: replace (clean slate) or merge (upsert) mode
- Full MVC: controller, models, view, template with create/restore modals
- New ACL permission: mokosuitebackup.snapshot.manage
- Submenu entry with camera icon, upgrade SQL for snapshots table
Improve full-site restore UI with confirmation modal offering options
for files, database, preserve config, and encryption password.
Config improvements:
- WebcronSecretField: CSPRNG generator, strength meter, rejects weak
patterns (password, admin, secret), enforces min 16 chars
- IpWhitelistField: table-based management, current IP detection with
one-click "Add my IP" button
- Default profile shows "Title (#ID)" format
- Default backup dir uses [DEFAULT_DIR] placeholder
- Install script generates random 32-char webcron secret
- Dashboard quick actions: full-width dropdown with button below
Static helper class for external consumers (bridge plugins, MCP servers)
to query backup status without bootstrapping the full component.
Methods:
- isInstalled(): check if MokoSuiteBackup is installed and enabled
- getLatestRecord(): get the most recent completed/failed backup
- getStatusSummary(): full heartbeat payload with latest status,
all-time/7-day totals, and consecutive success streak
Critical:
- Fix infinite recursion in getValidatedPrefix() — was calling itself
instead of extracting from $data array
- Fix SQL injection in actionResetAdmin() — prefix not validated,
now uses getValidatedPrefix()
High:
- Fix prefix abstraction to cover FK REFERENCES — str_replace now
targets backtick+prefix pattern to catch all table references in
CREATE TABLE output, not just the current table name
Medium:
- Security gate file write check — skip verification gracefully if
file cannot be written (don't lock user out)
- Stepped notification catch \Throwable instead of \Exception
Database prefix abstraction:
- DatabaseDumper uses #__ placeholder instead of live prefix in all
SQL output (DROP TABLE, CREATE TABLE, INSERT INTO)
- SteppedBackupEngine::dumpSingleTable() same #__ replacement
- DatabaseImporter replaces #__ with current site prefix on import
- MokoRestore replaces #__ with user-specified prefix on import
- Backups are now portable across sites with different prefixes
Stepped backup checksum:
- completeRecord() now computes and stores SHA-256 checksum
MokoRestore security gate:
- Writes .mokorestore-security.php with random 8-char code to site root
- User must read code from filesystem and enter it in browser
- Proves filesystem access before any restore actions are allowed
- Security file auto-deleted after successful verification
- All AJAX actions blocked until verification completes
Critical:
- Wrap cleanupOldBackups() in try-catch to prevent admin panel crash
- Add missing fields (total_size, files_count, etc.) to failure record
so failure notifications actually send
High:
- Log unlink failures in deleteBackupRecord() instead of silent return
- Wrap DB delete in try-catch so one failed record doesn't abort loop
- Check for ext-curl before calling curl_init() in sendNtfy()
Medium:
- Change runPreActionBackup catch from \Exception to \Throwable
- Log warning for skipped files during archive encryption
- Truncate ntfy response body in error logs (200 chars max)
2026-06-18 10:26:48 -05:00
227 changed files with 15984 additions and 2271 deletions
| 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}"
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
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the MokoGitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
echo "$RAW_URLS"
exit 1
fi
@@ -466,43 +570,33 @@ jobs:
name:Build RC Package
runs-on:ubuntu-latest
needs:[branch-policy, validate]
# Run only when both gates succeeded; always() forces evaluation so a skipped
# validate (e.g. template repos) skips this job cleanly instead of hanging.
- Use `margin-inline-end` instead of `margin-right` for RTL layout support
## [01.08.00] --- 2026-06-07
## [01.07.00] --- 2026-06-07
## [01.06.00] --- 2026-06-07
## [01.05.00] --- 2026-06-07
### Added
-Dashboard submenu entry as default landing page with `class:home` icon
-`[DEFAULT_DIR]` placeholder for portable backup directory configuration — resolves to `administrator/components/com_mokosuitebackup/backups` at runtime
- Live AJAX directory validation on backup_dir field — checks existence, writability, and placeholder resolution as user types (debounced 400ms)
-`checkDir` AJAX endpoint for real-time directory permission checking
- Web-accessible warning badge on backup download buttons when archive is inside web root
- Inline security warning in FolderPicker when default directory is selected
- Auto `.htaccess` and `index.html` protection for web-accessible backup directories on profile save and at backup time
- Font Awesome 6 submenu icons via CSS injection in `MokoSuiteBackupComponent::boot()`
-`syncMenuIcons()` installer postflight — syncs icon classes to `#__menu` on install and update
-`encryptionPassword` property on `SteppedSession` for upcoming stepped backup encryption support
-**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
-Profile `backup_dir` default changed from literal path to `[DEFAULT_DIR]` placeholder
- Backup engine fallback directory changed from hardcoded path to `[DEFAULT_DIR]`
-`isUsingDefaultBackupDir()` now matches `[DEFAULT_DIR]` placeholder in addition to literal path and empty values
- Dashboard submenu language key added to `.sys.ini` files (en-GB, en-US)
-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/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)
Full-site backup and restore for Joomla — database, files, and configuration.
## Overview
MokoSuiteBackup is a comprehensive backup solution for Joomla 4/5/6 sites. It creates complete site backups including the database, files, and configuration, packaged into downloadable ZIP archives. Supports multiple backup profiles, scheduled backups via CLI/cron, and a REST API for remote management.
BRIEF: Security vulnerability reporting and handling policy
-->
# Security Policy
## Purpose and Scope
This document defines the security vulnerability reporting, response, and disclosure policy for this Joomla Plugin template repository. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues.
## Supported Versions
Security updates are provided for the following versions:
| Version | Supported |
| ------- | ------------------ |
| 01.x.x | :white_check_mark: |
| < 01.0 | :x: |
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
## Reporting a Vulnerability
### Where to Report
**DO NOT** create public GitHub issues for security vulnerabilities.
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE_DESC="Allows users in this group to restore the site from a backup archive. This is a destructive operation that overwrites the current site."
COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD_DESC="Set a password to encrypt the backup archive with AES-256. Leave blank for no encryption. Required to restore encrypted backups."
COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD_DESC="AES-256 encryption password. Leave blank for no encryption. Required to restore."
COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC="Directory where backup archives are stored. Supports placeholders: [HOME] (user home directory), [host], [date], [year], [month], [day], [profile_name], [site_name], [type]. Use [HOME]/backups to store outside the web root. Absolute paths (starting with /) are used as-is; relative paths resolve from the Joomla root."
COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC="Where backups are stored. Use placeholders like [HOME]/backups for portability. Click the ? icon for full documentation."
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT="Archive Name Format"
COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="Include MokoRestore (standalone restore.php) inside the backup archive. Creates a self-contained package that can restore the site on a blank server without Joomla installed."
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT_DESC="Filename template (without extension). Click the placeholder buttons below to insert tokens."
COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="None: no restore script. Wrapped: bundled inside the ZIP. Standalone: separate restore.php file (ideal for remote servers)."
COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME_DESC="Custom filename for the restore script. Must end in .php. Use a non-obvious name to reduce discoverability on remote servers (e.g. moko-install-xyz.php)."
COM_MOKOJOOMBACKUP_FIELD_SANITIZE_PASSWORDS="Sanitize User Passwords"
COM_MOKOJOOMBACKUP_FIELD_SANITIZE_PASSWORDS_DESC="Replace password hashes with invalid values. Users must reset passwords after restore. For demos, staging, or GDPR."
COM_MOKOJOOMBACKUP_FIELD_PRESERVE_SUPER_ADMIN="Preserve Super Admin Password"
COM_MOKOJOOMBACKUP_FIELD_PRESERVE_SUPER_ADMIN_DESC="Keep the password for Super Users (group ID 8) intact. You will still be able to log in as a Super Admin after restoring."
COM_MOKOJOOMBACKUP_FIELD_SANITIZE_EMAILS="Sanitize User Emails"
COM_MOKOJOOMBACKUP_FIELD_SANITIZE_EMAILS_DESC="Replace emails with dummy values. Prevents accidental emails from cloned sites. Super admin preserved if enabled above."
COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS_DESC="Delete completed backups from this profile older than this many days. Set to 0 to use the global default from component options."
COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS_DESC="Delete completed backups from this profile older than this many days. Set to 0 for unlimited (keep by age disabled)."
COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT_DESC="Maximum number of completed backups to keep for this profile. Oldest are removed first. Set to 0 to use the global default from component options."
COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT_DESC="Maximum number of completed backups to keep for this profile. Oldest are removed first. Set to 0 for unlimited (keep by count disabled)."
COM_MOKOJOOMBACKUP_FIELD_NTFY_SPACER_DESC="<strong>Push Notifications (ntfy)</strong> — Send instant push notifications to your phone or desktop via <a href='https://ntfy.sh' target='_blank'>ntfy.sh</a> or a self-hosted ntfy server."
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC="ntfy Topic"
@@ -219,7 +270,35 @@ COM_MOKOJOOMBACKUP_VERIFY_FAILED="INTEGRITY CHECK FAILED — archive has been mo
COM_MOKOJOOMBACKUP_VERIFY_NO_CHECKSUM="No checksum stored for this backup. Only backups created after this update can be verified."
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC="Upload your SSH private key (id_rsa, id_ed25519). Stored base64-encoded in DB, written to temp file during upload only."
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE="Backup Before Extension Update"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE_DESC="Automatically run a full backup before any extension is updated. Uses the default profile. Throttled to once per 10 minutes to prevent duplicate backups during batch updates."
COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT="Auto-run on Update click"
COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT_DESC="When enabled, clicking the Update button on the Joomla Update or Extensions update page first pops the backup progress modal, runs the pre-update backup, then automatically continues the update. When disabled, a notice with a manual 'Back up now' button is shown instead."
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL="Backup Before Extension Uninstall"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL_DESC="Automatically run a full backup before any extension is uninstalled. Uses the default profile. Throttled to once per 10 minutes."
COM_MOKOJOOMBACKUP_CONFIG_SNAPSHOT_MAX_COUNT_DESC="Maximum number of content snapshots to keep. Oldest are removed first. Set to 0 for unlimited."
COM_MOKOJOOMBACKUP_CONFIG_SNAPSHOT_MAX_AGE="Max Snapshot Age (days)"
COM_MOKOJOOMBACKUP_CONFIG_SNAPSHOT_MAX_AGE_DESC="Delete snapshots older than this many days. Set to 0 for unlimited."
; Web Cron
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON="Web Cron"
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_ENABLED="Enable Web Cron"
@@ -296,9 +397,145 @@ COM_MOKOJOOMBACKUP_FIELD_NOTIFY_USER_GROUPS_DESC="Select Joomla user groups whos
; Dashboard warnings
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING_TITLE="Backup directory is inside the web root"
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING="One or more profiles store backups in the default directory inside the web root. This may expose backup archives if .htaccess is not supported. Move backups to a directory outside the web root for better security."
COM_MOKOJOOMBACKUP_DASHBOARD_LIVESITE_WARNING_TITLE="Site URL (live_site) is not set"
COM_MOKOJOOMBACKUP_DASHBOARD_LIVESITE_WARNING="Your Global Configuration has no Site URL (live_site). CLI and scheduled backups can't reliably determine the site's domain (archive names fall back to the server hostname), and some URLs may be generated incorrectly. Set Site URL in System → Global Configuration to your full site address including https://."
COM_MOKOJOOMBACKUP_WEB_ACCESSIBLE_WARNING="This backup is stored inside the web root and may be directly downloadable if .htaccess is not supported."
COM_MOKOJOOMBACKUP_RESTORE_PRESERVE_CONFIG="Preserve current configuration.php"
COM_MOKOJOOMBACKUP_RESTORE_PRESERVE_CONFIG_DESC="Keep your current database credentials and site paths. Recommended unless you know the backup has the correct credentials."
COM_MOKOJOOMBACKUP_RESTORE_PASSWORD_PLACEHOLDER="Leave blank if archive is not encrypted"
COM_MOKOJOOMBACKUP_SNAPSHOT_MODE_REPLACE_DESC="Remove all existing content of the selected types and replace with snapshot data. This gives you an exact copy of the snapshot state."
COM_MOKOJOOMBACKUP_SNAPSHOT_MODE_MERGE_DESC="Update existing items by ID and insert missing ones. Content added after the snapshot is preserved."
COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_TYPES="Types to restore"
COM_MOKOJOOMBACKUP_SNAPSHOT_REPLACE_WARNING="Replace mode will delete all current content of the selected types before restoring from the snapshot. This cannot be undone."
COM_MOKOSUITEBACKUP_ACTION_SNAPSHOT_MANAGE_DESC="Allows users in this group to create and restore content snapshots. Snapshots only affect articles, categories, and modules — not the full site."
; Webcron secret field
COM_MOKOJOOMBACKUP_WEBCRON_GENERATE="Generate"
COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_NONE="No secret set — webcron is disabled."
COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_SHORT="Too short — minimum %d characters required."
COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_WEAK="Weak — avoid common words like 'password', 'admin', 'secret'."
COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_OK="Acceptable — consider making it longer for better security."
COM_MOKOJOOMBACKUP_SNAPSHOT_NO_ARTICLES_SELECTED="No articles selected for restore."
; Purge
COM_MOKOJOOMBACKUP_TOOLBAR_PURGE="Purge Old Backups"
COM_MOKOJOOMBACKUP_PURGE_TITLE="Purge Old Backups"
COM_MOKOJOOMBACKUP_PURGE_DESC="Delete all completed backup records older than the selected date. This permanently removes archive files, log files, and database records."
COM_MOKOJOOMBACKUP_PURGE_DATE_LABEL="Delete all backups before this date"
COM_MOKOJOOMBACKUP_PURGE_SUBMIT="Purge Backups"
COM_MOKOJOOMBACKUP_PURGE_CONFIRM="Are you sure? This action cannot be undone."
COM_MOKOJOOMBACKUP_PURGE_COUNT_MSG="This will permanently delete %d backup(s) and their archive files."
COM_MOKOJOOMBACKUP_PURGE_NONE_FOUND="No completed backups found before the selected date."
COM_MOKOJOOMBACKUP_PURGE_INVALID_DATE="Invalid date. Please select a valid date."
COM_MOKOJOOMBACKUP_REMOTE_NONE_CONFIGURED="No remote destinations configured. Use 'Add Destination' to send backups to SFTP, S3, or Google Drive."
COM_MOKOJOOMBACKUP_REMOTE_LEGACY_NOTE="Legacy single-remote fields below are hidden when remote destinations are configured above. Existing legacy settings continue to work as a fallback."
COM_MOKOJOOMBACKUP_REMOTE_DELETE_CONFIRM="Are you sure you want to delete this remote destination?"
; Errors
COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND="Backup archive file not found or has been deleted."
COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED="No backup record selected for restore."
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE_DESC="Allows users in this group to restore the site from a backup archive. This is a destructive operation that overwrites the current site."
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE="Backup Before Extension Update"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE_DESC="Automatically run a full backup before any extension is updated. Uses the default profile. Throttled to once per 10 minutes to prevent duplicate backups during batch updates."
COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT="Auto-run on Update click"
COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT_DESC="When enabled, clicking the Update button on the Joomla Update or Extensions update page first pops the backup progress modal, runs the pre-update backup, then automatically continues the update. When disabled, a notice with a manual 'Back up now' button is shown instead."
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL="Backup Before Extension Uninstall"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL_DESC="Automatically run a full backup before any extension is uninstalled. Uses the default profile. Throttled to once per 10 minutes."
COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE="Max Backup Age (days)"
COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE_DESC="Default maximum age for backup records. Used by the system plugin and CLI cleanup command."
@@ -68,6 +88,8 @@ COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND="Directory not found"
COM_MOKOJOOMBACKUP_BACKUP_DIR_DEFAULT="Default (inside web root)"
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING_TITLE="Backup directory is inside the web root"
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING="One or more profiles store backups in the default directory inside the web root. This may expose backup archives if .htaccess is not supported. Move backups to a directory outside the web root for better security."
COM_MOKOJOOMBACKUP_DASHBOARD_LIVESITE_WARNING_TITLE="Site URL (live_site) is not set"
COM_MOKOJOOMBACKUP_DASHBOARD_LIVESITE_WARNING="Your Global Configuration has no Site URL (live_site). CLI and scheduled backups can't reliably determine the site's domain (archive names fall back to the server hostname), and some URLs may be generated incorrectly. Set Site URL in System → Global Configuration to your full site address including https://."
COM_MOKOJOOMBACKUP_WEB_ACCESSIBLE_WARNING="This backup is stored inside the web root and may be directly downloadable if .htaccess is not supported."
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_USER_GROUPS="Notify User Groups"
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_USER_GROUPS_DESC="Select Joomla user groups whose members will receive backup notifications. Combined with email addresses above."
; Purge
COM_MOKOJOOMBACKUP_TOOLBAR_PURGE="Purge Old Backups"
COM_MOKOJOOMBACKUP_PURGE_TITLE="Purge Old Backups"
COM_MOKOJOOMBACKUP_PURGE_DESC="Delete all completed backup records older than the selected date. This permanently removes archive files, log files, and database records."
COM_MOKOJOOMBACKUP_PURGE_DATE_LABEL="Delete all backups before this date"
COM_MOKOJOOMBACKUP_PURGE_SUBMIT="Purge Backups"
COM_MOKOJOOMBACKUP_PURGE_CONFIRM="Are you sure? This action cannot be undone."
COM_MOKOJOOMBACKUP_PURGE_COUNT_MSG="This will permanently delete %d backup(s) and their archive files."
COM_MOKOJOOMBACKUP_PURGE_NONE_FOUND="No completed backups found before the selected date."
COM_MOKOJOOMBACKUP_PURGE_INVALID_DATE="Invalid date. Please select a valid date."
COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS_DESC="Delete completed backups from this profile older than this many days. Set to 0 for unlimited (keep by age disabled)."
COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT_DESC="Maximum number of completed backups to keep for this profile. Oldest are removed first. Set to 0 for unlimited (keep by count disabled)."
ALTERTABLE`#__mokosuitebackup_profiles`ADDCOLUMN`notify_user_groups`VARCHAR(255)NOTNULLDEFAULT''COMMENT'Comma-separated Joomla user group IDs'AFTER`notify_email`;
-- Add archive_name_format column with placeholder support
ALTERTABLE`#__mokosuitebackup_profiles`ADDCOLUMN`archive_name_format`VARCHAR(512)NOTNULLDEFAULT'[host]_[datetime]_profile[profile_id]'COMMENT'Filename format with placeholders'AFTER`backup_dir`;
ALTERTABLE`#__mokosuitebackup_profiles`ADDCOLUMN`archive_name_format`VARCHAR(512)NOTNULLDEFAULT'[HOST]_[DATETIME]_profile[PROFILE_ID]'COMMENT'Filename format with placeholders'AFTER`backup_dir`;
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
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.