From 7e5a450c9685ad1516244134acba135fd32da544 Mon Sep 17 00:00:00 2001 From: binsky Date: Sun, 11 Jan 2026 20:19:36 +0100 Subject: [PATCH 1/3] combine old passman migrations into a new single one, using a new table prefix 'passman_next_' --- lib/Migration/ServerSideEncryption.php | 123 -------- .../Version02031334Date20210926234011.php | 60 ---- .../Version02031335Date20211001122343.php | 87 ------ ...> VersionNext030000Date20260110234900.php} | 268 ++++++++++-------- 4 files changed, 152 insertions(+), 386 deletions(-) delete mode 100644 lib/Migration/ServerSideEncryption.php delete mode 100644 lib/Migration/Version02031334Date20210926234011.php delete mode 100644 lib/Migration/Version02031335Date20211001122343.php rename lib/Migration/{Version020308Date20210711121919.php => VersionNext030000Date20260110234900.php} (64%) diff --git a/lib/Migration/ServerSideEncryption.php b/lib/Migration/ServerSideEncryption.php deleted file mode 100644 index d0777529..00000000 --- a/lib/Migration/ServerSideEncryption.php +++ /dev/null @@ -1,123 +0,0 @@ -. - * - */ - -namespace OCA\PassmanNext\Migration; - -use OCA\PassmanNext\Db\CredentialRevision; -use OCA\PassmanNext\Db\File; -use OCA\PassmanNext\Service\CredentialRevisionService; -use OCA\PassmanNext\Service\CredentialService; -use OCA\PassmanNext\Service\EncryptService; -use OCA\PassmanNext\Service\FileService; -use OCP\DB\Exception; -use OCP\IConfig; -use OCP\IDBConnection; -use OCP\Migration\IOutput; -use OCP\Migration\IRepairStep; -use Psr\Log\LoggerInterface; - - -class ServerSideEncryption implements IRepairStep { - - /** @var string */ - private $installedVersion; - - public function __construct( - private readonly EncryptService $encryptService, - private readonly IDBConnection $db, - private readonly LoggerInterface $logger, - private readonly CredentialService $credentialService, - private readonly CredentialRevisionService $revisionService, - private readonly FileService $fileService, - IConfig $config, - ) { - $this->installedVersion = $config->getAppValue('passman', 'installed_version'); - } - - public function getName() { - return 'Enabling server side encryption for passman'; - } - - public function run(IOutput $output) { - $output->info('Enabling Service Side Encryption for passman'); - - if (version_compare($this->installedVersion, '2.0.0RC4', '<')) { - $this->encryptCredentials(); - $this->encryptRevisions(); - $this->encryptFiles(); - } - } - - /** - * KEEP THIS METHOD PRIVATE!!! - * - * @param string $table - * @return mixed[] - * @throws Exception - */ - private function fetchAll(string $table): array { - $qb = $this->db->getQueryBuilder(); - $result = $qb->select('*') - ->from($table) - ->executeQuery(); - return $result->fetchAll(); - } - - private function encryptCredentials(): void { - $credentials = $this->fetchAll('passman_credentials'); - foreach ($credentials as $credential) { - $this->credentialService->updateCredential($credential); - } - } - - private function encryptRevisions(): void { - $revisions = $this->fetchAll('passman_revisions'); - foreach ($revisions as $_revision) { - $revision = new CredentialRevision(); - $revision->setId($_revision['id']); - $revision->setGuid($_revision['guid']); - $revision->setCredentialId($_revision['credential_id']); - $revision->setUserId($_revision['user_id']); - $revision->setCreated($_revision['created']); - $revision->setEditedBy($_revision['edited_by']); - $revision->setCredentialData($_revision['credential_data']); - $this->revisionService->updateRevision($revision); - } - } - - private function encryptFiles() { - $files = $this->fetchAll('passman_files'); - foreach ($files as $_file) { - $file = new File(); - $file->setId($_file['id']); - $file->setGuid($_file['guid']); - $file->setUserId($_file['user_id']); - $file->setMimetype($_file['minetype']); - $file->setFilename($_file['filename']); - $file->setSize($_file['size']); - $file->setCreated($_file['created']); - $file->setFileData($_file['file_data']); - $this->fileService->updateFile($file); - } - } -} diff --git a/lib/Migration/Version02031334Date20210926234011.php b/lib/Migration/Version02031334Date20210926234011.php deleted file mode 100644 index eaa2e86a..00000000 --- a/lib/Migration/Version02031334Date20210926234011.php +++ /dev/null @@ -1,60 +0,0 @@ -hasTable('passman_credentials')) { - $table = $schema->getTable('passman_credentials'); - if ($table->hasIndex('passman_credential_label_index')) { - $table->dropIndex('passman_credential_label_index'); - } - $labelColumn = $table->getColumn('label'); - if ($labelColumn->getLength() < 2048 || $labelColumn->getType() !== Type::getType('string')) { - $table->changeColumn('label', [ - 'type' => Type::getType('string'), - 'length' => 2048 - ]); - } - } - - return $schema; - } - - /** - * @param IOutput $output - * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` - * @param array $options - */ - public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { - } -} diff --git a/lib/Migration/Version02031335Date20211001122343.php b/lib/Migration/Version02031335Date20211001122343.php deleted file mode 100644 index 9cab6ba8..00000000 --- a/lib/Migration/Version02031335Date20211001122343.php +++ /dev/null @@ -1,87 +0,0 @@ -hasTable('passman_credentials')) { - $table = $schema->getTable('passman_credentials'); - if ($table->hasColumn($this->oldColumn) && !$table->hasColumn($this->newColumn)) { - $table->addColumn($this->newColumn, 'text', [ - 'notnull' => false, - ]); - $this->dataMigrationRequired = true; - } - } - - return $schema; - } - - /** - * @param IOutput $output - * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` - * @param array $options - */ - public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { - if ($this->dataMigrationRequired) { - $updateQuery = $this->connection->getQueryBuilder(); - $updateQuery->update('passman_credentials') - ->set($this->newColumn, $this->oldColumn) - ->where($this->newColumn . ' IS NULL') - ->andWhere($this->oldColumn . ' IS NOT NULL') - ->executeStatement(); - - /** @var ISchemaWrapper $schema */ - $schema = $schemaClosure(); - - if ($schema->hasTable('passman_credentials')) { - $table = $schema->getTable('passman_credentials'); - if ($table->hasColumn($this->oldColumn) && $table->hasColumn($this->newColumn)) { - $dropColumnStatement = $this->connection->prepare('ALTER TABLE ' . $table->getName() . ' DROP COLUMN ' . $this->oldColumn . ';'); - $dropColumnStatement->execute(); - } - } - } - } -} diff --git a/lib/Migration/Version020308Date20210711121919.php b/lib/Migration/VersionNext030000Date20260110234900.php similarity index 64% rename from lib/Migration/Version020308Date20210711121919.php rename to lib/Migration/VersionNext030000Date20260110234900.php index e785f250..342655f8 100644 --- a/lib/Migration/Version020308Date20210711121919.php +++ b/lib/Migration/VersionNext030000Date20260110234900.php @@ -6,67 +6,70 @@ use Closure; use OCP\DB\ISchemaWrapper; +use OCP\IDBConnection; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; /** - * Auto-generated migration step: Please modify to your needs! + * Initial migration for Passman Next, based on the combination of migrations from the Passman (legacy v2.4.12) app. */ -class Version020308Date20210711121919 extends SimpleMigrationStep { +class VersionNext030000Date20260110234900 extends SimpleMigrationStep +{ + + const TABLE_PREFIX = 'passman_next_'; /** - * @param IOutput $output - * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` - * @param array $options + * @param IDBConnection $connection */ - public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + public function __construct( + protected IDBConnection $connection, + ) { } /** * @param IOutput $output * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` * @param array $options - * @return null|ISchemaWrapper */ - public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { - /** @var ISchemaWrapper $schema */ - $schema = $schemaClosure(); + public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } - if (!$schema->hasTable('passman_vaults')) { - $table = $schema->createTable('passman_vaults'); + private function vaultsTable(ISchemaWrapper $schema): void { + if (!$schema->hasTable(self::TABLE_PREFIX . 'vaults')) { + $table = $schema->createTable(self::TABLE_PREFIX . 'vaults'); $table->addColumn('id', 'bigint', [ 'autoincrement' => true, - 'notnull' => true, - 'length' => 8, - 'unsigned' => true, + 'notnull' => true, + 'length' => 8, + 'unsigned' => true, ]); $table->addColumn('guid', 'string', [ 'notnull' => true, - 'length' => 64, + 'length' => 64, 'default' => '', ]); $table->addColumn('user_id', 'string', [ 'notnull' => true, - 'length' => 64, + 'length' => 64, 'default' => '', ]); $table->addColumn('name', 'string', [ 'notnull' => true, - 'length' => 100, + 'length' => 100, ]); $table->addColumn('vault_settings', 'text', [ 'notnull' => false, ]); $table->addColumn('created', 'bigint', [ - 'notnull' => false, - 'length' => 8, - 'default' => 0, + 'notnull' => false, + 'length' => 8, + 'default' => 0, 'unsigned' => true, ]); $table->addColumn('last_access', 'bigint', [ - 'notnull' => false, - 'length' => 8, - 'default' => 0, + 'notnull' => false, + 'length' => 8, + 'default' => 0, 'unsigned' => true, ]); $table->addColumn('public_sharing_key', 'text', [ @@ -77,7 +80,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt ]); $table->addColumn('sharing_keys_generated', 'bigint', [ 'notnull' => false, - 'length' => 8, + 'length' => 8, ]); $table->setPrimaryKey(['id']); $table->addIndex(['last_access'], 'passman_vault_last_access_index'); @@ -85,43 +88,46 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addIndex(['id'], 'npassman_vault_id_index'); $table->addIndex(['user_id'], 'passman_vault_uid_id_index'); } + } - if (!$schema->hasTable('passman_credentials')) { - $table = $schema->createTable('passman_credentials'); + private function credentialsTable(ISchemaWrapper $schema): void { + if (!$schema->hasTable(self::TABLE_PREFIX . 'credentials')) { + $table = $schema->createTable(self::TABLE_PREFIX . 'credentials'); $table->addColumn('id', 'bigint', [ 'autoincrement' => true, - 'notnull' => true, - 'length' => 8, - 'unsigned' => true, + 'notnull' => true, + 'length' => 8, + 'unsigned' => true, ]); $table->addColumn('guid', 'string', [ 'notnull' => false, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('user_id', 'string', [ 'notnull' => false, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('vault_id', 'bigint', [ 'notnull' => true, - 'length' => 8, + 'length' => 8, ]); - $table->addColumn('label', 'text', [ + $table->addColumn('label', 'string', [ 'notnull' => true, + 'length' => 2048 ]); $table->addColumn('description', 'text', [ 'notnull' => false, ]); $table->addColumn('created', 'bigint', [ - 'notnull' => false, - 'length' => 8, - 'default' => 0, + 'notnull' => false, + 'length' => 8, + 'default' => 0, 'unsigned' => true, ]); $table->addColumn('changed', 'bigint', [ - 'notnull' => false, - 'length' => 8, - 'default' => 0, + 'notnull' => false, + 'length' => 8, + 'default' => 0, 'unsigned' => true, ]); $table->addColumn('tags', 'text', [ @@ -140,16 +146,16 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt 'notnull' => false, ]); $table->addColumn('renew_interval', 'bigint', [ - 'notnull' => false, + 'notnull' => false, 'unsigned' => true, ]); $table->addColumn('expire_time', 'bigint', [ - 'notnull' => false, + 'notnull' => false, 'unsigned' => true, ]); $table->addColumn('delete_time', 'bigint', [ - 'notnull' => false, - 'length' => 8, + 'notnull' => false, + 'length' => 8, 'unsigned' => true, ]); $table->addColumn('files', 'text', [ @@ -179,26 +185,28 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addIndex(['vault_id'], 'passman_credential_vault_id_index'); $table->addIndex(['user_id'], 'passman_credential_user_id_index'); } + } - if (!$schema->hasTable('passman_files')) { - $table = $schema->createTable('passman_files'); + private function filesTable(ISchemaWrapper $schema): void { + if (!$schema->hasTable(self::TABLE_PREFIX . 'files')) { + $table = $schema->createTable(self::TABLE_PREFIX . 'files'); $table->addColumn('id', 'bigint', [ 'autoincrement' => true, - 'notnull' => true, - 'length' => 8, - 'unsigned' => true, + 'notnull' => true, + 'length' => 8, + 'unsigned' => true, ]); $table->addColumn('guid', 'string', [ 'notnull' => false, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('user_id', 'string', [ 'notnull' => false, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('mimetype', 'string', [ 'notnull' => true, - 'length' => 255, + 'length' => 255, ]); $table->addColumn('filename', 'text', [ 'notnull' => true, @@ -216,31 +224,33 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addIndex(['id'], 'passman_file_id_index'); $table->addIndex(['user_id'], 'passman_file_user_id_index'); } + } - if (!$schema->hasTable('passman_revisions')) { - $table = $schema->createTable('passman_revisions'); + private function revisionsTable(ISchemaWrapper $schema): void { + if (!$schema->hasTable(self::TABLE_PREFIX . 'revisions')) { + $table = $schema->createTable(self::TABLE_PREFIX . 'revisions'); $table->addColumn('id', 'bigint', [ 'autoincrement' => true, - 'notnull' => true, - 'length' => 8, - 'unsigned' => true, + 'notnull' => true, + 'length' => 8, + 'unsigned' => true, ]); $table->addColumn('guid', 'string', [ 'notnull' => true, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('credential_id', 'bigint', [ 'notnull' => true, - 'length' => 8, + 'length' => 8, ]); $table->addColumn('user_id', 'string', [ 'notnull' => true, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('created', 'bigint', [ - 'notnull' => false, - 'length' => 8, - 'default' => 0, + 'notnull' => false, + 'length' => 8, + 'default' => 0, 'unsigned' => true, ]); $table->addColumn('credential_data', 'text', [ @@ -248,65 +258,67 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt ]); $table->addColumn('edited_by', 'string', [ 'notnull' => true, - 'length' => 64, + 'length' => 64, ]); $table->setPrimaryKey(['id']); $table->addIndex(['id'], 'passman_revision_id_index'); $table->addIndex(['user_id'], 'passman_revision_user_id_index'); $table->addIndex(['credential_id'], 'passman_revision_credential_id_index'); } + } - if (!$schema->hasTable('passman_sharing_acl')) { - $table = $schema->createTable('passman_sharing_acl'); + private function sharingAclTable(ISchemaWrapper $schema): void { + if (!$schema->hasTable(self::TABLE_PREFIX . 'sharing_acl')) { + $table = $schema->createTable(self::TABLE_PREFIX . 'sharing_acl'); $table->addColumn('id', 'bigint', [ 'autoincrement' => true, - 'notnull' => true, - 'length' => 8, - 'unsigned' => true, + 'notnull' => true, + 'length' => 8, + 'unsigned' => true, ]); $table->addColumn('item_id', 'bigint', [ 'notnull' => true, - 'length' => 8, + 'length' => 8, ]); $table->addColumn('item_guid', 'string', [ 'notnull' => true, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('vault_id', 'bigint', [ - 'notnull' => false, - 'length' => 8, + 'notnull' => false, + 'length' => 8, 'unsigned' => true, ]); $table->addColumn('vault_guid', 'string', [ 'notnull' => false, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('user_id', 'string', [ 'notnull' => false, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('created', 'bigint', [ - 'notnull' => false, - 'length' => 64, - 'default' => 0, + 'notnull' => false, + 'length' => 64, + 'default' => 0, 'unsigned' => true, ]); $table->addColumn('expire', 'bigint', [ - 'notnull' => false, - 'length' => 64, - 'default' => 0, + 'notnull' => false, + 'length' => 64, + 'default' => 0, 'unsigned' => true, ]); $table->addColumn('expire_views', 'bigint', [ - 'notnull' => false, - 'length' => 64, - 'default' => 0, + 'notnull' => false, + 'length' => 64, + 'default' => 0, 'unsigned' => true, ]); $table->addColumn('permissions', 'smallint', [ - 'notnull' => true, - 'length' => 3, - 'default' => 0, + 'notnull' => true, + 'length' => 3, + 'default' => 0, 'unsigned' => true, ]); $table->addColumn('shared_key', 'text', [ @@ -314,86 +326,110 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt ]); $table->setPrimaryKey(['id']); } + } - if (!$schema->hasTable('passman_share_request')) { - $table = $schema->createTable('passman_share_request'); + private function shareRequestsTable(ISchemaWrapper $schema): void { + if (!$schema->hasTable(self::TABLE_PREFIX . 'share_request')) { + $table = $schema->createTable(self::TABLE_PREFIX . 'share_request'); $table->addColumn('id', 'bigint', [ 'autoincrement' => true, - 'notnull' => true, - 'length' => 8, - 'unsigned' => true, + 'notnull' => true, + 'length' => 8, + 'unsigned' => true, ]); $table->addColumn('item_id', 'bigint', [ 'notnull' => true, - 'length' => 8, + 'length' => 8, ]); $table->addColumn('item_guid', 'string', [ 'notnull' => true, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('target_user_id', 'string', [ 'notnull' => false, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('from_user_id', 'string', [ 'notnull' => false, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('target_vault_id', 'bigint', [ - 'notnull' => true, - 'length' => 8, + 'notnull' => true, + 'length' => 8, 'unsigned' => true, ]); $table->addColumn('target_vault_guid', 'string', [ 'notnull' => true, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('shared_key', 'text', [ 'notnull' => true, ]); $table->addColumn('permissions', 'smallint', [ - 'notnull' => true, - 'length' => 3, - 'default' => 0, + 'notnull' => true, + 'length' => 3, + 'default' => 0, 'unsigned' => true, ]); $table->addColumn('created', 'bigint', [ - 'notnull' => false, - 'length' => 64, - 'default' => 0, + 'notnull' => false, + 'length' => 64, + 'default' => 0, 'unsigned' => true, ]); $table->setPrimaryKey(['id']); } + } - if (!$schema->hasTable('passman_delete_vault_request')) { - $table = $schema->createTable('passman_delete_vault_request'); + private function deleteVaultRequestsTable(ISchemaWrapper $schema): void { + if (!$schema->hasTable(self::TABLE_PREFIX . 'delete_vault_request')) { + $table = $schema->createTable(self::TABLE_PREFIX . 'delete_vault_request'); $table->addColumn('id', 'bigint', [ 'autoincrement' => true, - 'notnull' => true, - 'length' => 8, - 'unsigned' => true, + 'notnull' => true, + 'length' => 8, + 'unsigned' => true, ]); $table->addColumn('vault_guid', 'string', [ 'notnull' => true, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('reason', 'string', [ 'notnull' => true, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('requested_by', 'string', [ 'notnull' => false, - 'length' => 64, + 'length' => 64, ]); $table->addColumn('created', 'bigint', [ - 'notnull' => false, - 'length' => 64, - 'default' => 0, + 'notnull' => false, + 'length' => 64, + 'default' => 0, 'unsigned' => true, ]); $table->setPrimaryKey(['id']); } + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $this->vaultsTable($schema); + $this->credentialsTable($schema); + $this->filesTable($schema); + $this->revisionsTable($schema); + $this->sharingAclTable($schema); + $this->shareRequestsTable($schema); + $this->deleteVaultRequestsTable($schema); + return $schema; } From 04a5eb1103b8f1673924a1869a52048ccebc9e75 Mon Sep 17 00:00:00 2001 From: binsky Date: Sun, 11 Jan 2026 20:21:21 +0100 Subject: [PATCH 2/3] migrate database table mappers to use our new table prefix 'passman_next_' --- lib/Db/CredentialMapper.php | 2 +- lib/Db/CredentialRevisionMapper.php | 2 +- lib/Db/DeleteVaultRequestMapper.php | 2 +- lib/Db/FileMapper.php | 2 +- lib/Db/ShareRequestMapper.php | 2 +- lib/Db/SharingACLMapper.php | 4 ++-- lib/Db/VaultMapper.php | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/Db/CredentialMapper.php b/lib/Db/CredentialMapper.php index 075619cf..2b459616 100644 --- a/lib/Db/CredentialMapper.php +++ b/lib/Db/CredentialMapper.php @@ -35,7 +35,7 @@ * @template-extends QBMapper */ class CredentialMapper extends QBMapper { - const TABLE_NAME = 'passman_credentials'; + const TABLE_NAME = 'passman_next_credentials'; public function __construct( IDBConnection $db, diff --git a/lib/Db/CredentialRevisionMapper.php b/lib/Db/CredentialRevisionMapper.php index 7b2797ef..a69dcdcd 100644 --- a/lib/Db/CredentialRevisionMapper.php +++ b/lib/Db/CredentialRevisionMapper.php @@ -34,7 +34,7 @@ * @template-extends QBMapper */ class CredentialRevisionMapper extends QBMapper { - const TABLE_NAME = 'passman_revisions'; + const TABLE_NAME = 'passman_next_revisions'; public function __construct(IDBConnection $db, private readonly Utils $utils) { parent::__construct($db, self::TABLE_NAME); diff --git a/lib/Db/DeleteVaultRequestMapper.php b/lib/Db/DeleteVaultRequestMapper.php index efb7c542..c59949b7 100644 --- a/lib/Db/DeleteVaultRequestMapper.php +++ b/lib/Db/DeleteVaultRequestMapper.php @@ -34,7 +34,7 @@ * @template-extends QBMapper */ class DeleteVaultRequestMapper extends QBMapper { - const TABLE_NAME = 'passman_delete_vault_request'; + const TABLE_NAME = 'passman_next_delete_vault_request'; public function __construct(IDBConnection $db) { parent::__construct($db, self::TABLE_NAME); diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php index 486c6b01..b525983b 100644 --- a/lib/Db/FileMapper.php +++ b/lib/Db/FileMapper.php @@ -35,7 +35,7 @@ * @template-extends QBMapper */ class FileMapper extends QBMapper { - const TABLE_NAME = 'passman_files'; + const TABLE_NAME = 'passman_next_files'; public function __construct( IDBConnection $db, diff --git a/lib/Db/ShareRequestMapper.php b/lib/Db/ShareRequestMapper.php index 72c6b144..04956fb4 100644 --- a/lib/Db/ShareRequestMapper.php +++ b/lib/Db/ShareRequestMapper.php @@ -35,7 +35,7 @@ * @template-extends QBMapper */ class ShareRequestMapper extends QBMapper { - const TABLE_NAME = 'passman_share_request'; + const TABLE_NAME = 'passman_next_share_request'; public function __construct(IDBConnection $db) { parent::__construct($db, self::TABLE_NAME); diff --git a/lib/Db/SharingACLMapper.php b/lib/Db/SharingACLMapper.php index 9f30410c..b4417d34 100644 --- a/lib/Db/SharingACLMapper.php +++ b/lib/Db/SharingACLMapper.php @@ -34,10 +34,10 @@ * @template-extends QBMapper */ class SharingACLMapper extends QBMapper { - const TABLE_NAME = 'passman_sharing_acl'; + const TABLE_NAME = 'passman_next_sharing_acl'; public function __construct(IDBConnection $db) { - parent::__construct($db, 'passman_sharing_acl'); + parent::__construct($db, self::TABLE_NAME); } /** diff --git a/lib/Db/VaultMapper.php b/lib/Db/VaultMapper.php index 21d5ca5c..e7f1d95b 100644 --- a/lib/Db/VaultMapper.php +++ b/lib/Db/VaultMapper.php @@ -34,7 +34,7 @@ * @template-extends QBMapper */ class VaultMapper extends QBMapper { - const TABLE_NAME = 'passman_vaults'; + const TABLE_NAME = 'passman_next_vaults'; public function __construct( IDBConnection $db, From 253d32dcdcc9417ec0395ca7e13b9bc8199433f4 Mon Sep 17 00:00:00 2001 From: binsky Date: Sun, 11 Jan 2026 20:23:25 +0100 Subject: [PATCH 3/3] add command 'passman-next:migrate-legacy' to migrate data from a legacy Passman installation into the new Passman Next tables --- appinfo/info.xml | 10 +- lib/Command/AbstractInteractiveCommand.php | 96 ++++++++ lib/Command/PassmanLegacyMigrateCommand.php | 223 ++++++++++++++++++ .../NonInteractiveShellException.php | 8 + 4 files changed, 331 insertions(+), 6 deletions(-) create mode 100644 lib/Command/AbstractInteractiveCommand.php create mode 100644 lib/Command/PassmanLegacyMigrateCommand.php create mode 100644 lib/Exception/NonInteractiveShellException.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 1a73cd6b..da4c1683 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -50,12 +50,6 @@ For a demo of this app visit [https://demo.passman.cc](https://demo.passman.cc) OCA\PassmanNext\BackgroundJob\ExpireCredentials - - - OCA\PassmanNext\Migration\ServerSideEncryption - - - Passwords @@ -64,6 +58,10 @@ For a demo of this app visit [https://demo.passman.cc](https://demo.passman.cc) + + OCA\PassmanNext\Command\PassmanLegacyMigrateCommand + + OCA\PassmanNext\Settings\Admin OCA\PassmanNext\Settings\AdminSection diff --git a/lib/Command/AbstractInteractiveCommand.php b/lib/Command/AbstractInteractiveCommand.php new file mode 100644 index 00000000..c8ed19b7 --- /dev/null +++ b/lib/Command/AbstractInteractiveCommand.php @@ -0,0 +1,96 @@ +. + * + */ + +namespace OCA\PassmanNext\Command; + +use OCA\PassmanNext\Exception\NonInteractiveShellException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; + +/** + * Class AbstractInteractiveCommand + * + * @package OCA\Passwords\Command + */ +abstract class AbstractInteractiveCommand extends Command +{ + + /** + * AbstractInteractiveCommand constructor. + * + * @param string|null $name + */ + public function __construct(?string $name = null) { + parent::__construct($name); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + * @throws NonInteractiveShellException + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + if (!$input->isInteractive() && !$input->getOption('no-interaction')) { + throw new NonInteractiveShellException(); + } elseif (!$input->isInteractive()) { + $output->writeln('"--no-interaction" is set, will assume yes for all questions.'); + $output->writeln(''); + } + + return 0; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @param string $description + * + * @return bool + */ + protected function requestConfirmation(InputInterface $input, OutputInterface $output, string $description): bool { + $output->writeln("❗❗❗ {$description} ❗❗❗"); + if (!$input->isInteractive()) { + $output->writeln(''); + return true; + } + + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new Question('Type "yes" to confirm this: '); + $yes = $helper->ask($input, $output, $question); + $output->writeln(''); + + if ($yes !== 'yes') { + $output->writeln('aborting'); + + return false; + } + + return true; + } +} diff --git a/lib/Command/PassmanLegacyMigrateCommand.php b/lib/Command/PassmanLegacyMigrateCommand.php new file mode 100644 index 00000000..822af18d --- /dev/null +++ b/lib/Command/PassmanLegacyMigrateCommand.php @@ -0,0 +1,223 @@ +. + * + */ + +namespace OCA\PassmanNext\Command; + +use OCA\PassmanNext\Exception\NonInteractiveShellException; +use OCP\DB\Exception; +use OCP\IAppConfig; +use OCP\IDBConnection; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class PassmanLegacyMigrateCommand extends AbstractInteractiveCommand +{ + const OLD_TABLE_PREFIX = 'passman_'; + const NEW_TABLE_PREFIX = 'passman_next_'; + const MIGRATE_TABLE_NAMES = [ + 'vaults', + 'credentials', + 'files', + 'revisions', + 'sharing_acl', + 'share_request', + 'delete_vault_request' + ]; + const CHECK_ICON = "✅"; + const PROBLEM_ICON = "❗"; + const MINIMUM_PASSMAN_LEGACY_VERSION = "2.4.0"; + + /** + * PassmanLegacyMigrateCommand constructor. + * + * @param IDBConnection $db + * @param IAppConfig $config + * @param string|null $name + */ + public function __construct( + private readonly IDBConnection $db, + private readonly IAppConfig $config, + ?string $name = null + ) { + parent::__construct($name); + } + + + protected function configure(): void { + $this->setName('passman-next:migrate-legacy') + ->setDescription('Migrates all data from the Passman (legacy) app into the Passman Next database tables'); + parent::configure(); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + * @throws NonInteractiveShellException|Exception + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + parent::execute($input, $output); + + $installedPassmanVersion = $this->config->getValueString('passman', 'installed_version'); + $passmanLegacyVersionSupported = false; + if (version_compare($installedPassmanVersion, self::MINIMUM_PASSMAN_LEGACY_VERSION, '>=')) { + $passmanLegacyVersionSupported = true; + } + $output->writeln(sprintf( + "Found legacy Passman %s installation %s", + $installedPassmanVersion, + $passmanLegacyVersionSupported ? self::CHECK_ICON : self::PROBLEM_ICON + )); + if (!$passmanLegacyVersionSupported) { + $output->writeln('Supported Passman versions: >= ' . self::MINIMUM_PASSMAN_LEGACY_VERSION); + return 1; + } + + $oldTableEntriesCountMap = []; + $newDataTableEntries = 0; + + $output->writeln("\nData to migrate:"); + foreach (self::MIGRATE_TABLE_NAMES as $mainTableName) { + $oldTableEntriesCount = $this->countAll(self::OLD_TABLE_PREFIX . $mainTableName); + $newTableEntriesCount = $this->countAll(self::NEW_TABLE_PREFIX . $mainTableName); + + $output->writeln( + sprintf( + "- %s: \n - Legacy: %d \n - Passman Next: %d %s", + $mainTableName, + $oldTableEntriesCount, + $newTableEntriesCount, + $newTableEntriesCount === 0 ? self::CHECK_ICON : self::PROBLEM_ICON + ) + ); + + $newDataTableEntries += $newTableEntriesCount; + $oldTableEntriesCountMap[$mainTableName] = $oldTableEntriesCount; + } + + if ($newDataTableEntries !== 0) { + $output->writeln( + "\n❗❗❗ You have already data in the Passman Next database tables. These will be deleted if you proceed! ❗❗❗\n" + ); + } + + if ($this->confirmMigration($input, $output)) { + try { + $this->db->beginTransaction(); + + foreach (self::MIGRATE_TABLE_NAMES as $mainTableName) { + $this->migrateTableData( + self::OLD_TABLE_PREFIX . $mainTableName, + self::NEW_TABLE_PREFIX . $mainTableName + ); + + $newCount = $this->countAll(self::NEW_TABLE_PREFIX . $mainTableName); + if ($oldTableEntriesCountMap[$mainTableName] !== $newCount) { + // new table entries count does not match the original ones + $output->writeln( + sprintf( + 'New table entries count in "%s" (%d) does not match the original ones (%d)', + self::NEW_TABLE_PREFIX . $mainTableName, + $newCount, + $oldTableEntriesCountMap[$mainTableName] + ) + ); + } + } + + $this->db->commit(); + } catch (\Exception $exception) { + $output->writeln($exception->getMessage()); + $output->writeln("\nRoll back transaction ..."); + $this->db->rollBack(); + $output->writeln("No data has been changed."); + return 1; + } + + $output->writeln('Done'); + return 0; + } + + return 1; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return bool + */ + protected function confirmMigration(InputInterface $input, OutputInterface $output): bool { + return $this->requestConfirmation( + $input, + $output, + 'Please confirm the data migration. It won\'t delete anything in the sources.' + ); + } + + /** + * KEEP THIS METHOD PRIVATE!!! + * + * @param string $table + * @return array + * @throws Exception + */ + private function fetchAll(string $table): array { + $qb = $this->db->getQueryBuilder(); + $result = $qb->select('*') + ->from($table) + ->executeQuery(); + return $result->fetchAll(); + } + + private function countAll(string $table, string $uniqueColumn = 'id'): int { + $qb = $this->db->getQueryBuilder(); + $result = $qb->select($uniqueColumn) + ->from($table) + ->executeQuery(); + return $result->rowCount(); + } + + private function migrateTableData(string $legacyTableName, string $newTableName) { + // ensure the destination table is cleared + $deleteQueryBuilder = $this->db->getQueryBuilder(); + $deleteQueryBuilder->delete($newTableName)->executeStatement(); + + $data = $this->fetchAll($legacyTableName); + foreach ($data as $datum) { + $qb = $this->db->getQueryBuilder(); + $qb->insert($newTableName); + + foreach ($datum as $key => $value) { + if (is_null($value)) { + $value = 'NULL'; + } elseif (!is_numeric($value)) { + $value = "'{$value}'"; + } + + $qb->setValue($key, $value); + } + + $qb->executeStatement(); + } + } +} diff --git a/lib/Exception/NonInteractiveShellException.php b/lib/Exception/NonInteractiveShellException.php new file mode 100644 index 00000000..551661d6 --- /dev/null +++ b/lib/Exception/NonInteractiveShellException.php @@ -0,0 +1,8 @@ +