Support opening remote databases (#10896)

* Use the import wizard to support opening a remote database

---------

Co-authored-by: Jonathan White <support@dmapps.us>
This commit is contained in:
sforst 2024-10-07 04:39:50 +02:00 committed by GitHub
parent abcb1414a3
commit d2da13da20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 379 additions and 62 deletions

View File

@ -11,6 +11,7 @@ KeePassXC allows you to import external databases from the following options:
* 1Password Vault (.opvault) * 1Password Vault (.opvault)
* Bitwarden (.json) * Bitwarden (.json)
* KeePass 1 Database (.kdb) * KeePass 1 Database (.kdb)
* Remote database (.kdbx)
To import any of these files, start KeePassXC and either click the `Import File` button on the welcome screen or use the menu Database > Import... to launch the Import Wizard. To import any of these files, start KeePassXC and either click the `Import File` button on the welcome screen or use the menu Database > Import... to launch the Import Wizard.
@ -67,6 +68,21 @@ To import a KeePass 1 database file in KeePassXC, perform the following steps:
3. Click `Continue` to unlock and preview the import. Click `Done` to complete the import. 3. Click `Continue` to unlock and preview the import. Click `Done` to complete the import.
=== Importing Remote Database
Database files that are stored in a remote location can be imported or opened with KeePassXC if you provide a command to download the file from the remote location.
To import (or temporarily open) a remote database file in KeePassXC, perform the following steps:
1. Open the Import Wizard as shown above. Select the Remote Database option.
2. Enter a command to download the remote database. If necessary, enter input that needs to be passed to the command. The command and/or input need a `{TEMP_DATABASE}` placeholder specified where the remote database is temporarily stored.
3. Enter the password for your database and optionally provide a key file.
4. Click `Continue` to unlock and preview the import. Click `Done` to complete the import.
Opening without importing a remote database is possible by selecting Temporary Database in the Import Into section of the wizard.
== Exporting Databases == Exporting Databases
KeePassXC supports multiple ways to export your database for transfer to another program or to print out and archive. KeePassXC supports multiple ways to export your database for transfer to another program or to print out and archive.

View File

@ -4490,6 +4490,14 @@ You can enable the DuckDuckGo website icon service in the security section of th
<source>Url</source> <source>Url</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Could not load key file.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not open remote database. Password or key file may be incorrect.</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>ImportWizardPageSelect</name> <name>ImportWizardPageSelect</name>
@ -4593,6 +4601,36 @@ You can enable the DuckDuckGo website icon service in the security section of th
<source>KeePass1 Database</source> <source>KeePass1 Database</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Temporary Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Command:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>e.g.: &quot;sftp user@hostname&quot; or &quot;scp user@hostname:DatabaseOnRemote.kdbx {TEMP_DATABASE}&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Input:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remote Database (.kdbx)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>e.g.:
get DatabaseOnRemote.kdbx {TEMP_DATABASE}
exit
---
{TEMP_DATABASE} is used as placeholder to store the database in a temporary location
The command has to exit. In case of `sftp` as last commend `exit` has to be sent
</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>KMessageWidget</name> <name>KMessageWidget</name>

View File

@ -35,13 +35,13 @@
#include "gui/osutils/macutils/MacUtils.h" #include "gui/osutils/macutils/MacUtils.h"
#endif #endif
#include "gui/wizard/NewDatabaseWizard.h" #include "gui/wizard/NewDatabaseWizard.h"
#include "wizard/ImportWizard.h"
DatabaseTabWidget::DatabaseTabWidget(QWidget* parent) DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
: QTabWidget(parent) : QTabWidget(parent)
, m_dbWidgetStateSync(new DatabaseWidgetStateSync(this)) , m_dbWidgetStateSync(new DatabaseWidgetStateSync(this))
, m_dbWidgetPendingLock(nullptr) , m_dbWidgetPendingLock(nullptr)
, m_databaseOpenDialog(new DatabaseOpenDialog(this)) , m_databaseOpenDialog(new DatabaseOpenDialog(this))
, m_importWizard(nullptr)
, m_databaseOpenInProgress(false) , m_databaseOpenInProgress(false)
{ {
auto* tabBar = new QTabBar(this); auto* tabBar = new QTabBar(this);
@ -255,53 +255,65 @@ void DatabaseTabWidget::addDatabaseTab(DatabaseWidget* dbWidget, bool inBackgrou
&DatabaseTabWidget::unlockDatabaseInDialogForSync); &DatabaseTabWidget::unlockDatabaseInDialogForSync);
} }
DatabaseWidget* DatabaseTabWidget::importFile() void DatabaseTabWidget::importFile()
{ {
// Show the import wizard // Show the import wizard
QScopedPointer wizard(new ImportWizard(this)); m_importWizard = new ImportWizard(this);
if (!wizard->exec()) {
return nullptr;
}
auto db = wizard->database(); connect(m_importWizard.data(), &QWizard::finished, [&](int result) {
if (!db) { if (result != QDialog::Accepted) {
// Import wizard was cancelled return;
return nullptr;
}
auto importInto = wizard->importInto();
if (importInto.first.isNull()) {
// Start the new database wizard with the imported database
auto newDb = execNewDatabaseWizard();
if (newDb) {
// Merge the imported db into the new one
Merger merger(db.data(), newDb.data());
merger.setSkipDatabaseCustomData(true);
merger.merge();
// Show the new database
auto dbWidget = new DatabaseWidget(newDb, this);
addDatabaseTab(dbWidget);
newDb->markAsModified();
return dbWidget;
} }
} else {
for (int i = 0, c = count(); i < c; ++i) { auto db = m_importWizard->database();
// Find the database and group to import into based on import wizard choice if (!db) {
auto dbWidget = databaseWidgetFromIndex(i); // Import wizard was cancelled
if (!dbWidget->isLocked() && dbWidget->database()->uuid() == importInto.first) { return;
auto group = dbWidget->database()->rootGroup()->findGroupByUuid(importInto.second); }
if (group) {
// Extract the root group from the import database switch (m_importWizard->importIntoType()) {
auto importGroup = db->setRootGroup(new Group()); case ImportWizard::EXISTING_DATABASE:
importGroup->setParent(group); for (int i = 0, c = count(); i < c; ++i) {
setCurrentIndex(i); auto importInto = m_importWizard->importInto();
return dbWidget; // Find the database and group to import into based on import wizard choice
auto dbWidget = databaseWidgetFromIndex(i);
if (!dbWidget->isLocked() && dbWidget->database()->uuid() == importInto.first) {
auto group = dbWidget->database()->rootGroup()->findGroupByUuid(importInto.second);
if (group) {
// Extract the root group from the import database
auto importGroup = db->setRootGroup(new Group());
importGroup->setParent(group);
setCurrentIndex(i);
return;
}
} }
} }
break;
case ImportWizard::TEMPORARY_DATABASE: {
// Use the already created database as temporary database
auto dbWidget = new DatabaseWidget(db, this);
addDatabaseTab(dbWidget);
return;
} }
} default:
// Start the new database wizard with the imported database
auto newDb = execNewDatabaseWizard();
if (newDb) {
// Merge the imported db into the new one
Merger merger(db.data(), newDb.data());
merger.setSkipDatabaseCustomData(true);
merger.merge();
// Show the new database
auto dbWidget = new DatabaseWidget(newDb, this);
addDatabaseTab(dbWidget);
newDb->markAsModified();
return;
}
}
});
return nullptr; // use `open` instead of `exec`. `exec` should not be used, see https://doc.qt.io/qt-6/qdialog.html#exec
m_importWizard->show();
} }
void DatabaseTabWidget::mergeDatabase() void DatabaseTabWidget::mergeDatabase()

