diff --git a/.mokogitea/CLAUDE.md b/.mokogitea/CLAUDE.md index c3ba57e3..c3c9d03d 100644 --- a/.mokogitea/CLAUDE.md +++ b/.mokogitea/CLAUDE.md @@ -60,7 +60,7 @@ Joomla **package** with four sub-extensions: - **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`) - **Minification**: handled at build time (CI) - **Wiki**: documentation lives in the Gitea wiki, not `docs/` files -- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokoplatform/wiki/Home) +- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home) ## Coding Standards diff --git a/source/packages/com_mokosuitebackup/api/src/View/Backups/JsonapiView.php b/source/packages/com_mokosuitebackup/api/src/View/Backups/JsonapiView.php index 147fe924..54e17f8f 100644 --- a/source/packages/com_mokosuitebackup/api/src/View/Backups/JsonapiView.php +++ b/source/packages/com_mokosuitebackup/api/src/View/Backups/JsonapiView.php @@ -24,7 +24,6 @@ class JsonapiView extends BaseApiView 'origin', 'backup_type', 'archivename', - 'absolute_path', 'total_size', 'db_size', 'files_count', diff --git a/source/packages/com_mokosuitebackup/sql/uninstall.mysql.sql b/source/packages/com_mokosuitebackup/sql/uninstall.mysql.sql index 2a39ae31..3e68ead9 100644 --- a/source/packages/com_mokosuitebackup/sql/uninstall.mysql.sql +++ b/source/packages/com_mokosuitebackup/sql/uninstall.mysql.sql @@ -1,3 +1,4 @@ DROP TABLE IF EXISTS `#__mokosuitebackup_remotes`; DROP TABLE IF EXISTS `#__mokosuitebackup_records`; +DROP TABLE IF EXISTS `#__mokosuitebackup_snapshots`; DROP TABLE IF EXISTS `#__mokosuitebackup_profiles`; diff --git a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php index c3d396f9..9d568ad7 100644 --- a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php @@ -1201,6 +1201,7 @@ class AjaxController extends BaseController private function maskSecrets(array $config, string $type): array { $secrets = [ + 'ftp' => ['password'], 'sftp' => ['password', 'passphrase', 'key_data'], 's3' => ['secret_key'], 'google_drive' => ['client_secret', 'refresh_token'], @@ -1223,6 +1224,7 @@ class AjaxController extends BaseController private function mergeExistingSecrets(int $id, array $config, string $type): array { $secrets = [ + 'ftp' => ['password'], 'sftp' => ['password', 'passphrase', 'key_data'], 's3' => ['secret_key'], 'google_drive' => ['client_secret', 'refresh_token'], diff --git a/source/packages/com_mokosuitebackup/src/Engine/DatabaseDumper.php b/source/packages/com_mokosuitebackup/src/Engine/DatabaseDumper.php index 429e0523..81a2935a 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/DatabaseDumper.php +++ b/source/packages/com_mokosuitebackup/src/Engine/DatabaseDumper.php @@ -326,7 +326,7 @@ class DatabaseDumper } $createSql = str_replace('`' . $prefix, '`#__', $createRow[1]); - fwrite($fp, 'DROP TABLE IF EXISTS `' . $abstractName . "`;\\n"); + fwrite($fp, 'DROP TABLE IF EXISTS `' . $abstractName . "`;\n"); fwrite($fp, $createSql . ";\n\n"); } diff --git a/source/packages/com_mokosuitebackup/src/Engine/DatabaseImporter.php b/source/packages/com_mokosuitebackup/src/Engine/DatabaseImporter.php index af681d5f..5ce1aaa0 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/DatabaseImporter.php +++ b/source/packages/com_mokosuitebackup/src/Engine/DatabaseImporter.php @@ -20,6 +20,13 @@ use Joomla\CMS\Factory; class DatabaseImporter { + /** + * Non-fatal per-statement errors collected during the last import(). + * + * @var string[] + */ + private array $errors = []; + /** * Import a SQL dump file into the database. * @@ -31,6 +38,8 @@ class DatabaseImporter */ public function import(string $sqlFile): int { + $this->errors = []; + if (!is_file($sqlFile) || !is_readable($sqlFile)) { throw new \RuntimeException('SQL file not readable: ' . $sqlFile); } @@ -97,8 +106,10 @@ class DatabaseImporter } catch (\Exception $e) { // Log but don't abort — some statements may fail on // different MySQL versions (e.g. charset differences) - // but the overall restore should continue. + // but the overall restore should continue. Errors are + // collected so the caller can surface a warning status. error_log('MokoSuiteBackup SQL import warning: ' . $e->getMessage()); + $this->errors[] = $e->getMessage(); } } } @@ -115,6 +126,7 @@ class DatabaseImporter $statementsExecuted++; } catch (\Exception $e) { error_log('MokoSuiteBackup SQL import warning (final): ' . $e->getMessage()); + $this->errors[] = $e->getMessage(); } } } finally { @@ -123,4 +135,24 @@ class DatabaseImporter return $statementsExecuted; } + + /** + * Non-fatal errors from the last import(), if any. A non-empty result + * means the restore completed with problems and should be treated as a + * warning rather than a clean success. + * + * @return string[] + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * Whether the last import() had any non-fatal statement errors. + */ + public function hasErrors(): bool + { + return $this->errors !== []; + } } diff --git a/source/packages/com_mokosuitebackup/src/Engine/SftpUploader.php b/source/packages/com_mokosuitebackup/src/Engine/SftpUploader.php index 980ea4a7..d32f4c64 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/SftpUploader.php +++ b/source/packages/com_mokosuitebackup/src/Engine/SftpUploader.php @@ -207,7 +207,7 @@ class SftpUploader implements RemoteUploaderInterface */ private function buildScpCommand(string $localPath, string $remoteTarget, ?string $keyFile): string { - $parts = ['scp', '-o', 'StrictHostKeyChecking=no', '-o', 'BatchMode=yes']; + $parts = ['scp', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'BatchMode=yes']; if ($this->port !== 22) { $parts[] = '-P'; @@ -235,7 +235,7 @@ class SftpUploader implements RemoteUploaderInterface */ private function buildSshCommand(string $remoteCmd, ?string $keyFile): string { - $parts = ['ssh', '-o', 'StrictHostKeyChecking=no', '-o', 'BatchMode=yes']; + $parts = ['ssh', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'BatchMode=yes']; if ($this->port !== 22) { $parts[] = '-p'; diff --git a/source/packages/plg_console_mokosuitebackup/src/Command/SnapshotCommand.php b/source/packages/plg_console_mokosuitebackup/src/Command/SnapshotCommand.php index 9dbf58af..1ae866b6 100644 --- a/source/packages/plg_console_mokosuitebackup/src/Command/SnapshotCommand.php +++ b/source/packages/plg_console_mokosuitebackup/src/Command/SnapshotCommand.php @@ -227,11 +227,11 @@ class SnapshotCommand extends AbstractCommand } // Delete the snapshot file if it exists - if (!empty($record->file_path) && is_file($record->file_path)) { - if (!@unlink($record->file_path)) { - $io->warning('Could not delete snapshot file: ' . $record->file_path); + if (!empty($record->data_file) && is_file($record->data_file)) { + if (!@unlink($record->data_file)) { + $io->warning('Could not delete snapshot file: ' . $record->data_file); } else { - $io->text('Deleted file: ' . $record->file_path); + $io->text('Deleted file: ' . $record->data_file); } } diff --git a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml index fe4b04af..80d3cb25 100644 --- a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml @@ -59,7 +59,7 @@ type="sql" label="PLG_CONTENT_MOKOJOOMBACKUP_FIELD_PROFILE" description="PLG_CONTENT_MOKOJOOMBACKUP_FIELD_PROFILE_DESC" - query="SELECT id AS value, title AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY ordering ASC" + query="SELECT id AS value, title AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY id ASC" default="1" > diff --git a/source/packages/plg_content_mokosuitebackup/src/Extension/MokoSuiteBackupContent.php b/source/packages/plg_content_mokosuitebackup/src/Extension/MokoSuiteBackupContent.php index f3f98b0a..4e0654bd 100644 --- a/source/packages/plg_content_mokosuitebackup/src/Extension/MokoSuiteBackupContent.php +++ b/source/packages/plg_content_mokosuitebackup/src/Extension/MokoSuiteBackupContent.php @@ -24,9 +24,11 @@ final class MokoSuiteBackupContent extends CMSPlugin implements SubscriberInterf public static function getSubscribedEvents(): array { + // Pre-update backups are owned by plg_system_mokosuitebackup, which also + // subscribes to onExtensionBeforeUpdate. Only pre-install is handled here + // to avoid running the backup twice on a single extension update. return [ 'onExtensionBeforeInstall' => 'onExtensionBeforeInstall', - 'onExtensionBeforeUpdate' => 'onExtensionBeforeUpdate', ]; } @@ -42,18 +44,6 @@ final class MokoSuiteBackupContent extends CMSPlugin implements SubscriberInterf $this->triggerAutoBackup('Pre-install backup'); } - /** - * Trigger a backup before an extension is updated. - */ - public function onExtensionBeforeUpdate(Event $event): void - { - if (!(int) $this->params->get('backup_before_update', 1)) { - return; - } - - $this->triggerAutoBackup('Pre-update backup'); - } - /** * Run a backup using the configured profile. */ diff --git a/source/packages/plg_task_mokosuitebackup/forms/run_profile.xml b/source/packages/plg_task_mokosuitebackup/forms/run_profile.xml index cf1fc9cf..8471774f 100644 --- a/source/packages/plg_task_mokosuitebackup/forms/run_profile.xml +++ b/source/packages/plg_task_mokosuitebackup/forms/run_profile.xml @@ -11,7 +11,7 @@ type="sql" label="PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE" description="PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE_DESC" - query="SELECT id AS value, title AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY ordering ASC" + query="SELECT id AS value, title AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY id ASC" default="1" required="true" >