From 68605ffc0559ef9a7965aedb00b07ce57a6f724e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 13:22:44 -0500 Subject: [PATCH 1/4] refactor(remote): remove legacy single-remote storage in favor of remotes table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../com_mokosuitebackup/forms/profile.xml | 247 ------------------ .../language/en-US/com_mokosuitebackup.ini | 4 + .../com_mokosuitebackup/sql/install.mysql.sql | 26 -- .../sql/updates/mysql/02.52.25.sql | 27 ++ .../src/Engine/BackupEngine.php | 89 +------ .../src/Engine/PreflightCheck.php | 101 +++---- .../src/Engine/SteppedBackupEngine.php | 190 ++++---------- .../src/Engine/SteppedSession.php | 1 - .../com_mokosuitebackup/tmpl/profile/edit.php | 31 +-- source/script.php | 46 +--- 10 files changed, 167 insertions(+), 595 deletions(-) create mode 100644 source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql diff --git a/source/packages/com_mokosuitebackup/forms/profile.xml b/source/packages/com_mokosuitebackup/forms/profile.xml index 81a46de..d7fa692 100644 --- a/source/packages/com_mokosuitebackup/forms/profile.xml +++ b/source/packages/com_mokosuitebackup/forms/profile.xml @@ -206,25 +206,6 @@
- - - - - - - JYES - - - - - - - - - - - - - -
@@ -408,157 +314,4 @@ />
-
- - - - - - - - - - - - - -
- -
- - - - -
- -
- - - - - - -
diff --git a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini index db5e85b..fc50e39 100644 --- a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini @@ -126,6 +126,10 @@ COM_MOKOJOOMBACKUP_CANCEL_SUCCESS="%d stalled backup(s) cancelled." ; Backup status COM_MOKOJOOMBACKUP_STATUS_WARNING="Warning" +; Delete feedback +COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED="%d backup records deleted." +COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED_1="%d backup record deleted." + ; ACL - Cancel COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL="Cancel Stalled Backup" COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL_DESC="Allows users to cancel backup records stuck in running status and clean up partial archive files." diff --git a/source/packages/com_mokosuitebackup/sql/install.mysql.sql b/source/packages/com_mokosuitebackup/sql/install.mysql.sql index 705ebfd..8b16d99 100644 --- a/source/packages/com_mokosuitebackup/sql/install.mysql.sql +++ b/source/packages/com_mokosuitebackup/sql/install.mysql.sql @@ -11,32 +11,6 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` ( `exclude_dirs` TEXT NOT NULL COMMENT 'Newline-separated directory paths to exclude', `exclude_files` TEXT NOT NULL COMMENT 'Newline-separated filename patterns to exclude', `exclude_tables` TEXT NOT NULL COMMENT 'Newline-separated table names to exclude', - `remote_storage` VARCHAR(20) NOT NULL DEFAULT 'none' COMMENT 'none, ftp, google_drive, s3', - `ftp_host` VARCHAR(255) NOT NULL DEFAULT '', - `ftp_port` INT(5) UNSIGNED NOT NULL DEFAULT 21, - `ftp_username` VARCHAR(255) NOT NULL DEFAULT '', - `ftp_password` VARCHAR(255) NOT NULL DEFAULT '', - `ftp_path` VARCHAR(512) NOT NULL DEFAULT '/backups', - `ftp_passive` TINYINT(1) NOT NULL DEFAULT 1, - `ftp_ssl` TINYINT(1) NOT NULL DEFAULT 0, - `sftp_host` VARCHAR(255) NOT NULL DEFAULT '', - `sftp_port` INT(5) UNSIGNED NOT NULL DEFAULT 22, - `sftp_username` VARCHAR(255) NOT NULL DEFAULT '', - `sftp_auth_type` VARCHAR(20) NOT NULL DEFAULT 'key', - `sftp_password` VARCHAR(255) NOT NULL DEFAULT '', - `sftp_key_data` MEDIUMTEXT, - `sftp_passphrase` VARCHAR(255) NOT NULL DEFAULT '', - `sftp_path` VARCHAR(512) NOT NULL DEFAULT '/backups', - `gdrive_client_id` VARCHAR(255) NOT NULL DEFAULT '', - `gdrive_client_secret` VARCHAR(255) NOT NULL DEFAULT '', - `gdrive_refresh_token` VARCHAR(512) NOT NULL DEFAULT '', - `gdrive_folder_id` VARCHAR(255) NOT NULL DEFAULT '', - `s3_endpoint` VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'S3 endpoint URL (blank = AWS default)', - `s3_region` VARCHAR(50) NOT NULL DEFAULT 'us-east-1', - `s3_access_key` VARCHAR(255) NOT NULL DEFAULT '', - `s3_secret_key` VARCHAR(255) NOT NULL DEFAULT '', - `s3_bucket` VARCHAR(255) NOT NULL DEFAULT '', - `s3_path` VARCHAR(512) NOT NULL DEFAULT '/backups', `remote_keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload', `encryption_password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'AES-256 archive encryption password (blank = no encryption)', `include_mokorestore` VARCHAR(20) NOT NULL DEFAULT '0' COMMENT 'MokoRestore mode: 0=none, 1=wrapped, standalone', diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql new file mode 100644 index 0000000..0551d9d --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql @@ -0,0 +1,27 @@ +ALTER TABLE `#__mokosuitebackup_profiles` + DROP COLUMN IF EXISTS `remote_storage`, + DROP COLUMN IF EXISTS `ftp_host`, + DROP COLUMN IF EXISTS `ftp_port`, + DROP COLUMN IF EXISTS `ftp_username`, + DROP COLUMN IF EXISTS `ftp_password`, + DROP COLUMN IF EXISTS `ftp_path`, + DROP COLUMN IF EXISTS `ftp_passive`, + DROP COLUMN IF EXISTS `ftp_ssl`, + DROP COLUMN IF EXISTS `sftp_host`, + DROP COLUMN IF EXISTS `sftp_port`, + DROP COLUMN IF EXISTS `sftp_username`, + DROP COLUMN IF EXISTS `sftp_auth_type`, + DROP COLUMN IF EXISTS `sftp_password`, + DROP COLUMN IF EXISTS `sftp_key_data`, + DROP COLUMN IF EXISTS `sftp_passphrase`, + DROP COLUMN IF EXISTS `sftp_path`, + DROP COLUMN IF EXISTS `gdrive_client_id`, + DROP COLUMN IF EXISTS `gdrive_client_secret`, + DROP COLUMN IF EXISTS `gdrive_refresh_token`, + DROP COLUMN IF EXISTS `gdrive_folder_id`, + DROP COLUMN IF EXISTS `s3_endpoint`, + DROP COLUMN IF EXISTS `s3_region`, + DROP COLUMN IF EXISTS `s3_access_key`, + DROP COLUMN IF EXISTS `s3_secret_key`, + DROP COLUMN IF EXISTS `s3_bucket`, + DROP COLUMN IF EXISTS `s3_path`; diff --git a/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php b/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php index 04b8f33..b0a8c92 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php @@ -321,48 +321,6 @@ class BackupEngine @unlink($archivePath); $this->log('Local copy removed (remote_keep_local = off)'); } - } else { - /* Backward-compat: fall back to legacy single-remote column */ - $remoteStorage = $profile->remote_storage ?? 'none'; - - if ($remoteStorage !== 'none') { - try { - $this->log('Starting remote upload (' . $remoteStorage . ')...'); - $uploader = $this->createUploader($remoteStorage, $profile); - $uploadResult = $uploader->upload($archivePath, $archiveName); - - if ($uploadResult['success']) { - $remoteFilename = $uploadResult['remote_path'] ?? $archiveName; - $this->log('Remote upload complete: ' . $uploadResult['message']); - - if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) { - $restoreBasename = basename($restoreScriptPath); - $this->log('Uploading standalone ' . $restoreBasename . '...'); - $restoreUpload = $uploader->upload($restoreScriptPath, $restoreBasename); - - if ($restoreUpload['success']) { - $this->log('Standalone ' . $restoreBasename . ' uploaded'); - } else { - $this->log('WARNING: ' . $restoreBasename . ' upload failed: ' . $restoreUpload['message']); - } - } - - // Delete local copy if configured - if (empty($profile->remote_keep_local) && is_file($archivePath)) { - @unlink($archivePath); - $this->log('Local copy removed (remote_keep_local = off)'); - } - } else { - $uploadFailed = true; - $this->log('WARNING: Remote upload failed: ' . $uploadResult['message']); - $this->log('Local backup is preserved.'); - } - } catch (\Throwable $e) { - $uploadFailed = true; - $this->log('WARNING: Remote upload threw an exception: ' . $e->getMessage()); - $this->log('Local backup is preserved.'); - } - } } // Write log file alongside the archive @@ -519,23 +477,7 @@ class BackupEngine } /** - * Create the appropriate remote uploader based on the storage type. - * Legacy method — used by backward-compat fallback when remotes table - * does not exist. - */ - private function createUploader(string $type, object $profile): RemoteUploaderInterface - { - return match ($type) { - 'ftp' => new FtpUploader($profile), - 'sftp' => new SftpUploader($profile), - 'google_drive' => new GoogleDriveUploader($profile), - 's3' => new S3Uploader($profile), - default => throw new \InvalidArgumentException('Unknown remote storage type: ' . $type), - }; - } - - /** - * Create a remote uploader from JSON params (multi-remote destinations). + * Create a remote uploader from JSON params. * * Builds a fake profile-like object from the params array so the existing * uploader constructors work without modification. @@ -569,31 +511,18 @@ class BackupEngine /** * Load enabled remote destinations for a profile from the remotes table. - * - * Returns an empty array when the table does not exist (pre-migration) - * so the caller can fall back to the legacy single-remote column. - * - * @param object $db Database driver - * @param int $profileId Profile ID - * - * @return object[] Array of remote destination rows */ private function loadRemoteDestinations(object $db, int $profileId): array { - try { - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuitebackup_remotes')) - ->where($db->quoteName('profile_id') . ' = ' . (int) $profileId) - ->where($db->quoteName('enabled') . ' = 1') - ->order($db->quoteName('ordering') . ' ASC'); - $db->setQuery($query); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitebackup_remotes')) + ->where($db->quoteName('profile_id') . ' = ' . (int) $profileId) + ->where($db->quoteName('enabled') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC'); + $db->setQuery($query); - return $db->loadObjectList() ?: []; - } catch (\Throwable $e) { - // Table does not exist yet (pre-migration) — fall back to legacy - return []; - } + return $db->loadObjectList() ?: []; } /** diff --git a/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php b/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php index 664a3e8..6ccbd90 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php +++ b/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php @@ -77,7 +77,7 @@ class PreflightCheck $this->checkDiskSpace($profile, $db); $this->checkRunningBackup($profile, $db); $this->checkExcludedTables($profile, $db); - $this->checkRemoteCredentials($profile); + $this->checkRemoteCredentials($profile, $db); return $this->result(); } @@ -102,12 +102,8 @@ class PreflightCheck } } - // curl is only needed for remote upload and ntfy notifications - $needsCurl = ($profile->remote_storage ?? 'none') !== 'none' - || !empty($profile->ntfy_topic); - - if ($needsCurl && !extension_loaded('curl')) { - $this->warnings[] = 'ext-curl is not loaded — remote upload and ntfy notifications will not work'; + if (!empty($profile->ntfy_topic) && !extension_loaded('curl')) { + $this->warnings[] = 'ext-curl is not loaded — ntfy notifications will not work'; } } @@ -280,65 +276,76 @@ class PreflightCheck } /** - * Check that remote storage credentials are minimally configured. + * Check that remote destination credentials are minimally configured. * Does not test the actual connection (too slow for preflight). */ - private function checkRemoteCredentials(object $profile): void + private function checkRemoteCredentials(object $profile, object $db): void { - $remote = $profile->remote_storage ?? 'none'; + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitebackup_remotes')) + ->where($db->quoteName('profile_id') . ' = ' . (int) $profile->id) + ->where($db->quoteName('enabled') . ' = 1'); + $db->setQuery($query); + $remotes = $db->loadObjectList(); - if ($remote === 'none') { + if (empty($remotes)) { return; } - switch ($remote) { - case 'ftp': - if (empty($profile->ftp_host)) { - $this->warnings[] = 'FTP host is not configured — remote upload will fail'; - } + foreach ($remotes as $remote) { + $params = json_decode($remote->params, true) ?: []; + $label = $remote->title ?: ('Remote #' . $remote->id); - if (empty($profile->ftp_username)) { - $this->warnings[] = 'FTP username is not configured — remote upload will fail'; - } + switch ($remote->type) { + case 'ftp': + if (empty($params['host'])) { + $this->warnings[] = $label . ': FTP host is not configured — upload will fail'; + } - break; + if (empty($params['username'])) { + $this->warnings[] = $label . ': FTP username is not configured — upload will fail'; + } - case 's3': - if (empty($profile->s3_bucket)) { - $this->warnings[] = 'S3 bucket is not configured — remote upload will fail'; - } + break; - if (empty($profile->s3_access_key) || empty($profile->s3_secret_key)) { - $this->warnings[] = 'S3 credentials are not configured — remote upload will fail'; - } + case 's3': + if (empty($params['bucket'])) { + $this->warnings[] = $label . ': S3 bucket is not configured — upload will fail'; + } - break; + if (empty($params['access_key']) || empty($params['secret_key'])) { + $this->warnings[] = $label . ': S3 credentials are not configured — upload will fail'; + } - case 'sftp': - if (empty($profile->sftp_host)) { - $this->warnings[] = 'SFTP host is not configured — remote upload will fail'; - } + break; - if (empty($profile->sftp_username)) { - $this->warnings[] = 'SFTP username is not configured — remote upload will fail'; - } + case 'sftp': + if (empty($params['host'])) { + $this->warnings[] = $label . ': SFTP host is not configured — upload will fail'; + } - if (empty($profile->sftp_key_data) && empty($profile->sftp_password)) { - $this->warnings[] = 'SFTP requires either a private key or password — remote upload will fail'; - } + if (empty($params['username'])) { + $this->warnings[] = $label . ': SFTP username is not configured — upload will fail'; + } - break; + if (empty($params['key_data']) && empty($params['password'])) { + $this->warnings[] = $label . ': SFTP requires either a private key or password — upload will fail'; + } - case 'google_drive': - if (empty($profile->gdrive_client_id) || empty($profile->gdrive_client_secret)) { - $this->warnings[] = 'Google Drive OAuth credentials are not configured — remote upload will fail'; - } + break; - if (empty($profile->gdrive_refresh_token)) { - $this->warnings[] = 'Google Drive refresh token is missing — remote upload will fail'; - } + case 'google_drive': + if (empty($params['client_id']) || empty($params['client_secret'])) { + $this->warnings[] = $label . ': Google Drive OAuth credentials are not configured — upload will fail'; + } - break; + if (empty($params['refresh_token'])) { + $this->warnings[] = $label . ': Google Drive refresh token is missing — upload will fail'; + } + + break; + } } } diff --git a/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php b/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php index ee640e7..467b616 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php @@ -69,7 +69,6 @@ class SteppedBackupEngine $session->excludeFiles = BackupDirectory::parseNewlineList($profile->exclude_files ?? ''); $session->excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? ''); $session->backupDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER; - $session->remoteStorage = $profile->remote_storage ?? 'none'; $session->includeMokoRestore = $profile->include_mokorestore ?? '0'; $session->restoreScriptName = $profile->restore_script_name ?? 'restore.php'; $session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true); @@ -153,15 +152,8 @@ class SteppedBackupEngine $totalSteps += 1; // finalize step - // Determine upload step count: one step per remote destination, - // or one step for legacy single-remote, or zero if no remotes. $remoteCount = count($session->remoteDestinations); - - if ($remoteCount > 0) { - $totalSteps += $remoteCount; - } elseif ($session->remoteStorage !== 'none') { - $totalSteps += 1; - } + $totalSteps += $remoteCount; $session->totalSteps = $totalSteps; $session->currentStep = 1; @@ -421,11 +413,7 @@ class SteppedBackupEngine $session->currentStep++; - // Determine next phase: multi-remote, legacy single-remote, or complete - $hasMultiRemote = !empty($session->remoteDestinations); - $hasLegacyRemote = $session->remoteStorage !== 'none'; - - if ($hasMultiRemote || $hasLegacyRemote) { + if (!empty($session->remoteDestinations)) { $session->phase = 'upload'; } else { $session->phase = 'complete'; @@ -440,11 +428,7 @@ class SteppedBackupEngine } /** - * Upload phase: send archive to remote storage. - * - * When multi-remote destinations are configured, each call uploads to - * one destination (one step per remote). When only the legacy - * single-remote column is set, uploads in a single step. + * Upload phase: send archive to one remote destination per call. */ private function stepUpload(SteppedSession $session): void { @@ -452,133 +436,65 @@ class SteppedBackupEngine $remoteFilename = ''; $uploadFailed = false; - if (!empty($session->remoteDestinations)) { - // ── Multi-remote path ────────────────────────────────── - $index = $session->remoteIndex; + $index = $session->remoteIndex; - if ($index >= count($session->remoteDestinations)) { - // All remotes processed — move to complete - $session->phase = 'complete'; - $session->statusMessage = 'All remote uploads finished'; - $this->completeRecord($session); + if ($index >= count($session->remoteDestinations)) { + $session->phase = 'complete'; + $session->statusMessage = 'All remote uploads finished'; + $this->completeRecord($session); - return; - } + return; + } - $remote = (object) $session->remoteDestinations[$index]; + $remote = (object) $session->remoteDestinations[$index]; - try { - $title = $remote->title ?? ('Remote #' . ($index + 1)); - $type = $remote->type ?? 'unknown'; - $params = json_decode($remote->params ?? '{}', true) ?: []; + try { + $title = $remote->title ?? ('Remote #' . ($index + 1)); + $type = $remote->type ?? 'unknown'; + $params = json_decode($remote->params ?? '{}', true) ?: []; - $session->log('Uploading to: ' . $title . ' (' . $type . ')...'); - $uploader = $this->createUploaderFromParams($type, $params); - $result = $uploader->upload($session->archivePath, $session->archiveName); + $session->log('Uploading to: ' . $title . ' (' . $type . ')...'); + $uploader = $this->createUploaderFromParams($type, $params); + $result = $uploader->upload($session->archivePath, $session->archiveName); - if ($result['success']) { - $remoteFilename = $result['remote_path'] ?? $session->archiveName; - $session->log(' Upload complete: ' . $result['message']); + if ($result['success']) { + $remoteFilename = $result['remote_path'] ?? $session->archiveName; + $session->log(' Upload complete: ' . $result['message']); - if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) { - $uploader->upload($session->restoreScriptPath, basename($session->restoreScriptPath)); - } - } else { - $uploadFailed = true; - $session->log(' WARNING: Upload failed: ' . $result['message']); + if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) { + $uploader->upload($session->restoreScriptPath, basename($session->restoreScriptPath)); } - } catch (\Throwable $e) { + } else { $uploadFailed = true; - $session->log(' WARNING: Upload exception: ' . $e->getMessage()); + $session->log(' WARNING: Upload failed: ' . $result['message']); + } + } catch (\Throwable $e) { + $uploadFailed = true; + $session->log(' WARNING: Upload exception: ' . $e->getMessage()); + } + + $session->remoteIndex++; + $session->currentStep++; + + $remaining = count($session->remoteDestinations) - $session->remoteIndex; + $session->statusMessage = 'Uploaded to ' . ($remote->title ?? 'remote') . ($remaining > 0 ? ' (' . $remaining . ' remaining)' : ''); + + if ($session->remoteIndex >= count($session->remoteDestinations)) { + if (!$uploadFailed && !$session->remoteKeepLocal && is_file($session->archivePath)) { + @unlink($session->archivePath); + $session->log('Local copy removed (remote_keep_local = off)'); } - $session->remoteIndex++; - $session->currentStep++; - - $remaining = count($session->remoteDestinations) - $session->remoteIndex; - $session->statusMessage = 'Uploaded to ' . ($remote->title ?? 'remote') . ($remaining > 0 ? ' (' . $remaining . ' remaining)' : ''); - - if ($session->remoteIndex >= count($session->remoteDestinations)) { - // All remotes done — delete local if configured and no failures - if (!$uploadFailed && !$session->remoteKeepLocal && is_file($session->archivePath)) { - @unlink($session->archivePath); - $session->log('Local copy removed (remote_keep_local = off)'); - } - - // Update record with remote filename - $update = (object) [ - 'id' => $session->recordId, - 'remote_filename' => $remoteFilename, - 'filesexist' => is_file($session->archivePath) ? 1 : 0, - ]; - $db->updateObject('#__mokosuitebackup_records', $update, 'id'); - - $session->phase = 'complete'; - $session->statusMessage = $uploadFailed - ? 'Backup complete (some remote uploads failed — local archive preserved)' - : 'Backup complete'; - $this->completeRecord($session, $uploadFailed); - } - } else { - // ── Legacy single-remote fallback ────────────────────── - try { - // Reload profile for remote settings - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuitebackup_profiles')) - ->where($db->quoteName('id') . ' = ' . $session->profileId); - $db->setQuery($query); - $profile = $db->loadObject(); - - $uploader = match ($session->remoteStorage) { - 'ftp' => new FtpUploader($profile), - 'sftp' => new SftpUploader($profile), - 'google_drive' => new GoogleDriveUploader($profile), - 's3' => new S3Uploader($profile), - default => throw new \InvalidArgumentException('Unknown storage: ' . $session->remoteStorage), - }; - - $session->log('Starting remote upload (' . $session->remoteStorage . ')...'); - $result = $uploader->upload($session->archivePath, $session->archiveName); - - if ($result['success']) { - $remoteFilename = $result['remote_path'] ?? $session->archiveName; - $session->log('Remote upload complete: ' . $result['message']); - - if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) { - $restoreBasename = basename($session->restoreScriptPath); - $session->log('Uploading standalone ' . $restoreBasename . '...'); - $uploader->upload($session->restoreScriptPath, $restoreBasename); - } - - if (!$session->remoteKeepLocal && is_file($session->archivePath)) { - @unlink($session->archivePath); - $session->log('Local copy removed'); - } - } else { - $uploadFailed = true; - $session->log('WARNING: Remote upload failed: ' . $result['message']); - $session->log('Local backup is preserved.'); - } - } catch (\Throwable $e) { - $uploadFailed = true; - $session->log('WARNING: Remote upload threw an exception: ' . $e->getMessage()); - $session->log('Local backup is preserved.'); - } - - // Update record with remote filename $update = (object) [ 'id' => $session->recordId, 'remote_filename' => $remoteFilename, 'filesexist' => is_file($session->archivePath) ? 1 : 0, ]; - $db->updateObject('#__mokosuitebackup_records', $update, 'id'); - $session->currentStep++; $session->phase = 'complete'; $session->statusMessage = $uploadFailed - ? 'Backup complete (remote upload failed — local archive preserved)' + ? 'Backup complete (some remote uploads failed — local archive preserved)' : 'Backup complete'; $this->completeRecord($session, $uploadFailed); } @@ -859,21 +775,15 @@ class SteppedBackupEngine */ private function loadRemoteDestinations(object $db, int $profileId): array { - try { - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuitebackup_remotes')) - ->where($db->quoteName('profile_id') . ' = ' . (int) $profileId) - ->where($db->quoteName('enabled') . ' = 1') - ->order($db->quoteName('ordering') . ' ASC'); - $db->setQuery($query); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitebackup_remotes')) + ->where($db->quoteName('profile_id') . ' = ' . (int) $profileId) + ->where($db->quoteName('enabled') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC'); + $db->setQuery($query); - // Use loadAssocList so the data survives JSON serialization in SteppedSession - return $db->loadAssocList() ?: []; - } catch (\Throwable $e) { - // Table does not exist yet (pre-migration) — fall back to legacy - return []; - } + return $db->loadAssocList() ?: []; } /** diff --git a/source/packages/com_mokosuitebackup/src/Engine/SteppedSession.php b/source/packages/com_mokosuitebackup/src/Engine/SteppedSession.php index c153c52..3a103b3 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/SteppedSession.php +++ b/source/packages/com_mokosuitebackup/src/Engine/SteppedSession.php @@ -50,7 +50,6 @@ class SteppedSession public array $excludeDirs = []; public array $excludeFiles = []; public array $excludeTables = []; - public string $remoteStorage = 'none'; public string $includeMokoRestore = '0'; public string $restoreScriptName = 'restore.php'; public string $restoreScriptPath = ''; diff --git a/source/packages/com_mokosuitebackup/tmpl/profile/edit.php b/source/packages/com_mokosuitebackup/tmpl/profile/edit.php index 1f6e636..779a212 100644 --- a/source/packages/com_mokosuitebackup/tmpl/profile/edit.php +++ b/source/packages/com_mokosuitebackup/tmpl/profile/edit.php @@ -65,7 +65,6 @@ $token = Session::getFormToken();
-
@@ -97,20 +96,13 @@ $token = Session::getFormToken();

-
+ +
+ +
- -
- - form->renderFieldset('remote'); ?> - form->renderFieldset('ftp'); ?> - form->renderFieldset('google_drive'); ?> - form->renderFieldset('s3'); ?> -
+ form->renderFieldset('remote'); ?>
@@ -280,9 +272,8 @@ document.addEventListener('DOMContentLoaded', function() { const tbody = document.getElementById('remoteDestBody'); const emptyMsg = document.getElementById('remoteDestEmpty'); const loadingTr = document.getElementById('remoteDestLoading'); - const legacy = document.getElementById('legacyRemoteFields'); - const legacyNote = document.getElementById('legacyRemoteNote'); - const modal = new bootstrap.Modal(document.getElementById('remoteModal')); + const modalEl = document.getElementById('remoteModal'); + const modal = bootstrap.Modal.getOrCreateInstance(modalEl); // Type badge colours const typeBadge = {sftp: 'bg-primary', s3: 'bg-warning text-dark', google_drive: 'bg-success'}; @@ -336,14 +327,10 @@ document.addEventListener('DOMContentLoaded', function() { if (!remotesData.length) { emptyMsg.style.display = ''; - legacy.style.display = ''; - legacyNote.style.display = 'none'; return; } emptyMsg.style.display = 'none'; - legacy.style.display = 'none'; - legacyNote.style.display = 'block'; remotesData.forEach(function(item) { const tr = document.createElement('tr'); @@ -506,8 +493,8 @@ document.addEventListener('DOMContentLoaded', function() { const fields = configFields[item.type] || []; fields.forEach(function(f) { const el = document.getElementById('remoteCfg_' + prefix + f); - if (el && item.config && item.config[f] !== undefined) { - el.value = item.config[f]; + if (el && item.params && item.params[f] !== undefined) { + el.value = item.params[f]; } }); } diff --git a/source/script.php b/source/script.php index 67668df..b3d47fc 100644 --- a/source/script.php +++ b/source/script.php @@ -665,41 +665,23 @@ class Pkg_MokoSuiteBackupInstallerScript { try { - $db = Factory::getDbo(); - $db->setQuery( - $db->getQuery(true) - ->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')]) - ->from($db->quoteName('#__update_sites')) - ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteBackup%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteBackup%') . ')') - ->setLimit(1) - ); - $site = $db->loadObject(); - - if ($site) - { - $eq = (string) ($site->extra_query ?? ''); - if (!empty($eq) && strpos($eq, 'dlid=') !== false) { parse_str($eq, $p); if (!empty($p['dlid'])) { return; } } - $editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id; - } - else - { - $editUrl = 'index.php?option=com_installer&view=updatesites'; - } - Factory::getApplication()->enqueueMessage( - 'Moko Consulting License Key Required — ' - . 'No download key is configured. Updates will not be available until a valid license key is entered. ' - . 'Enter License Key', - 'warning' + '

MokoSuiteBackup installed successfully!

', + 'info' + ); + + // Show post-install license key prompt + Factory::getApplication()->enqueueMessage( + 'Moko Consulting License Key Required
' + . 'A download key (DLID) is required to receive updates. ' + . 'Enter your key in the Update Sites manager ' + . 'or contact Moko Consulting Support to obtain one.', + 'warning' ); } - catch (\Exception $e) { - error_log('MokoSuiteBackup: License key check failed: ' . $e->getMessage()); - Factory::getApplication()->enqueueMessage( - 'MokoSuiteBackup could not verify your license key status. ' - . 'Please check System → Update Sites to ensure a valid license key is configured.', - 'warning' - ); + catch (\Exception $e) + { + error_log('MokoSuiteBackup: License key prompt failed: ' . $e->getMessage()); } } } -- 2.52.0 From 5d4662342fb2ac9bcc39ac4330d74c4eb949b5e2 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sat, 4 Jul 2026 18:23:15 +0000 Subject: [PATCH 2/4] chore(version): pre-release bump to 02.52.25-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- SECURITY.md | 2 +- source/packages/MokoSuiteClient | 2 +- source/packages/com_mokosuitebackup/mokosuitebackup.xml | 2 +- .../mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml | 2 +- .../packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml | 2 +- .../packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml | 2 +- .../plg_webservices_mokosuitebackup/mokosuitebackup.xml | 2 +- source/pkg_mokosuitebackup.xml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 11958bd..2b91385 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.00.00 +# VERSION: 02.52.25 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/SECURITY.md b/SECURITY.md index c72f18e..73cecfe 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla PATH: /SECURITY.md -VERSION: 02.52.24 +VERSION: 02.52.25 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/source/packages/MokoSuiteClient b/source/packages/MokoSuiteClient index c7e6670..bb0b6ec 160000 --- a/source/packages/MokoSuiteClient +++ b/source/packages/MokoSuiteClient @@ -1 +1 @@ -Subproject commit c7e66705443f74e3ee2ffdfecc08224cc40240aa +Subproject commit bb0b6ecac46a5c0a14606e16411121a9d158695b diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml index f099e1a..61696e8 100644 --- a/source/packages/com_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/com_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> MokoSuiteBackup - 02.52.24 + 02.52.25 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml index 099fe46..09c6624 100644 --- a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml +++ b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml @@ -8,7 +8,7 @@ --> mod_mokosuitebackup_cpanel - 02.52.24 + 02.52.25 2026-06-23 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml index ced288a..1c25140 100644 --- a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Action Log - MokoSuiteBackup - 02.52.24 + 02.52.25 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml index 1e8f1a8..1612b4a 100644 --- a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Console - MokoSuiteBackup - 02.52.24 + 02.52.25 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml index 7d17290..967a767 100644 --- a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Content - MokoSuiteBackup - 02.52.24 + 02.52.25 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml index e9fa579..af8383d 100644 --- a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml @@ -1,7 +1,7 @@ Quick Icon - MokoSuiteBackup - 02.52.24 + 02.52.25 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml index 4cefd2c..23218c4 100644 --- a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> System - MokoSuiteBackup - 02.52.24 + 02.52.25 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml index 4381063..452d63d 100644 --- a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Task - MokoSuiteBackup - 02.52.24 + 02.52.25 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml index bc268fc..c854bc0 100644 --- a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Web Services - MokoSuiteBackup - 02.52.24 + 02.52.25 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml index 15b18fd..e89b07e 100644 --- a/source/pkg_mokosuitebackup.xml +++ b/source/pkg_mokosuitebackup.xml @@ -8,7 +8,7 @@ Package - MokoSuiteBackup mokosuitebackup - 02.52.24 + 02.52.25 2026-06-02 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 46daabc34fb38befc5097a50b3d586dd95a6fe53 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 14:56:48 -0500 Subject: [PATCH 3/4] fix(remote-cleanup): resolve dropped-column references found in review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../sql/updates/mysql/02.52.25.sql | 56 ++-- .../src/Controller/AjaxController.php | 178 ------------ .../src/Engine/AkeebaImporter.php | 21 +- .../src/Field/SftpPathField.php | 253 ------------------ 4 files changed, 33 insertions(+), 475 deletions(-) delete mode 100644 source/packages/com_mokosuitebackup/src/Field/SftpPathField.php diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql index 0551d9d..550a037 100644 --- a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql @@ -1,27 +1,31 @@ +-- Remove legacy single-remote storage columns (superseded by #__mokosuitebackup_remotes). +-- Plain DROP COLUMN (no IF EXISTS): all columns are created by install.mysql.sql and +-- earlier updates, so they always exist here. `DROP COLUMN IF EXISTS` is a MariaDB-only +-- extension and errors on Oracle MySQL 8.x, which Joomla also supports. ALTER TABLE `#__mokosuitebackup_profiles` - DROP COLUMN IF EXISTS `remote_storage`, - DROP COLUMN IF EXISTS `ftp_host`, - DROP COLUMN IF EXISTS `ftp_port`, - DROP COLUMN IF EXISTS `ftp_username`, - DROP COLUMN IF EXISTS `ftp_password`, - DROP COLUMN IF EXISTS `ftp_path`, - DROP COLUMN IF EXISTS `ftp_passive`, - DROP COLUMN IF EXISTS `ftp_ssl`, - DROP COLUMN IF EXISTS `sftp_host`, - DROP COLUMN IF EXISTS `sftp_port`, - DROP COLUMN IF EXISTS `sftp_username`, - DROP COLUMN IF EXISTS `sftp_auth_type`, - DROP COLUMN IF EXISTS `sftp_password`, - DROP COLUMN IF EXISTS `sftp_key_data`, - DROP COLUMN IF EXISTS `sftp_passphrase`, - DROP COLUMN IF EXISTS `sftp_path`, - DROP COLUMN IF EXISTS `gdrive_client_id`, - DROP COLUMN IF EXISTS `gdrive_client_secret`, - DROP COLUMN IF EXISTS `gdrive_refresh_token`, - DROP COLUMN IF EXISTS `gdrive_folder_id`, - DROP COLUMN IF EXISTS `s3_endpoint`, - DROP COLUMN IF EXISTS `s3_region`, - DROP COLUMN IF EXISTS `s3_access_key`, - DROP COLUMN IF EXISTS `s3_secret_key`, - DROP COLUMN IF EXISTS `s3_bucket`, - DROP COLUMN IF EXISTS `s3_path`; + DROP COLUMN `remote_storage`, + DROP COLUMN `ftp_host`, + DROP COLUMN `ftp_port`, + DROP COLUMN `ftp_username`, + DROP COLUMN `ftp_password`, + DROP COLUMN `ftp_path`, + DROP COLUMN `ftp_passive`, + DROP COLUMN `ftp_ssl`, + DROP COLUMN `sftp_host`, + DROP COLUMN `sftp_port`, + DROP COLUMN `sftp_username`, + DROP COLUMN `sftp_auth_type`, + DROP COLUMN `sftp_password`, + DROP COLUMN `sftp_key_data`, + DROP COLUMN `sftp_passphrase`, + DROP COLUMN `sftp_path`, + DROP COLUMN `gdrive_client_id`, + DROP COLUMN `gdrive_client_secret`, + DROP COLUMN `gdrive_refresh_token`, + DROP COLUMN `gdrive_folder_id`, + DROP COLUMN `s3_endpoint`, + DROP COLUMN `s3_region`, + DROP COLUMN `s3_access_key`, + DROP COLUMN `s3_secret_key`, + DROP COLUMN `s3_bucket`, + DROP COLUMN `s3_path`; diff --git a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php index 264464f..c3d396f 100644 --- a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php @@ -1265,184 +1265,6 @@ class AjaxController extends BaseController return $config; } - /** - * Browse directories on a remote SFTP server for the path picker. - * POST: task=ajax.browseSftpDir&profile_id=1&path=/some/path - */ - public function browseSftpDir(): void - { - if (!Session::checkToken('get') && !Session::checkToken('post')) { - $this->sendJson(['error' => true, 'message' => 'Invalid token'], 403); - - return; - } - - if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) { - $this->sendJson(['error' => true, 'message' => 'Access denied'], 403); - - return; - } - - $profileId = $this->input->getInt('profile_id', 0); - - if (!$profileId) { - $this->sendJson(['error' => true, 'message' => 'Missing profile_id']); - - return; - } - - /* Load the profile to get SFTP credentials */ - try { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuitebackup_profiles')) - ->where($db->quoteName('id') . ' = ' . $profileId); - $db->setQuery($query); - $profile = $db->loadObject(); - } catch (\Exception $e) { - $this->sendJson(['error' => true, 'message' => 'Failed to load profile'], 500); - - return; - } - - if (!$profile) { - $this->sendJson(['error' => true, 'message' => 'Profile not found'], 404); - - return; - } - - $host = $profile->sftp_host ?? ''; - $port = (int) ($profile->sftp_port ?? 22); - $username = $profile->sftp_username ?? ''; - $keyData = $profile->sftp_key_data ?? ''; - $password = $profile->sftp_password ?? ''; - - if (empty($host) || empty($username)) { - $this->sendJson(['error' => true, 'message' => 'SFTP host and username must be configured and saved before browsing']); - - return; - } - - if (empty($keyData) && empty($password)) { - $this->sendJson(['error' => true, 'message' => 'SFTP credentials (key or password) must be configured and saved before browsing']); - - return; - } - - $requestPath = $this->input->getString('path', '/'); - - /* Sanitize: must start with / and not contain shell meta-characters */ - $requestPath = '/' . ltrim($requestPath, '/'); - - if (preg_match('/[;&|`$<>]/', $requestPath)) { - $this->sendJson(['error' => true, 'message' => 'Invalid path characters']); - - return; - } - - $keyFile = null; - - try { - /* Write temp key if using key auth (same pattern as SftpUploader) */ - if (!empty($keyData)) { - $keyContent = base64_decode($keyData, true); - - if ($keyContent === false) { - $keyContent = $keyData; - } - - $keyFile = sys_get_temp_dir() . '/mokobackup-sftp-browse-' . bin2hex(random_bytes(8)) . '.key'; - - if (file_put_contents($keyFile, $keyContent) === false) { - throw new \RuntimeException('Cannot write temporary SSH key file'); - } - - chmod($keyFile, 0600); - } - - /* Build SSH command to list directories */ - $escapedPath = escapeshellarg($requestPath); - $remoteCmd = 'ls -1pa ' . $escapedPath . ' 2>/dev/null | grep "/$"'; - - $parts = ['ssh', '-o', 'StrictHostKeyChecking=no', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10']; - - if ($port !== 22) { - $parts[] = '-p'; - $parts[] = (string) $port; - } - - if ($keyFile !== null) { - $parts[] = '-i'; - $parts[] = escapeshellarg($keyFile); - } - - $parts[] = escapeshellarg($username . '@' . $host); - $parts[] = escapeshellarg($remoteCmd); - - $cmd = implode(' ', $parts); - - $output = []; - $exitCode = 0; - exec($cmd . ' 2>&1', $output, $exitCode); - - /* exitCode 1 from grep means no matches (empty dir), which is OK */ - if ($exitCode !== 0 && $exitCode !== 1) { - throw new \RuntimeException('SSH command failed (exit ' . $exitCode . '): ' . implode(' ', $output)); - } - - /* Parse output: each line is a directory name ending with / */ - $dirs = []; - - foreach ($output as $line) { - $line = trim($line); - - if ($line === '' || $line === './' || $line === '../') { - continue; - } - - $dirName = rtrim($line, '/'); - - if ($dirName === '' || $dirName === '.' || $dirName === '..') { - continue; - } - - $fullPath = rtrim($requestPath, '/') . '/' . $dirName; - - $dirs[] = [ - 'name' => $dirName, - 'path' => $fullPath, - ]; - } - - usort($dirs, fn($a, $b) => strcasecmp($a['name'], $b['name'])); - - /* Parent path */ - $parent = null; - - if ($requestPath !== '/') { - $parent = \dirname($requestPath); - - if ($parent === '') { - $parent = '/'; - } - } - - $this->sendJson([ - 'error' => false, - 'current' => $requestPath, - 'parent' => $parent, - 'dirs' => $dirs, - ]); - } catch (\Throwable $e) { - $this->sendJson(['error' => true, 'message' => 'SFTP browse failed: ' . $e->getMessage()]); - } finally { - if ($keyFile !== null && is_file($keyFile)) { - unlink($keyFile); - } - } - } - /** * Send a JSON response and close the application. */ diff --git a/source/packages/com_mokosuitebackup/src/Engine/AkeebaImporter.php b/source/packages/com_mokosuitebackup/src/Engine/AkeebaImporter.php index 7020926..79dca60 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/AkeebaImporter.php +++ b/source/packages/com_mokosuitebackup/src/Engine/AkeebaImporter.php @@ -228,24 +228,9 @@ class AkeebaImporter 'exclude_dirs' => implode("\n", $filters['exclude_dirs']), 'exclude_files' => implode("\n", $filters['exclude_files']), 'exclude_tables' => implode("\n", $filters['exclude_tables']), - 'remote_storage' => $this->mapRemoteStorage($config), - 'ftp_host' => $config['engine.postproc.ftp.host'] ?? '', - 'ftp_port' => (int) ($config['engine.postproc.ftp.port'] ?? 21), - 'ftp_username' => $config['engine.postproc.ftp.user'] ?? '', - 'ftp_password' => $config['engine.postproc.ftp.pass'] ?? '', - 'ftp_path' => $config['engine.postproc.ftp.initial_directory'] ?? '/backups', - 'ftp_passive' => (int) ($config['engine.postproc.ftp.passive_mode'] ?? 1), - 'ftp_ssl' => (int) ($config['engine.postproc.ftp.ftps'] ?? 0), - 'gdrive_client_id' => $config['engine.postproc.googledrive.client_id'] ?? '', - 'gdrive_client_secret' => $config['engine.postproc.googledrive.client_secret'] ?? '', - 'gdrive_refresh_token' => $config['engine.postproc.googledrive.refresh_token'] ?? '', - 'gdrive_folder_id' => $config['engine.postproc.googledrive.directory'] ?? '', - 's3_endpoint' => $config['engine.postproc.s3.custom_endpoint'] ?? '', - 's3_region' => $config['engine.postproc.s3.region'] ?? 'us-east-1', - 's3_access_key' => $config['engine.postproc.s3.access_key'] ?? ($config['engine.postproc.s3.accesskey'] ?? ''), - 's3_secret_key' => $config['engine.postproc.s3.secret_key'] ?? ($config['engine.postproc.s3.secretkey'] ?? ''), - 's3_bucket' => $config['engine.postproc.s3.bucket'] ?? '', - 's3_path' => $config['engine.postproc.s3.directory'] ?? '/backups', + // Remote storage is no longer stored on the profile — it lives in + // #__mokosuitebackup_remotes. Akeeba remote settings are not imported; + // re-add remote destinations on the profile's Remote tab after import. 'remote_keep_local' => 1, 'include_mokorestore' => (int) (($config['akeeba.advanced.embedded_installer'] ?? 'none') !== 'none'), 'published' => 1, diff --git a/source/packages/com_mokosuitebackup/src/Field/SftpPathField.php b/source/packages/com_mokosuitebackup/src/Field/SftpPathField.php deleted file mode 100644 index b501999..0000000 --- a/source/packages/com_mokosuitebackup/src/Field/SftpPathField.php +++ /dev/null @@ -1,253 +0,0 @@ - - * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. - * @license GNU General Public License version 3 or later; see LICENSE - * - * SFTP remote path field with Browse Remote button and modal directory browser. - */ - -namespace Joomla\Component\MokoSuiteBackup\Administrator\Field; - -defined('_JEXEC') or die; - -use Joomla\CMS\Form\FormField; - -class SftpPathField extends FormField -{ - protected $type = 'SftpPath'; - - protected function getInput(): string - { - $value = htmlspecialchars($this->value ?: $this->default, ENT_QUOTES, 'UTF-8'); - $id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8'); - $name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8'); - - return << - - -
- - -HTML; - } -} -- 2.52.0 From d3e3fd25e7ca17519342058b2acc55cfee605658 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sat, 4 Jul 2026 20:18:21 +0000 Subject: [PATCH 4/4] chore(version): pre-release bump to 02.52.30-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- SECURITY.md | 2 +- source/packages/MokoSuiteClient | 2 +- source/packages/com_mokosuitebackup/mokosuitebackup.xml | 2 +- .../packages/com_mokosuitebackup/sql/updates/mysql/02.52.30.sql | 1 + .../mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml | 2 +- .../packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml | 2 +- .../packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml | 2 +- .../plg_webservices_mokosuitebackup/mokosuitebackup.xml | 2 +- source/pkg_mokosuitebackup.xml | 2 +- 14 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.30.sql diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 2b91385..829019f 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 02.52.25 +# VERSION: 02.52.30 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/SECURITY.md b/SECURITY.md index 73cecfe..4f0da85 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla PATH: /SECURITY.md -VERSION: 02.52.25 +VERSION: 02.52.30 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/source/packages/MokoSuiteClient b/source/packages/MokoSuiteClient index bb0b6ec..555376e 160000 --- a/source/packages/MokoSuiteClient +++ b/source/packages/MokoSuiteClient @@ -1 +1 @@ -Subproject commit bb0b6ecac46a5c0a14606e16411121a9d158695b +Subproject commit 555376e4a5127b08ea953e8d405bd8c5fff1c1f8 diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml index 61696e8..5463fb1 100644 --- a/source/packages/com_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/com_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> MokoSuiteBackup - 02.52.25 + 02.52.30 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.30.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.30.sql new file mode 100644 index 0000000..4d38f7e --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.30.sql @@ -0,0 +1 @@ +/* 02.52.30 — no schema changes */ diff --git a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml index 09c6624..cf06585 100644 --- a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml +++ b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml @@ -8,7 +8,7 @@ --> mod_mokosuitebackup_cpanel - 02.52.25 + 02.52.30 2026-06-23 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml index 1c25140..fb20ce7 100644 --- a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Action Log - MokoSuiteBackup - 02.52.25 + 02.52.30 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml index 1612b4a..6b46622 100644 --- a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Console - MokoSuiteBackup - 02.52.25 + 02.52.30 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml index 967a767..91db104 100644 --- a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Content - MokoSuiteBackup - 02.52.25 + 02.52.30 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml index af8383d..adb35a5 100644 --- a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml @@ -1,7 +1,7 @@ Quick Icon - MokoSuiteBackup - 02.52.25 + 02.52.30 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml index 23218c4..35ffc50 100644 --- a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> System - MokoSuiteBackup - 02.52.25 + 02.52.30 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml index 452d63d..d5a5fb9 100644 --- a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Task - MokoSuiteBackup - 02.52.25 + 02.52.30 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml index c854bc0..7b59d6f 100644 --- a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Web Services - MokoSuiteBackup - 02.52.25 + 02.52.30 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml index e89b07e..cd6dc5d 100644 --- a/source/pkg_mokosuitebackup.xml +++ b/source/pkg_mokosuitebackup.xml @@ -8,7 +8,7 @@ Package - MokoSuiteBackup mokosuitebackup - 02.52.25 + 02.52.30 2026-06-02 Moko Consulting hello@mokoconsulting.tech -- 2.52.0