View File

@ -21,6 +21,7 @@
#include "DatabaseOpenDialog.h" #include "DatabaseOpenDialog.h"
#include "config-keepassx.h" #include "config-keepassx.h"
#include "gui/MessageWidget.h" #include "gui/MessageWidget.h"
#include "wizard/ImportWizard.h"
#include <QTabWidget> #include <QTabWidget>
#include <QTimer> #include <QTimer>
@ -64,7 +65,7 @@ public slots:
DatabaseWidget* newDatabase(); DatabaseWidget* newDatabase();
void openDatabase(); void openDatabase();
void mergeDatabase(); void mergeDatabase();
DatabaseWidget* importFile(); void importFile();
bool saveDatabase(int index = -1); bool saveDatabase(int index = -1);
bool saveDatabaseAs(int index = -1); bool saveDatabaseAs(int index = -1);
bool saveDatabaseBackup(int index = -1); bool saveDatabaseBackup(int index = -1);
@ -123,6 +124,7 @@ private:
QPointer<DatabaseWidgetStateSync> m_dbWidgetStateSync; QPointer<DatabaseWidgetStateSync> m_dbWidgetStateSync;
QPointer<DatabaseWidget> m_dbWidgetPendingLock; QPointer<DatabaseWidget> m_dbWidgetPendingLock;
QPointer<DatabaseOpenDialog> m_databaseOpenDialog; QPointer<DatabaseOpenDialog> m_databaseOpenDialog;
QPointer<ImportWizard> m_importWizard;
QTimer m_lockDelayTimer; QTimer m_lockDelayTimer;
bool m_databaseOpenInProgress; bool m_databaseOpenInProgress;
}; };

