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)
SteppedBackupEngine now sends email + ntfy notifications on both
success (completeRecord) and failure (failRecord). Previously only
BackupEngine (synchronous CLI/toolbar path) sent notifications.
Download link in backups template now includes the CSRF token in
the URL query string, fixing "security token did not match" error
when clicking download buttons.
The update object passed to NotificationSender only had fields
being updated in the DB (total_size, checksum, etc). It was missing
backup_type, archivename, description, origin, and backupstart —
which are set on the initial insert and don't change. This caused
ntfy notifications to show empty Type and Archive fields.
BackupEngine: check ext-zip, ext-pdo, ext-pdo_mysql, ext-mbstring
before running (was only zip + mbstring).
Installer preflight: warn about missing extensions (zip, pdo,
pdo_mysql, mbstring, curl) during install/update. Warns but does
not block installation so the component can still be configured.
MokoRestore already checks ext-zip, ext-pdo_mysql, ext-mbstring,
ext-json in its preflight step.
composer.json already declares all six extensions as requirements
(zip, pdo, pdo_mysql, curl, ftp, mbstring) — composer install
fails if any are missing, which CI enforces.
Closes#22
Each profile can now set its own retention_days and retention_count.
A value of 0 means use the global default from component options.
Cleanup logic refactored to iterate per-profile with individual
retention thresholds. Also cleans up orphaned records where the
parent profile was deleted. Log files alongside archives are now
removed during cleanup.
Extracted deleteBackupRecord() helper for consistent file+DB cleanup.
Critical/High:
- Fix undefined $configFile → $configPath in from-scratch config path
- Escape all user input with addcslashes before interpolating into
configuration.php (both regex-replace and HEREDOC paths)
- Add getValidatedPrefix() helper — validates db_prefix format before
use in SQL table names across all restore functions
- fixPackageClientId() now warns user via enqueueMessage on failure
- sanitizeConfiguration() logs error on file read failure
Medium:
- Content-Disposition header uses RFC 6266 rawurlencode (both admin
and API download controllers)
- Remove @unlink suppression, log warning on failure
- viewLog() catch block now logs exception context
- writeDefaultHtaccess() checks copy/write, returns status to caller
- actionConfig() checks file_put_contents return value
Critical:
- Fix garbled getDbConnection() in MokoRestore — duplicated lines and
broken regex causing parse errors in the standalone restore script
High:
- fixPackageClientId() now warns user via enqueueMessage on failure
- sanitizeConfiguration() logs error when file read fails
- actionConfig() checks file_put_contents return value on both paths
- writeDefaultHtaccess() returns status string, checks copy and write,
callers append warnings to response message
Medium:
- Remove @unlink suppression before archive rename, log warning
- viewLog() catch block now logs exception message for diagnostics