From 46daabc34fb38befc5097a50b3d586dd95a6fe53 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 14:56:48 -0500 Subject: [PATCH] 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; - } -}