View File

@ -69,6 +69,11 @@ bool ImportWizard::validateCurrentPage()
return ret; return ret;
} }
ImportWizard::ImportIntoType ImportWizard::importIntoType()
{
return static_cast<ImportIntoType>(field("ImportIntoType").toInt());
}
QPair<QUuid, QUuid> ImportWizard::importInto() QPair<QUuid, QUuid> ImportWizard::importInto()
{ {
auto list = field("ImportInto").toList(); auto list = field("ImportInto").toList();

View File

@ -19,6 +19,7 @@
#define KEEPASSXC_IMPORTWIZARD_H #define KEEPASSXC_IMPORTWIZARD_H
#include <QPointer> #include <QPointer>
#include <QUuid>
#include <QWizard> #include <QWizard>
class Database; class Database;
@ -39,7 +40,6 @@ public:
bool validateCurrentPage() override; bool validateCurrentPage() override;
QSharedPointer<Database> database(); QSharedPointer<Database> database();
QPair<QUuid, QUuid> importInto();
enum ImportType enum ImportType
{ {
@ -48,9 +48,20 @@ public:
IMPORT_OPVAULT, IMPORT_OPVAULT,
IMPORT_OPUX, IMPORT_OPUX,
IMPORT_BITWARDEN, IMPORT_BITWARDEN,
IMPORT_KEEPASS1 IMPORT_KEEPASS1,
IMPORT_REMOTE,
}; };
enum ImportIntoType
{
NEW_DATABASE = 1,
EXISTING_DATABASE,
TEMPORARY_DATABASE,
};
ImportWizard::ImportIntoType importIntoType();
QPair<QUuid, QUuid> importInto();
private: private:
QSharedPointer<Database> m_db; QSharedPointer<Database> m_db;
QPointer<ImportWizardPageSelect> m_pageSelect; QPointer<ImportWizardPageSelect> m_pageSelect;

View File

@ -27,14 +27,23 @@
#include "gui/csvImport/CsvImportWidget.h" #include "gui/csvImport/CsvImportWidget.h"
#include "gui/wizard/ImportWizard.h" #include "gui/wizard/ImportWizard.h"
#include "cli/Utils.h"
#include "keys/FileKey.h"
#include "keys/PasswordKey.h"
#include <QBoxLayout> #include <QBoxLayout>
#include <QDir> #include <QDir>
#include <QHeaderView> #include <QHeaderView>
#include <QTableWidget> #include <QTableWidget>
#include "gui/remote/RemoteSettings.h"
struct RemoteParams;
ImportWizardPageReview::ImportWizardPageReview(QWidget* parent) ImportWizardPageReview::ImportWizardPageReview(QWidget* parent)
: QWizardPage(parent) : QWizardPage(parent)
, m_ui(new Ui::ImportWizardPageReview) , m_ui(new Ui::ImportWizardPageReview)
, m_remoteHandler(new RemoteHandler(this))
{ {
} }
@ -80,6 +89,12 @@ void ImportWizardPageReview::initializePage()
m_db = importBitwarden(filename, field("ImportPassword").toString()); m_db = importBitwarden(filename, field("ImportPassword").toString());
setupDatabasePreview(); setupDatabasePreview();
break; break;
case ImportWizard::IMPORT_REMOTE:
m_db = importRemote(field("DownloadCommand").toString(),
field("DownloadInput").toString(),
field("ImportPassword").toString(),
field("ImportKeyFile").toString());
setupDatabasePreview();
default: default:
break; break;
} }
@ -200,3 +215,43 @@ ImportWizardPageReview::importKeePass1(const QString& filename, const QString& p
return db; return db;
} }
QSharedPointer<Database> ImportWizardPageReview::importRemote(const QString& downloadCommand,
const QString& downloadInput,
const QString& password,
const QString& keyfile)
{
auto* params = new RemoteParams();
params->downloadCommand = downloadCommand;
params->downloadInput = downloadInput;
auto result = m_remoteHandler->download(params);
if (!result.success) {
m_ui->messageWidget->showMessage(result.errorMessage, KMessageWidget::Error, -1);
}
auto key = QSharedPointer<CompositeKey>::create();
if (!password.isEmpty()) {
key->addKey(QSharedPointer<PasswordKey>::create(password));
}
if (!keyfile.isEmpty()) {
QSharedPointer<FileKey> fileKey = QSharedPointer<FileKey>::create();
if (Utils::loadFileKey(keyfile, fileKey)) {
key->addKey(fileKey);
} else {
m_ui->messageWidget->showMessage(tr("Could not load key file."), KMessageWidget::Error, -1);
}
}
QString error;
QSharedPointer<Database> remoteDb = QSharedPointer<Database>::create();
remoteDb->markAsTemporaryDatabase();
if (!remoteDb->open(result.filePath, key, &error)) {
m_ui->messageWidget->showMessage(
tr("Could not open remote database. Password or key file may be incorrect."), KMessageWidget::Error, -1);
}
return remoteDb;
}

View File

@ -21,6 +21,12 @@
#include <QPointer> #include <QPointer>
#include <QWizardPage> #include <QWizardPage>
#include <QLabel>
#include <QProgressBar>
#include <QStatusBar>
#include "../remote/RemoteHandler.h"
class CsvImportWidget; class CsvImportWidget;
class Database; class Database;
namespace Ui namespace Ui
@ -48,6 +54,10 @@ private:
QSharedPointer<Database> importBitwarden(const QString& filename, const QString& password); QSharedPointer<Database> importBitwarden(const QString& filename, const QString& password);
QSharedPointer<Database> importOPVault(const QString& filename, const QString& password); QSharedPointer<Database> importOPVault(const QString& filename, const QString& password);
QSharedPointer<Database> importKeePass1(const QString& filename, const QString& password, const QString& keyfile); QSharedPointer<Database> importKeePass1(const QString& filename, const QString& password, const QString& keyfile);
QSharedPointer<Database> importRemote(const QString& downloadCommand,
const QString& downloadInput,
const QString& password,
const QString& keyfile);
void setupDatabasePreview(); void setupDatabasePreview();
@ -55,6 +65,7 @@ private:
QSharedPointer<Database> m_db; QSharedPointer<Database> m_db;
QPointer<CsvImportWidget> m_csvWidget; QPointer<CsvImportWidget> m_csvWidget;
QPointer<RemoteHandler> m_remoteHandler;
}; };
#endif #endif

View File

@ -25,6 +25,8 @@
#include "gui/Icons.h" #include "gui/Icons.h"
#include "gui/MainWindow.h" #include "gui/MainWindow.h"
#include <QDesktopServices>
ImportWizardPageSelect::ImportWizardPageSelect(QWidget* parent) ImportWizardPageSelect::ImportWizardPageSelect(QWidget* parent)
: QWizardPage(parent) : QWizardPage(parent)
, m_ui(new Ui::ImportWizardPageSelect()) , m_ui(new Ui::ImportWizardPageSelect())
@ -36,12 +38,14 @@ ImportWizardPageSelect::ImportWizardPageSelect(QWidget* parent)
new QListWidgetItem(icons()->icon("onepassword"), tr("1Password Vault (.opvault)"), m_ui->importTypeList); new QListWidgetItem(icons()->icon("onepassword"), tr("1Password Vault (.opvault)"), m_ui->importTypeList);
new QListWidgetItem(icons()->icon("bitwarden"), tr("Bitwarden (.json)"), m_ui->importTypeList); new QListWidgetItem(icons()->icon("bitwarden"), tr("Bitwarden (.json)"), m_ui->importTypeList);
new QListWidgetItem(icons()->icon("object-locked"), tr("KeePass 1 Database (.kdb)"), m_ui->importTypeList); new QListWidgetItem(icons()->icon("object-locked"), tr("KeePass 1 Database (.kdb)"), m_ui->importTypeList);
new QListWidgetItem(icons()->icon("web"), tr("Remote Database (.kdbx)"), m_ui->importTypeList);
m_ui->importTypeList->item(0)->setData(Qt::UserRole, ImportWizard::IMPORT_CSV); m_ui->importTypeList->item(0)->setData(Qt::UserRole, ImportWizard::IMPORT_CSV);
m_ui->importTypeList->item(1)->setData(Qt::UserRole, ImportWizard::IMPORT_OPUX); m_ui->importTypeList->item(1)->setData(Qt::UserRole, ImportWizard::IMPORT_OPUX);
m_ui->importTypeList->item(2)->setData(Qt::UserRole, ImportWizard::IMPORT_OPVAULT); m_ui->importTypeList->item(2)->setData(Qt::UserRole, ImportWizard::IMPORT_OPVAULT);
m_ui->importTypeList->item(3)->setData(Qt::UserRole, ImportWizard::IMPORT_BITWARDEN); m_ui->importTypeList->item(3)->setData(Qt::UserRole, ImportWizard::IMPORT_BITWARDEN);
m_ui->importTypeList->item(4)->setData(Qt::UserRole, ImportWizard::IMPORT_KEEPASS1); m_ui->importTypeList->item(4)->setData(Qt::UserRole, ImportWizard::IMPORT_KEEPASS1);
m_ui->importTypeList->item(5)->setData(Qt::UserRole, ImportWizard::IMPORT_REMOTE);
connect(m_ui->importTypeList, &QListWidget::currentItemChanged, this, &ImportWizardPageSelect::itemSelected); connect(m_ui->importTypeList, &QListWidget::currentItemChanged, this, &ImportWizardPageSelect::itemSelected);
m_ui->importTypeList->setCurrentRow(0); m_ui->importTypeList->setCurrentRow(0);
@ -54,11 +58,22 @@ ImportWizardPageSelect::ImportWizardPageSelect(QWidget* parent)
updateDatabaseChoices(); updateDatabaseChoices();
m_ui->downloadCommandHelpButton->setIcon(icons()->icon("system-help"));
connect(m_ui->downloadCommandHelpButton, &QToolButton::clicked, this, [] {
QDesktopServices::openUrl(QUrl("https://keepassxc.org/docs/KeePassXC_UserGuide#_remote_database_support"));
});
connect(m_ui->importFileEdit, &QLineEdit::textChanged, this, &QWizardPage::completeChanged);
connect(m_ui->downloadCommand, &QLineEdit::textChanged, this, &QWizardPage::completeChanged);
registerField("ImportType", this); registerField("ImportType", this);
registerField("ImportFile*", m_ui->importFileEdit); registerField("ImportFile", m_ui->importFileEdit);
registerField("ImportInto", m_ui->importIntoLabel); registerField("ImportIntoType", m_ui->importIntoGroupBox); // This is intentional
registerField("ImportInto", m_ui->importIntoLabel); // This is intentional
registerField("ImportPassword", m_ui->passwordEdit, "text", "textChanged"); registerField("ImportPassword", m_ui->passwordEdit, "text", "textChanged");
registerField("ImportKeyFile", m_ui->keyFileEdit); registerField("ImportKeyFile", m_ui->keyFileEdit);
registerField("DownloadCommand", m_ui->downloadCommand);
registerField("DownloadInput", m_ui->downloadCommandInput, "plainText", "textChanged");
} }
ImportWizardPageSelect::~ImportWizardPageSelect() ImportWizardPageSelect::~ImportWizardPageSelect()
@ -77,14 +92,27 @@ bool ImportWizardPageSelect::validatePage()
if (m_ui->existingDatabaseChoice->currentIndex() == -1) { if (m_ui->existingDatabaseChoice->currentIndex() == -1) {
return false; return false;
} }
setField("ImportIntoType", ImportWizard::EXISTING_DATABASE);
setField("ImportInto", m_ui->existingDatabaseChoice->currentData()); setField("ImportInto", m_ui->existingDatabaseChoice->currentData());
} else if (m_ui->temporaryDatabaseRadio->isChecked()) {
setField("ImportIntoType", ImportWizard::TEMPORARY_DATABASE);
setField("ImportInto", {});
} else { } else {
setField("ImportIntoType", ImportWizard::NEW_DATABASE);
setField("ImportInto", {}); setField("ImportInto", {});
} }
return true; return true;
} }
bool ImportWizardPageSelect::isComplete() const
{
if (field("ImportType").toInt() == ImportWizard::IMPORT_REMOTE) {
return !field("DownloadCommand").toString().isEmpty();
}
return !field("ImportFile").toString().isEmpty();
}
void ImportWizardPageSelect::itemSelected(QListWidgetItem* current, QListWidgetItem* previous) void ImportWizardPageSelect::itemSelected(QListWidgetItem* current, QListWidgetItem* previous)
{ {
Q_UNUSED(previous) Q_UNUSED(previous)
@ -105,15 +133,22 @@ void ImportWizardPageSelect::itemSelected(QListWidgetItem* current, QListWidgetI
case ImportWizard::IMPORT_CSV: case ImportWizard::IMPORT_CSV:
case ImportWizard::IMPORT_OPUX: case ImportWizard::IMPORT_OPUX:
setCredentialState(false); setCredentialState(false);
setDownloadCommand(false);
break; break;
// Password may be required // Password may be required
case ImportWizard::IMPORT_BITWARDEN: case ImportWizard::IMPORT_BITWARDEN:
case ImportWizard::IMPORT_OPVAULT: case ImportWizard::IMPORT_OPVAULT:
setCredentialState(true); setCredentialState(true);
setDownloadCommand(false);
break; break;
// Password and/or Key File may be required // Password and/or Key File may be required
case ImportWizard::IMPORT_KEEPASS1: case ImportWizard::IMPORT_KEEPASS1:
setCredentialState(true, true); setCredentialState(true, true);
setDownloadCommand(false);
break;
case ImportWizard::IMPORT_REMOTE:
setCredentialState(true, true);
setDownloadCommand(true);
break; break;
default: default:
Q_ASSERT(false); Q_ASSERT(false);
@ -228,6 +263,33 @@ void ImportWizardPageSelect::setCredentialState(bool passwordEnabled, bool keyFi
} }
} }
void ImportWizardPageSelect::setDownloadCommand(bool downloadCommandEnabled)
{
bool downloadCommandStateChanged = m_ui->downloadCommandLabel->isVisible() != downloadCommandEnabled;
m_ui->downloadCommandLabel->setVisible(downloadCommandEnabled);
m_ui->downloadCommand->setVisible(downloadCommandEnabled);
m_ui->downloadCommandInputLabel->setVisible(downloadCommandEnabled);
m_ui->downloadCommandInput->setVisible(downloadCommandEnabled);
m_ui->downloadCommandHelpButton->setVisible(downloadCommandEnabled);
m_ui->temporaryDatabaseRadio->setVisible(downloadCommandEnabled);
m_ui->importFileLabel->setVisible(!downloadCommandEnabled);
m_ui->importFileEdit->setVisible(!downloadCommandEnabled);
m_ui->importFileButton->setVisible(!downloadCommandEnabled);
// Workaround Qt bug where the wizard window is not updated when the internal layout changes
if (window()) {
int height = window()->height();
if (downloadCommandStateChanged) {
auto diff = m_ui->downloadCommand->height() + m_ui->downloadCommandInput->height()
+ m_ui->temporaryDatabaseRadio->height() + m_ui->inputFields->layout()->spacing();
height += downloadCommandEnabled ? diff : -diff;
}
window()->resize(window()->width(), height);
}
}
QString ImportWizardPageSelect::importFileFilter() QString ImportWizardPageSelect::importFileFilter()
{ {
switch (field("ImportType").toInt()) { switch (field("ImportType").toInt()) {

View File

@ -39,6 +39,7 @@ public:
void initializePage() override; void initializePage() override;
bool validatePage() override; bool validatePage() override;
bool isComplete() const override;
private slots: private slots:
void itemSelected(QListWidgetItem* current, QListWidgetItem* previous); void itemSelected(QListWidgetItem* current, QListWidgetItem* previous);
@ -49,6 +50,7 @@ private slots:
private: private:
QString importFileFilter(); QString importFileFilter();
void setCredentialState(bool passwordEnabled, bool keyFileEnable = false); void setCredentialState(bool passwordEnabled, bool keyFileEnable = false);
void setDownloadCommand(bool downloadCommandEnabled);
QScopedPointer<Ui::ImportWizardPageSelect> m_ui; QScopedPointer<Ui::ImportWizardPageSelect> m_ui;
}; };

View File

@ -94,14 +94,14 @@
<property name="sizeConstraint"> <property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum> <enum>QLayout::SetMinimumSize</enum>
</property> </property>
<item row="0" column="0"> <item row="3" column="0">
<widget class="QLabel" name="importFileLabel"> <widget class="QLabel" name="importFileLabel">
<property name="text"> <property name="text">
<string>Import File:</string> <string>Import File:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="3" column="1">
<layout class="QHBoxLayout" name="importFileLayout"> <layout class="QHBoxLayout" name="importFileLayout">
<item> <item>
<widget class="QLineEdit" name="importFileEdit"/> <widget class="QLineEdit" name="importFileEdit"/>
@ -115,24 +115,24 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="1" column="0"> <item row="4" column="0">
<widget class="QLabel" name="passwordLabel"> <widget class="QLabel" name="passwordLabel">
<property name="text"> <property name="text">
<string>Password:</string> <string>Password:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="4" column="1">
<widget class="PasswordWidget" name="passwordEdit" native="true"/> <widget class="PasswordWidget" name="passwordEdit" native="true"/>
</item> </item>
<item row="2" column="0"> <item row="5" column="0">
<widget class="QLabel" name="keyFileLabel"> <widget class="QLabel" name="keyFileLabel">
<property name="text"> <property name="text">
<string>Key File:</string> <string>Key File:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="5" column="1">
<layout class="QHBoxLayout" name="keyFileLayout"> <layout class="QHBoxLayout" name="keyFileLayout">
<item> <item>
<widget class="QLineEdit" name="keyFileEdit"/> <widget class="QLineEdit" name="keyFileEdit"/>
@ -146,7 +146,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="3" column="1"> <item row="6" column="1">
<spacer name="verticalSpacer_3"> <spacer name="verticalSpacer_3">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -162,7 +162,7 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="4" column="0"> <item row="7" column="0">
<widget class="QLabel" name="importIntoLabel"> <widget class="QLabel" name="importIntoLabel">
<property name="text"> <property name="text">
<string>Import Into:</string> <string>Import Into:</string>
@ -172,7 +172,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="7" column="1">
<widget class="QGroupBox" name="importIntoGroupBox"> <widget class="QGroupBox" name="importIntoGroupBox">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
@ -239,9 +239,64 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QRadioButton" name="temporaryDatabaseRadio">
<property name="text">
<string>Temporary Database</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="1" column="0">
<widget class="QLabel" name="downloadCommandLabel">
<property name="text">
<string>Command:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="downloadCommandLayout">
<item>
<widget class="QLineEdit" name="downloadCommand">
<property name="placeholderText">
<string>e.g.: &quot;sftp user@hostname&quot; or &quot;scp user@hostname:DatabaseOnRemote.kdbx {TEMP_DATABASE}&quot;</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="downloadCommandHelpButton">
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="downloadCommandInputLabel">
<property name="text">
<string>Input:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPlainTextEdit" name="downloadCommandInput">
<property name="placeholderText">
<string>e.g.:
get DatabaseOnRemote.kdbx {TEMP_DATABASE}
exit
---
{TEMP_DATABASE} is used as placeholder to store the database in a temporary location
The command has to exit. In case of `sftp` as last commend `exit` has to be sent
</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

Binary file not shown.

View File

@ -30,6 +30,7 @@
#include <QRadioButton> #include <QRadioButton>
#include <QSignalSpy> #include <QSignalSpy>
#include <QSpinBox> #include <QSpinBox>
#include <QTableWidget>
#include <QTest> #include <QTest>
#include <QToolBar> #include <QToolBar>
@ -397,14 +398,10 @@ void TestGui::prepareAndTriggerRemoteSync(const QString& sourceToSync)
QVERIFY(saveSettingsButton != nullptr); QVERIFY(saveSettingsButton != nullptr);
QTest::mouseClick(saveSettingsButton, Qt::LeftButton); QTest::mouseClick(saveSettingsButton, Qt::LeftButton);
// find and click dialog OK button auto okButton = dbSettingsDialog->findChild<QDialogButtonBox*>("buttonBox")->button(QDialogButtonBox::Ok);
auto buttons = dbSettingsDialog->findChild<QDialogButtonBox*>()->findChildren<QPushButton*>(); QVERIFY(okButton);
for (QPushButton* b : buttons) { QTest::mouseClick(okButton, Qt::LeftButton);
if (b->text() == "OK") {
QTest::mouseClick(b, Qt::LeftButton);
break;
}
}
QTRY_COMPARE(m_dbWidget->getRemoteParams().size(), 1); QTRY_COMPARE(m_dbWidget->getRemoteParams().size(), 1);
// trigger aboutToShow to create remote actions // trigger aboutToShow to create remote actions
@ -477,6 +474,56 @@ void TestGui::testRemoteSyncDatabaseRequiresPassword()
QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1); QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
} }
void TestGui::testOpenRemoteDatabase()
{
// close current database
cleanup();
QString sourceToSync = "sftp user@server:Database.kdbx";
RemoteHandler::setRemoteProcessFunc([sourceToSync](QObject* parent) {
return QScopedPointer<RemoteProcess>(
new MockRemoteProcess(parent, QString(KEEPASSX_TEST_DATA_DIR).append("/SyncDatabase.kdbx")));
});
auto* openRemoteButton = QApplication::activeWindow()->findChild<QPushButton*>("buttonImport");
QVERIFY(openRemoteButton);
QVERIFY(openRemoteButton->isVisible());
QTest::mouseClick(openRemoteButton, Qt::LeftButton);
QApplication::processEvents();
TEST_MODAL_NO_WAIT(
ImportWizard * wizard; QTRY_VERIFY(wizard = m_tabWidget->findChild<ImportWizard*>());
auto* importTypeList = wizard->currentPage()->findChild<QListWidget*>("importTypeList");
QVERIFY(importTypeList);
importTypeList->scrollToBottom();
QListWidgetItem* remoteOption = importTypeList->item(importTypeList->count() - 1);
QRect remoteOptionRect = importTypeList->visualItemRect(remoteOption);
QTest::mouseClick(importTypeList->viewport(), Qt::LeftButton, nullptr, remoteOptionRect.center());
auto* downloadCommandEdit = wizard->currentPage()->findChild<QLineEdit*>("downloadCommand");
QVERIFY(downloadCommandEdit);
QTest::keyClicks(downloadCommandEdit, sourceToSync);
auto* temporaryDatabaseRadio = wizard->currentPage()->findChild<QRadioButton*>("temporaryDatabaseRadio");
QVERIFY(temporaryDatabaseRadio);
QTest::mouseClick(temporaryDatabaseRadio, Qt::LeftButton);
auto* passwordEdit = wizard->currentPage()->findChild<QLineEdit*>("passwordEdit");
QVERIFY(passwordEdit);
QTest::keyClicks(passwordEdit, "a");
QTest::keyClick(passwordEdit, Qt::Key_Enter);
QApplication::processEvents();
QVERIFY(wizard->currentPage()->findChildren<QTableWidget*>().count() > 0);
QTest::keyClick(passwordEdit, Qt::Key_Enter););
// remote database has been opened
QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("SyncDatabase [Temporary]"));
}
void TestGui::testAutoreloadDatabase() void TestGui::testAutoreloadDatabase()
{ {
config()->set(Config::AutoReloadOnChange, false); config()->set(Config::AutoReloadOnChange, false);

View File

@ -42,6 +42,7 @@ private slots:
void testMergeDatabase(); void testMergeDatabase();
void testRemoteSyncDatabaseSameKey(); void testRemoteSyncDatabaseSameKey();
void testRemoteSyncDatabaseRequiresPassword(); void testRemoteSyncDatabaseRequiresPassword();
void testOpenRemoteDatabase();
void testAutoreloadDatabase(); void testAutoreloadDatabase();
void testTabs(); void testTabs();
void testEditEntry(); void testEditEntry();