Support remote database access using external tools (#7222)

* Provide remote database sync capability

Allow arbitrary commands to be defined and executed for syncing databases with remote services. This includes sftp, scp, rsync, etc. 

Remote commands are stored per-database and sync operations are manually triggered by the user from the Database -> Remote Sync menu. 

---------

Co-authored-by: Stefan Forstenlechner <t-h-e@users.noreply.github.com>
Co-authored-by: Jonathan White <support@dmapps.us>
This commit is contained in:
sforst 2024-06-13 12:23:41 +02:00 committed by GitHub
parent ad8a00d56b
commit 1ca607792d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1780 additions and 21 deletions

View File

@ -203,6 +203,7 @@ Files: share/icons/application/scalable/actions/application-exit.svg
share/icons/application/scalable/actions/password-show-on.svg
share/icons/application/scalable/actions/qrcode.svg
share/icons/application/scalable/actions/refresh.svg
share/icons/application/scalable/actions/remote-sync.svg
share/icons/application/scalable/actions/reports.svg
share/icons/application/scalable/actions/reports-exclude.svg
share/icons/application/scalable/actions/sort-alphabetical-ascending.svg

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -389,6 +389,24 @@ You will be asked to enter the HMAC key you created earlier, copy/paste they key
== Command Line Tool
KeePassXC comes with the command line tool *keepassxc-cli* to access, view, and manipulate your database directly from a terminal window. The tool is documented through a separate man page, which can be shown using `man keepassxc-cli`, or through the on-demand help using `keepassxc-cli [command] -h`. An online version of the man page is https://github.com/keepassxreboot/keepassxc/blob/master/docs/man/keepassxc-cli.1.adoc[available on GitHub].
== Remote database support
KeePassXC provides support for syncing database files that reside in a remote location. If you can download/upload the database file via a commandline tool (e.g. rsync, ssh, scp etc.) KeePassXC offers easy to use functionality to sync the remote database.
=== Sync with remote database
Open the remote sync settings via _Database > Database Settings… > Remote_ to create commands to sync a local database or a temporary local copy of a remote database.
Define a name for your sync command and specify a download *(A)* as well as an upload command *(B)*. The command and/or input need a `{TEMP_DATABASE}` placeholder specified where the remote database is temporarily stored. Do not forget to save the command settings with the save button *\(C)*. Remote settings are added as menu entries below the _Remote Sync…_ menu for quick access.
WARNING: If your download or upload command require a password prompt, the command will most likely not succeed. In case of an SSH connection (e.g. sftp), it is recommended to use <<KeePassXC SSH Agent integration,SSH agent>> so that no password prompt is needed.
.Remote sync settings
image::sync_remote_settings.png[]
Select the remote sync command from the _Database > Remote Sync…_ menu to start the syncing process and a progress bar will show up in the lower right corner.
WARNING: In case the remote database is changed by another user/process after the downloading command finishes and before uploading again, those changes will be overwritten. Syncing is not an atomic operation.
// end::advanced[]
== Storing a Database File

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.03 18C13.08 18.7 13.24 19.38 13.5 20H6.5C5 20 3.69 19.5 2.61 18.43C1.54 17.38 1 16.09 1 14.58C1 13.28 1.39 12.12 2.17 11.1S4 9.43 5.25 9.15C5.67 7.62 6.5 6.38 7.75 5.43S10.42 4 12 4C13.95 4 15.6 4.68 16.96 6.04C18.32 7.4 19 9.05 19 11C19.04 11 19.07 11 19.1 11C18.36 11.07 17.65 11.23 17 11.5V11C17 9.62 16.5 8.44 15.54 7.46C14.56 6.5 13.38 6 12 6S9.44 6.5 8.46 7.46C7.5 8.44 7 9.62 7 11H6.5C5.53 11 4.71 11.34 4.03 12.03C3.34 12.71 3 13.53 3 14.5S3.34 16.29 4.03 17C4.71 17.66 5.53 18 6.5 18H13.03M19 13.5V12L16.75 14.25L19 16.5V15C20.38 15 21.5 16.12 21.5 17.5C21.5 17.9 21.41 18.28 21.24 18.62L22.33 19.71C22.75 19.08 23 18.32 23 17.5C23 15.29 21.21 13.5 19 13.5M19 20C17.62 20 16.5 18.88 16.5 17.5C16.5 17.1 16.59 16.72 16.76 16.38L15.67 15.29C15.25 15.92 15 16.68 15 17.5C15 19.71 16.79 21.5 19 21.5V23L21.25 20.75L19 18.5V20Z" /></svg>

After

Width:  |  Height:  |  Size: 914 B

View File

@ -69,6 +69,7 @@
<file>application/scalable/actions/password-show-on.svg</file>
<file>application/scalable/actions/qrcode.svg</file>
<file>application/scalable/actions/refresh.svg</file>
<file>application/scalable/actions/remote-sync.svg</file>
<file>application/scalable/actions/reports.svg</file>
<file>application/scalable/actions/reports-exclude.svg</file>
<file>application/scalable/actions/sort-alphabetical-ascending.svg</file>

View File

@ -1672,6 +1672,10 @@ Are you sure you want to continue with this file?.</source>
<source>Maintenance</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remote Sync</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingsWidgetBrowser</name>
@ -2241,6 +2245,121 @@ removed from the database.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingsWidgetRemote</name>
<message>
<source>Sync Commands</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remove</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Command Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Download</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Command:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Download command field</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>Download input field</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>
<message>
<source>Upload</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Upload command field</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>e.g.: &quot;sftp user@hostname&quot; or &quot;scp {TEMP_DATABASE} user@hostname:DatabaseOnRemote.kdbx&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Upload input field</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>e.g.:
put {TEMP_DATABASE} DatabaseOnRemote.kdbx
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>
<message>
<source>Name cannot be empty.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Test</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Download command cannot be empty.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Download failed with error: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Download finished, but file %1 could not be found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Download successful.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Save Remote Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>You have unsaved changes. Do you want to save them?</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseTabWidget</name>
<message>
@ -2313,6 +2432,11 @@ This is definitely a bug, please report it to the developers.</source>
<comment>Database tab name modifier</comment>
<translation type="unfinished"></translation>
</message>
<message>
<source>%1 [Temporary]</source>
<comment>Database tab name modifier</comment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseWidget</name>
@ -2505,6 +2629,34 @@ Disable safe saves and try again?</source>
<comment>Database tab name modifier</comment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remote Sync did not contain any download or upload commands.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remote sync &apos;%1&apos; completed successfully!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remote sync &apos;%1&apos; failed: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error while saving database %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Downloading...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Uploading...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Syncing...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remove passkey from entry</source>
<translation type="unfinished"></translation>
@ -5879,10 +6031,18 @@ We recommend you use the AppImage available on our downloads page.</source>
<source>Toggle Allow Screen Capture</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remote S&amp;ync</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remove Passkey From Entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Setup Remote Sync</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ManageDatabase</name>
@ -8753,6 +8913,37 @@ This option is deprecated, use --set-key-file instead.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>RemoteHandler</name>
<message>
<source>Command `%1` did not finish in time. Process was killed.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to upload merged database. Command `%1` did not finish in time. Process was killed.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid download parameters provided.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Command `%1` failed to download database.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid database pointer or upload parameters provided.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Command `%1` exited with status code: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to upload merged database. Command `%1` exited with status code: %2</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ReportsWidgetBrowserStatistics</name>
<message>

View File

@ -152,6 +152,10 @@ set(keepassx_SOURCES
gui/dbsettings/DatabaseSettingsWidgetMetaDataSimple.cpp
gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp
gui/remote/DatabaseSettingsWidgetRemote.cpp
gui/remote/RemoteHandler.cpp
gui/remote/RemoteProcess.cpp
gui/remote/RemoteSettings.cpp
gui/reports/ReportsWidget.cpp
gui/reports/ReportsDialog.cpp
gui/reports/ReportsWidgetHealthcheck.cpp

View File

@ -27,6 +27,7 @@ const QString CustomData::BrowserLegacyKeyPrefix = QStringLiteral("Public Key: "
const QString CustomData::ExcludeFromReportsLegacy = QStringLiteral("KnownBad");
const QString CustomData::FdoSecretsExposedGroup = QStringLiteral("FDO_SECRETS_EXPOSED_GROUP");
const QString CustomData::RandomSlug = QStringLiteral("KPXC_RANDOM_SLUG");
const QString CustomData::RemoteProgramSettings = QStringLiteral("KPXC_REMOTE_SYNC_SETTINGS");
// Fallback item for return by reference
static const CustomData::CustomDataItem NULL_ITEM{};
@ -190,7 +191,8 @@ void CustomData::updateLastModified(QDateTime lastModified)
bool CustomData::isProtected(const QString& key) const
{
return key.startsWith(BrowserKeyPrefix) || key == Created || key == FdoSecretsExposedGroup;
return key.startsWith(BrowserKeyPrefix) || key == Created || key == FdoSecretsExposedGroup
|| key == CustomData::RemoteProgramSettings;
}
bool CustomData::isAutoGenerated(const QString& key) const

View File

@ -71,6 +71,7 @@ public:
static const QString BrowserLegacyKeyPrefix;
static const QString FdoSecretsExposedGroup;
static const QString RandomSlug;
static const QString RemoteProgramSettings;
// Pre-KDBX 4.1
static const QString ExcludeFromReportsLegacy;

View File

@ -1049,3 +1049,13 @@ QUuid Database::publicUuid()
return QUuid::fromRfc4122(publicCustomData()["KPXC_PUBLIC_UUID"].toByteArray());
}
void Database::markAsTemporaryDatabase()
{
m_isTemporaryDatabase = true;
}
bool Database::isTemporaryDatabase()
{
return m_isTemporaryDatabase;
}

View File

@ -150,6 +150,9 @@ public:
bool changeKdf(const QSharedPointer<Kdf>& kdf);
QByteArray transformedDatabaseKey() const;
void markAsTemporaryDatabase();
bool isTemporaryDatabase();
static Database* databaseByUuid(const QUuid& uuid);
public slots:
@ -233,6 +236,7 @@ private:
bool m_modified = false;
bool m_hasNonDataChange = false;
QString m_keyError;
bool m_isTemporaryDatabase = false;
QStringList m_commonUsernames;
QStringList m_tagList;

View File

@ -201,6 +201,9 @@ void DatabaseOpenDialog::complete(bool accepted)
{
// save DB, since DatabaseOpenWidget will reset its data after accept() is called
m_db = m_view->database();
if (m_db && m_intent == Intent::RemoteSync) {
m_db->markAsTemporaryDatabase();
}
if (accepted) {
accept();
@ -211,3 +214,10 @@ void DatabaseOpenDialog::complete(bool accepted)
emit dialogFinished(accepted, m_currentDbWidget);
clearForms();
}
void DatabaseOpenDialog::closeEvent(QCloseEvent* e)
{
emit dialogFinished(false, m_currentDbWidget);
clearForms();
QDialog::closeEvent(e);
}

View File

@ -39,6 +39,7 @@ public:
None,
AutoType,
Merge,
RemoteSync,
Browser
};
@ -62,6 +63,7 @@ protected:
void showEvent(QShowEvent* event) override;
private:
void closeEvent(QCloseEvent* e) override;
void selectTabOffset(int offset);
QPointer<DatabaseOpenWidget> m_view;

View File

@ -184,7 +184,7 @@ void DatabaseTabWidget::addDatabaseTab(const QString& filePath,
auto* dbWidget = new DatabaseWidget(QSharedPointer<Database>::create(cleanFilePath), this);
addDatabaseTab(dbWidget, inBackground);
dbWidget->performUnlockDatabase(password, keyfile);
updateLastDatabases(cleanFilePath);
updateLastDatabases(dbWidget->database());
}
/**
@ -249,6 +249,10 @@ void DatabaseTabWidget::addDatabaseTab(DatabaseWidget* dbWidget, bool inBackgrou
connect(dbWidget, SIGNAL(databaseUnlocked()), SLOT(emitDatabaseLockChanged()));
connect(dbWidget, SIGNAL(databaseLocked()), SLOT(updateTabName()));
connect(dbWidget, SIGNAL(databaseLocked()), SLOT(emitDatabaseLockChanged()));
connect(dbWidget,
&DatabaseWidget::unlockDatabaseInDialogForSync,
this,
&DatabaseTabWidget::unlockDatabaseInDialogForSync);
}
DatabaseWidget* DatabaseTabWidget::importFile()
@ -416,7 +420,7 @@ bool DatabaseTabWidget::saveDatabaseAs(int index)
auto* dbWidget = databaseWidgetFromIndex(index);
bool ok = dbWidget->saveAs();
if (ok) {
updateLastDatabases(dbWidget->database()->filePath());
updateLastDatabases(dbWidget->database());
}
return ok;
}
@ -430,7 +434,7 @@ bool DatabaseTabWidget::saveDatabaseBackup(int index)
auto* dbWidget = databaseWidgetFromIndex(index);
bool ok = dbWidget->saveBackup();
if (ok) {
updateLastDatabases(dbWidget->database()->filePath());
updateLastDatabases(dbWidget->database());
}
return ok;
}
@ -619,6 +623,11 @@ QString DatabaseTabWidget::tabName(int index)
tabName = tr("%1 [Locked]", "Database tab name modifier").arg(tabName);
}
if (dbWidget->database()->isTemporaryDatabase()) {
tabName = tr("%1 [Temporary]", "Database tab name modifier").arg(tabName);
}
// needs to be last check, as MainWindow may remove the asterisk again
if (dbWidget->database()->isModified()) {
tabName.append("*");
}
@ -742,6 +751,11 @@ void DatabaseTabWidget::unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent int
displayUnlockDialog();
}
void DatabaseTabWidget::unlockDatabaseInDialogForSync(const QString& filePath)
{
unlockDatabaseInDialog(currentDatabaseWidget(), DatabaseOpenDialog::Intent::RemoteSync, filePath);
}
/**
* Display the unlock dialog after it's been initialized.
* This is an internal method, it should only be called by unlockDatabaseInDialog or unlockAnyDatabaseInDialog.
@ -768,7 +782,7 @@ void DatabaseTabWidget::handleDatabaseUnlockDialogFinished(bool accepted, Databa
{
// change the active tab to the database that was just unlocked in the dialog
auto intent = m_databaseOpenDialog->intent();
if (accepted && intent != DatabaseOpenDialog::Intent::Merge) {
if (accepted && intent != DatabaseOpenDialog::Intent::Merge && intent != DatabaseOpenDialog::Intent::RemoteSync) {
int index = indexOf(dbWidget);
if (index != -1) {
setCurrentIndex(index);
@ -803,8 +817,12 @@ void DatabaseTabWidget::relockPendingDatabase()
m_dbWidgetPendingLock = nullptr;
}
void DatabaseTabWidget::updateLastDatabases(const QString& filename)
void DatabaseTabWidget::updateLastDatabases(const QSharedPointer<Database>& database)
{
if (database->isTemporaryDatabase() || database->filePath().isEmpty()) {
return;
}
auto filename = database->filePath();
if (!config()->get(Config::RememberLastDatabases).toBool()) {
config()->remove(Config::LastDatabases);
} else {
@ -824,10 +842,7 @@ void DatabaseTabWidget::updateLastDatabases()
auto dbWidget = currentDatabaseWidget();
if (dbWidget) {
auto filePath = dbWidget->database()->filePath();
if (!filePath.isEmpty()) {
updateLastDatabases(filePath);
}
updateLastDatabases(dbWidget->database());
}
}

View File

@ -77,6 +77,7 @@ public slots:
void closeDatabaseFromSender();
void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent);
void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent, const QString& filePath);
void unlockDatabaseInDialogForSync(const QString& filePath);
void unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent intent);
void relockPendingDatabase();
@ -114,7 +115,7 @@ private slots:
private:
QSharedPointer<Database> execNewDatabaseWizard();
void updateLastDatabases(const QString& filename);
void updateLastDatabases(const QSharedPointer<Database>& database);
bool warnOnExport();
void displayUnlockDialog();

View File

@ -32,6 +32,7 @@
#include <QTextEdit>
#include "autotype/AutoType.h"
#include "core/AsyncTask.h"
#include "core/EntrySearcher.h"
#include "core/Merger.h"
#include "core/Tools.h"
@ -55,6 +56,8 @@
#include "gui/tag/TagView.h"
#include "gui/widgets/ElidedLabel.h"
#include "keeshare/KeeShare.h"
#include "remote/RemoteHandler.h"
#include "remote/RemoteSettings.h"
#ifdef WITH_XC_NETWORKING
#include "gui/IconDownloaderDialog.h"
@ -88,6 +91,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
, m_groupView(new GroupView(m_db.data(), this))
, m_tagView(new TagView(this))
, m_saveAttempts(0)
, m_remoteSettings(new RemoteSettings(m_db, this))
, m_entrySearcher(new EntrySearcher(false))
{
Q_ASSERT(m_db);
@ -460,6 +464,7 @@ void DatabaseWidget::replaceDatabase(QSharedPointer<Database> db)
connectDatabaseSignals();
m_groupView->changeDatabase(m_db);
m_tagView->setDatabase(m_db);
m_remoteSettings->setDatabase(m_db);
// Restore the new parent group pointer, if not found default to the root group
// this prevents data loss when merging a database while creating a new entry
@ -1074,6 +1079,87 @@ int DatabaseWidget::addChildWidget(QWidget* w)
return index;
}
void DatabaseWidget::syncWithRemote(const RemoteParams* params)
{
setDisabled(true);
emit databaseSyncInProgress();
QScopedPointer<RemoteHandler> remoteHandler(new RemoteHandler(this));
RemoteHandler::RemoteResult result;
result.success = false;
result.errorMessage = tr("Remote Sync did not contain any download or upload commands.");
// Download the database
if (!params->downloadCommand.isEmpty()) {
emit updateSyncProgress(25, tr("Downloading..."));
// Start a download first then merge and upload in the callback
result = remoteHandler->download(params);
if (result.success) {
QString error;
QSharedPointer<Database> remoteDb = QSharedPointer<Database>::create();
if (!remoteDb->open(result.filePath, m_db->key(), &error)) {
// Failed to open downloaded remote database with same key
// Unlock downloaded remote database via dialog
syncDatabaseWithLockedDatabase(result.filePath, params);
return;
}
remoteDb->markAsTemporaryDatabase();
if (!syncWithDatabase(remoteDb, error)) {
// Something failed during the sync process
result.success = false;
result.errorMessage = error;
}
}
}
uploadAndFinishSync(params, result);
}
void DatabaseWidget::syncDatabaseWithLockedDatabase(const QString& filePath, const RemoteParams* params)
{
// disconnect any previously added slots to these signal
disconnect(this, &DatabaseWidget::databaseSyncUnlocked, nullptr, nullptr);
disconnect(this, &DatabaseWidget::databaseSyncUnlockFailed, nullptr, nullptr);
connect(this, &DatabaseWidget::databaseSyncUnlocked, [this, params](const RemoteHandler::RemoteResult& result) {
uploadAndFinishSync(params, result);
});
connect(this, &DatabaseWidget::databaseSyncUnlockFailed, [this, params](const RemoteHandler::RemoteResult& result) {
finishSync(params, result);
});
emit unlockDatabaseInDialogForSync(filePath);
}
void DatabaseWidget::uploadAndFinishSync(const RemoteParams* params, RemoteHandler::RemoteResult result)
{
QScopedPointer<RemoteHandler> remoteHandler(new RemoteHandler(this));
if (result.success && !params->uploadCommand.isEmpty()) {
emit updateSyncProgress(75, tr("Uploading..."));
result = remoteHandler->upload(result.filePath, params);
}
finishSync(params, result);
}
void DatabaseWidget::finishSync(const RemoteParams* params, RemoteHandler::RemoteResult result)
{
setDisabled(false);
emit updateSyncProgress(-1, "");
if (result.success) {
emit databaseSyncCompleted(params->name);
showMessage(tr("Remote sync '%1' completed successfully!").arg(params->name), MessageWidget::Positive, false);
} else {
emit databaseSyncFailed(params->name, result.errorMessage);
showErrorMessage(tr("Remote sync '%1' failed: %2").arg(params->name, result.errorMessage));
}
}
QList<RemoteParams*> DatabaseWidget::getRemoteParams() const
{
return m_remoteSettings->getAllRemoteParams();
}
void DatabaseWidget::switchToMainView(bool previousDialogAccepted)
{
setCurrentWidget(m_mainWidget);
@ -1243,6 +1329,59 @@ void DatabaseWidget::mergeDatabase(bool accepted)
emit databaseMerged(m_db);
}
void DatabaseWidget::syncUnlockedDatabase(bool accepted)
{
if (accepted) {
if (!m_db) {
showMessage(tr("No current database."), MessageWidget::Error);
return;
}
auto* senderDialog = qobject_cast<DatabaseOpenDialog*>(sender());
Q_ASSERT(senderDialog);
if (!senderDialog) {
return;
}
auto destinationDb = senderDialog->database();
if (!destinationDb) {
showMessage(tr("No source database, nothing to do."), MessageWidget::Error);
return;
}
RemoteHandler::RemoteResult result;
QString error;
result.success = syncWithDatabase(destinationDb, error);
result.errorMessage = error;
result.filePath = destinationDb->filePath();
emit databaseSyncUnlocked(result);
}
switchToMainView();
}
bool DatabaseWidget::syncWithDatabase(const QSharedPointer<Database>& otherDb, QString& error)
{
emit updateSyncProgress(50, tr("Syncing..."));
Merger firstMerge(m_db.data(), otherDb.data());
Merger secondMerge(otherDb.data(), m_db.data());
QStringList changeList = firstMerge.merge() + secondMerge.merge();
if (!changeList.isEmpty()) {
// Save synced databases
if (!m_db->save(Database::Atomic, {}, &error)) {
error = tr("Error while saving database %1: %2").arg(m_db->filePath(), error);
return false;
}
if (!otherDb->save(Database::Atomic, {}, &error)) {
error = tr("Error while saving database %1: %2").arg(otherDb->filePath(), error);
return false;
}
}
return true;
}
/**
* Unlock the database.
*
@ -1256,12 +1395,23 @@ void DatabaseWidget::unlockDatabase(bool accepted)
if (!senderDialog && (!m_db || !m_db->isInitialized())) {
emit closeRequest();
}
if (senderDialog && senderDialog->intent() == DatabaseOpenDialog::Intent::RemoteSync) {
RemoteHandler::RemoteResult result;
result.success = false;
result.errorMessage = "Remote database unlock cancelled.";
emit databaseSyncUnlockFailed(result);
}
return;
}
if (senderDialog && senderDialog->intent() == DatabaseOpenDialog::Intent::Merge) {
mergeDatabase(accepted);
return;
if (senderDialog) {
if (senderDialog->intent() == DatabaseOpenDialog::Intent::Merge) {
mergeDatabase(accepted);
return;
} else if (senderDialog->intent() == DatabaseOpenDialog::Intent::RemoteSync) {
syncUnlockedDatabase(accepted);
return;
}
}
QSharedPointer<Database> db;
@ -1416,6 +1566,12 @@ void DatabaseWidget::switchToDatabaseSecurity()
m_databaseSettingDialog->showDatabaseKeySettings();
}
void DatabaseWidget::switchToRemoteSettings()
{
switchToDatabaseSettings();
m_databaseSettingDialog->showRemoteSettings();
}
#ifdef WITH_XC_BROWSER_PASSKEYS
void DatabaseWidget::switchToPasskeys()
{
@ -1596,6 +1752,7 @@ void DatabaseWidget::onGroupChanged()
void DatabaseWidget::onDatabaseModified()
{
refreshSearch();
m_remoteSettings->loadSettings();
int autosaveDelayMs = m_db->metadata()->autosaveDelayMin() * 60 * 1000; // min to msec for QTimer
bool autosaveAfterEveryChangeConfig = config()->get(Config::AutoSaveAfterEveryChange).toBool();
if (autosaveDelayMs > 0 && autosaveAfterEveryChangeConfig) {

View File

@ -19,7 +19,6 @@
#ifndef KEEPASSX_DATABASEWIDGET_H
#define KEEPASSX_DATABASEWIDGET_H
#include <QBuffer>
#include <QStackedWidget>
#include "core/Database.h"
@ -27,6 +26,7 @@
#include "core/Metadata.h"
#include "gui/MessageWidget.h"
#include "gui/entry/EntryModel.h"
#include "remote/RemoteHandler.h"
class DatabaseOpenDialog;
class DatabaseOpenWidget;
@ -45,6 +45,8 @@ class QLabel;
class EntryPreviewWidget;
class TagView;
class ElidedLabel;
class RemoteSettings;
struct RemoteParams;
namespace Ui
{
@ -122,6 +124,10 @@ public:
void setSplitterSizes(const QHash<Config::ConfigKey, QList<int>>& sizes);
void setSearchStringForAutoType(const QString& search);
void syncWithRemote(const RemoteParams* params);
void syncDatabaseWithLockedDatabase(const QString& filePath, const RemoteParams* params);
QList<RemoteParams*> getRemoteParams() const;
signals:
// relayed Database signals
void databaseFilePathChanged(const QString& oldPath, const QString& newPath);
@ -142,6 +148,13 @@ signals:
void
requestOpenDatabase(const QString& filePath, bool inBackground, const QString& password, const QString& keyFile);
void databaseMerged(QSharedPointer<Database> mergedDb);
void databaseSyncInProgress();
void databaseSyncCompleted(const QString& syncName);
void databaseSyncFailed(const QString& syncName, const QString& error);
void databaseSyncUnlockFailed(const RemoteHandler::RemoteResult& result);
void databaseSyncUnlocked(const RemoteHandler::RemoteResult& result);
void unlockDatabaseInDialogForSync(const QString& filePath);
void updateSyncProgress(int percentage, QString message);
void groupContextMenuRequested(const QPoint& globalPos);
void entryContextMenuRequested(const QPoint& globalPos);
void listModeAboutToActivate();
@ -209,6 +222,7 @@ public slots:
void switchToDatabaseSecurity();
void switchToDatabaseReports();
void switchToDatabaseSettings();
void switchToRemoteSettings();
#ifdef WITH_XC_BROWSER_PASSKEYS
void switchToPasskeys();
void showImportPasskeyDialog(bool isEntry = false);
@ -260,6 +274,10 @@ private slots:
void loadDatabase(bool accepted);
void unlockDatabase(bool accepted);
void mergeDatabase(bool accepted);
void syncUnlockedDatabase(bool accepted);
bool syncWithDatabase(const QSharedPointer<Database>& otherDb, QString& error);
void uploadAndFinishSync(const RemoteParams* params, RemoteHandler::RemoteResult result);
void finishSync(const RemoteParams* params, RemoteHandler::RemoteResult result);
void emitCurrentModeChanged();
// Database autoreload slots
void reloadDatabaseFile();
@ -302,6 +320,8 @@ private:
int m_saveAttempts;
QScopedPointer<RemoteSettings> m_remoteSettings;
// Search state
QScopedPointer<EntrySearcher> m_entrySearcher;
QString m_lastSearchText;

View File

@ -47,6 +47,7 @@
#include "gui/ShortcutSettingsPage.h"
#include "gui/entry/EntryView.h"
#include "gui/osutils/OSUtils.h"
#include "gui/remote/RemoteSettings.h"
#ifdef WITH_XC_UPDATECHECK
#include "gui/UpdateCheckDialog.h"
@ -161,6 +162,8 @@ MainWindow::MainWindow()
m_entryNewContextMenu = new QMenu(this);
m_entryNewContextMenu->addAction(m_ui->actionEntryNew);
connect(m_ui->menuRemoteSync, &QMenu::aboutToShow, this, &MainWindow::updateRemoteSyncMenuEntries);
// Build Entry Level Auto-Type menu
auto autotypeMenu = new QMenu({}, this);
autotypeMenu->addAction(m_ui->actionEntryAutoTypeSequence);
@ -355,6 +358,7 @@ MainWindow::MainWindow()
m_ui->actionLockAllDatabases->setIcon(icons()->icon("database-lock-all"));
m_ui->actionQuit->setIcon(icons()->icon("application-exit"));
m_ui->actionDatabaseMerge->setIcon(icons()->icon("database-merge"));
m_ui->menuRemoteSync->setIcon(icons()->icon("remote-sync"));
m_ui->actionImport->setIcon(icons()->icon("document-import"));
m_ui->menuExport->setIcon(icons()->icon("document-export"));
@ -668,7 +672,11 @@ MainWindow::MainWindow()
m_progressBar->setFixedHeight(15);
m_progressBar->setMaximum(100);
statusBar()->addPermanentWidget(m_progressBar);
connect(clipboard(), SIGNAL(updateCountdown(int, QString)), this, SLOT(updateProgressBar(int, QString)));
connect(clipboard(), &Clipboard::updateCountdown, this, &MainWindow::updateProgressBar);
m_actionMultiplexer.connect(SIGNAL(updateSyncProgress(int, QString)), this, SLOT(updateProgressBar(int, QString)));
m_actionMultiplexer.connect(SIGNAL(databaseSyncInProgress()), this, SLOT(disableMenuAndToolbar()));
m_actionMultiplexer.connect(SIGNAL(databaseSyncCompleted(QString)), this, SLOT(enableMenuAndToolbar()));
m_actionMultiplexer.connect(SIGNAL(databaseSyncFailed(QString, const QString)), this, SLOT(enableMenuAndToolbar()));
m_statusBarLabel = new QLabel(statusBar());
m_statusBarLabel->setObjectName("statusBarLabel");
statusBar()->addPermanentWidget(m_statusBarLabel);
@ -860,6 +868,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionDatabaseClose->setEnabled(true);
m_ui->actionDatabaseMerge->setEnabled(inDatabaseTabWidget);
m_ui->menuRemoteSync->setEnabled(inDatabaseTabWidget);
m_ui->actionDatabaseNew->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
@ -965,6 +974,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionEntryImportPasskey->setEnabled(singleEntrySelected);
m_ui->actionEntryRemovePasskey->setEnabled(singleEntryHasPasskey);
#endif
m_ui->menuRemoteSync->setEnabled(true);
#ifdef WITH_XC_SSHAGENT
bool singleEntryHasSshKey =
singleEntrySelected && sshAgent()->isEnabled() && dbWidget->currentEntryHasSshKey();
@ -1020,6 +1030,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionExportCsv->setEnabled(false);
m_ui->actionExportHtml->setEnabled(false);
m_ui->actionDatabaseMerge->setEnabled(false);
m_ui->menuRemoteSync->setEnabled(false);
// Only disable the action in the database menu so that the
// menu remains active in the toolbar, if necessary
m_ui->actionLockDatabase->setEnabled(false);
@ -1071,6 +1082,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionExportCsv->setEnabled(false);
m_ui->actionExportHtml->setEnabled(false);
m_ui->actionDatabaseMerge->setEnabled(false);
m_ui->menuRemoteSync->setEnabled(false);
// Hide entry-specific actions
m_ui->actionEntryMoveUp->setVisible(false);
m_ui->actionEntryMoveDown->setVisible(false);
@ -1298,6 +1310,27 @@ void MainWindow::switchToDatabaseFile(const QString& file)
switchToDatabases();
}
void MainWindow::updateRemoteSyncMenuEntries()
{
m_ui->menuRemoteSync->clear();
auto dbWidget = m_ui->tabWidget->currentDatabaseWidget();
if (dbWidget) {
// Setup sync shortcut
auto action = m_ui->menuRemoteSync->addAction(tr("Setup Remote Sync…"));
connect(action, &QAction::triggered, dbWidget, &DatabaseWidget::switchToRemoteSettings);
m_ui->menuRemoteSync->addSeparator();
// Build remote sync menu
for (const auto params : dbWidget->getRemoteParams()) {
auto* remoteSyncAction = new QAction(params->name, this);
m_ui->menuRemoteSync->addAction(remoteSyncAction);
connect(remoteSyncAction, &QAction::triggered, dbWidget, [=] { dbWidget->syncWithRemote(params); });
}
}
}
void MainWindow::databaseStatusChanged(DatabaseWidget* dbWidget)
{
Q_UNUSED(dbWidget);
@ -1485,6 +1518,18 @@ void MainWindow::focusSearchWidget()
}
}
void MainWindow::enableMenuAndToolbar()
{
m_ui->toolBar->setDisabled(false);
m_ui->menubar->setDisabled(false);
}
void MainWindow::disableMenuAndToolbar()
{
m_ui->toolBar->setDisabled(true);
m_ui->menubar->setDisabled(true);
}
void MainWindow::saveWindowInformation()
{
if (isVisible()) {
@ -1497,7 +1542,7 @@ bool MainWindow::saveLastDatabases()
{
if (config()->get(Config::OpenPreviousDatabasesOnStartup).toBool()) {
auto currentDbWidget = m_ui->tabWidget->currentDatabaseWidget();
if (currentDbWidget) {
if (currentDbWidget && !currentDbWidget->database()->isTemporaryDatabase()) {
config()->set(Config::LastActiveDatabase, currentDbWidget->database()->filePath());
} else {
config()->remove(Config::LastActiveDatabase);
@ -1506,7 +1551,9 @@ bool MainWindow::saveLastDatabases()
QStringList openDatabases;
for (int i = 0; i < m_ui->tabWidget->count(); ++i) {
auto dbWidget = m_ui->tabWidget->databaseWidgetFromIndex(i);
openDatabases.append(QDir::toNativeSeparators(dbWidget->database()->filePath()));
if (!dbWidget->database()->isTemporaryDatabase()) {
openDatabases.append(QDir::toNativeSeparators(dbWidget->database()->filePath()));
}
}
config()->set(Config::LastOpenedDatabases, openDatabases);

View File

@ -125,6 +125,7 @@ private slots:
void switchToNewDatabase();
void switchToOpenDatabase();
void switchToDatabaseFile(const QString& file);
void updateRemoteSyncMenuEntries();
void databaseStatusChanged(DatabaseWidget* dbWidget);
void databaseTabChanged(int tabIndex);
void openRecentDatabase(QAction* action);
@ -150,6 +151,8 @@ private slots:
void updateProgressBar(int percentage, QString message);
void updateEntryCountLabel();
void focusSearchWidget();
void enableMenuAndToolbar();
void disableMenuAndToolbar();
private:
static const QString BaseWindowTitle;

View File

@ -239,6 +239,12 @@
<addaction name="actionExportHtml"/>
<addaction name="actionExportXML"/>
</widget>
<widget class="QMenu" name="menuRemoteSync">
<property name="title">
<string>Remote S&amp;ync…</string>
</property>
<addaction name="separator"/>
</widget>
<addaction name="actionDatabaseNew"/>
<addaction name="actionDatabaseOpen"/>
<addaction name="menuRecentDatabases"/>
@ -258,6 +264,7 @@
<addaction name="actionImportPasskey"/>
<addaction name="separator"/>
<addaction name="actionDatabaseMerge"/>
<addaction name="menuRemoteSync"/>
<addaction name="actionImport"/>
<addaction name="menuExport"/>
<addaction name="separator"/>

View File

@ -25,6 +25,7 @@
#ifdef WITH_XC_BROWSER
#include "DatabaseSettingsWidgetBrowser.h"
#endif
#include "../remote/DatabaseSettingsWidgetRemote.h"
#include "DatabaseSettingsWidgetMaintenance.h"
#ifdef WITH_XC_KEESHARE
#include "keeshare/DatabaseSettingsPageKeeShare.h"
@ -72,6 +73,7 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
, m_browserWidget(new DatabaseSettingsWidgetBrowser(this))
#endif
, m_maintenanceWidget(new DatabaseSettingsWidgetMaintenance(this))
, m_remoteWidget(new DatabaseSettingsWidgetRemote(this))
{
m_ui->setupUi(this);
@ -79,9 +81,8 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
m_ui->categoryList->addCategory(tr("General"), icons()->icon("preferences-other"));
m_ui->categoryList->addCategory(tr("Security"), icons()->icon("security-high"));
m_ui->stackedWidget->addWidget(m_generalWidget);
m_ui->categoryList->addCategory(tr("Security"), icons()->icon("security-high"));
m_ui->stackedWidget->addWidget(m_securityTabWidget);
auto* scrollArea = new QScrollArea(parent);
@ -95,6 +96,9 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings"));
m_ui->categoryList->addCategory(tr("Remote Sync"), icons()->icon("remote-sync"));
m_ui->stackedWidget->addWidget(m_remoteWidget);
#if defined(WITH_XC_KEESHARE)
addSettingsPage(new DatabaseSettingsPageKeeShare());
#endif
@ -132,6 +136,7 @@ void DatabaseSettingsDialog::load(const QSharedPointer<Database>& db)
m_browserWidget->load(db);
#endif
m_maintenanceWidget->load(db);
m_remoteWidget->load(db);
for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings(db);
}
@ -158,6 +163,11 @@ void DatabaseSettingsDialog::showDatabaseKeySettings()
m_securityTabWidget->setCurrentIndex(0);
}
void DatabaseSettingsDialog::showRemoteSettings()
{
m_ui->categoryList->setCurrentCategory(2);
}
void DatabaseSettingsDialog::save()
{
if (!m_generalWidget->save()) {
@ -172,6 +182,10 @@ void DatabaseSettingsDialog::save()
return;
}
if (!m_remoteWidget->save()) {
return;
}
for (const ExtraPage& extraPage : asConst(m_extraPages)) {
extraPage.saveSettings();
}

View File

@ -31,6 +31,7 @@ class DatabaseSettingsWidgetDatabaseKey;
class DatabaseSettingsWidgetBrowser;
#endif
class DatabaseSettingsWidgetMaintenance;
class DatabaseSettingsWidgetRemote;
class QTabWidget;
namespace Ui
@ -61,6 +62,7 @@ public:
void load(const QSharedPointer<Database>& db);
void addSettingsPage(IDatabaseSettingsPage* page);
void showDatabaseKeySettings();
void showRemoteSettings();
signals:
void editFinished(bool accepted);
@ -87,6 +89,7 @@ private:
QPointer<DatabaseSettingsWidgetBrowser> m_browserWidget;
#endif
QPointer<DatabaseSettingsWidgetMaintenance> m_maintenanceWidget;
QPointer<DatabaseSettingsWidgetRemote> m_remoteWidget;
class ExtraPage;
QList<ExtraPage> m_extraPages;

View File

@ -0,0 +1,200 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "DatabaseSettingsWidgetRemote.h"
#include "ui_DatabaseSettingsWidgetRemote.h"
#include "core/Global.h"
#include "core/Metadata.h"
#include "RemoteHandler.h"
#include "RemoteSettings.h"
#include "gui/MessageBox.h"
DatabaseSettingsWidgetRemote::DatabaseSettingsWidgetRemote(QWidget* parent)
: DatabaseSettingsWidget(parent)
, m_remoteSettings(new RemoteSettings(nullptr, this))
, m_ui(new Ui::DatabaseSettingsWidgetRemote())
{
m_ui->setupUi(this);
m_ui->messageWidget->setHidden(true);
connect(m_ui->saveSettingsButton, &QPushButton::clicked, this, &DatabaseSettingsWidgetRemote::saveCurrentSettings);
connect(
m_ui->removeSettingsButton, &QPushButton::clicked, this, &DatabaseSettingsWidgetRemote::removeCurrentSettings);
connect(m_ui->settingsListWidget,
&QListWidget::itemSelectionChanged,
this,
&DatabaseSettingsWidgetRemote::editCurrentSettings);
connect(m_ui->testDownloadCommandButton, &QPushButton::clicked, this, &DatabaseSettingsWidgetRemote::testDownload);
auto setModified = [this]() { m_modified = true; };
connect(m_ui->nameLineEdit, &QLineEdit::textChanged, setModified);
connect(m_ui->downloadCommand, &QLineEdit::textChanged, setModified);
connect(m_ui->inputForDownload, &QPlainTextEdit::textChanged, setModified);
connect(m_ui->uploadCommand, &QLineEdit::textChanged, setModified);
connect(m_ui->inputForUpload, &QPlainTextEdit::textChanged, setModified);
}
DatabaseSettingsWidgetRemote::~DatabaseSettingsWidgetRemote() = default;
void DatabaseSettingsWidgetRemote::initialize()
{
clearFields();
m_remoteSettings->setDatabase(m_db);
updateSettingsList();
if (m_ui->settingsListWidget->count() > 0) {
m_ui->settingsListWidget->setCurrentRow(0);
m_ui->removeSettingsButton->setEnabled(true);
} else {
m_ui->removeSettingsButton->setDisabled(true);
}
}
void DatabaseSettingsWidgetRemote::uninitialize()
{
}
bool DatabaseSettingsWidgetRemote::save()
{
if (m_modified) {
auto ans = MessageBox::question(this,
tr("Save Remote Settings"),
tr("You have unsaved changes. Do you want to save them?"),
MessageBox::Save | MessageBox::Discard | MessageBox::Cancel,
MessageBox::Save);
if (ans == MessageBox::Save) {
saveCurrentSettings();
} else if (ans == MessageBox::Cancel) {
return false;
}
}
m_remoteSettings->saveSettings();
return true;
}
void DatabaseSettingsWidgetRemote::saveCurrentSettings()
{
QString name = m_ui->nameLineEdit->text();
if (name.isEmpty()) {
m_ui->messageWidget->showMessage(tr("Name cannot be empty."), MessageWidget::Warning);
return;
}
auto* params = new RemoteParams();
params->name = m_ui->nameLineEdit->text();
params->downloadCommand = m_ui->downloadCommand->text();
params->downloadInput = m_ui->inputForDownload->toPlainText();
params->uploadCommand = m_ui->uploadCommand->text();
params->uploadInput = m_ui->inputForUpload->toPlainText();
m_remoteSettings->addRemoteParams(params);
updateSettingsList();
auto item = findItemByName(name);
m_ui->settingsListWidget->setCurrentItem(item);
m_ui->removeSettingsButton->setEnabled(true);
m_modified = false;
}
QListWidgetItem* DatabaseSettingsWidgetRemote::findItemByName(const QString& name)
{
return m_ui->settingsListWidget->findItems(name, Qt::MatchExactly).first();
}
void DatabaseSettingsWidgetRemote::removeCurrentSettings()
{
m_remoteSettings->removeRemoteParams(m_ui->nameLineEdit->text());
updateSettingsList();
if (!m_remoteSettings->getAllRemoteParams().empty()) {
m_ui->settingsListWidget->setCurrentRow(0);
m_ui->removeSettingsButton->setEnabled(true);
} else {
clearFields();
m_ui->removeSettingsButton->setDisabled(true);
}
}
void DatabaseSettingsWidgetRemote::editCurrentSettings()
{
if (!m_ui->settingsListWidget->currentItem()) {
return;
}
QString name = m_ui->settingsListWidget->currentItem()->text();
auto* params = m_remoteSettings->getRemoteParams(name);
if (!params) {
return;
}
m_ui->nameLineEdit->setText(params->name);
m_ui->downloadCommand->setText(params->downloadCommand);
m_ui->inputForDownload->setPlainText(params->downloadInput);
m_ui->uploadCommand->setText(params->uploadCommand);
m_ui->inputForUpload->setPlainText(params->uploadInput);
m_modified = false;
}
void DatabaseSettingsWidgetRemote::updateSettingsList()
{
m_ui->settingsListWidget->clear();
for (auto params : m_remoteSettings->getAllRemoteParams()) {
auto* item = new QListWidgetItem(m_ui->settingsListWidget);
item->setText(params->name);
m_ui->settingsListWidget->addItem(item);
}
}
void DatabaseSettingsWidgetRemote::clearFields()
{
m_ui->nameLineEdit->setText("");
m_ui->downloadCommand->setText("");
m_ui->inputForDownload->setPlainText("");
m_ui->uploadCommand->setText("");
m_ui->inputForUpload->setPlainText("");
m_modified = false;
}
void DatabaseSettingsWidgetRemote::testDownload()
{
auto* params = new RemoteParams();
params->name = m_ui->nameLineEdit->text();
params->downloadCommand = m_ui->downloadCommand->text();
params->downloadInput = m_ui->inputForDownload->toPlainText();
QScopedPointer<RemoteHandler> remoteHandler(new RemoteHandler(this));
if (params->downloadCommand.isEmpty()) {
m_ui->messageWidget->showMessage(tr("Download command cannot be empty."), MessageWidget::Warning);
return;
}
RemoteHandler::RemoteResult result = remoteHandler->download(params);
if (!result.success) {
m_ui->messageWidget->showMessage(tr("Download failed with error: %1").arg(result.errorMessage),
MessageWidget::Error);
return;
}
if (!QFile::exists(result.filePath)) {
m_ui->messageWidget->showMessage(tr("Download finished, but file %1 could not be found.").arg(result.filePath),
MessageWidget::Error);
return;
}
m_ui->messageWidget->showMessage(tr("Download successful."), MessageWidget::Positive);
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_DATABASESETTINGSWIDGETREMOTE_H
#define KEEPASSX_DATABASESETTINGSWIDGETREMOTE_H
#include "gui/dbsettings/DatabaseSettingsWidget.h"
#include <QListWidgetItem>
#include <QPointer>
class Database;
class RemoteSettings;
namespace Ui
{
class DatabaseSettingsWidgetRemote;
}
class DatabaseSettingsWidgetRemote : public DatabaseSettingsWidget
{
Q_OBJECT
public:
explicit DatabaseSettingsWidgetRemote(QWidget* parent = nullptr);
Q_DISABLE_COPY(DatabaseSettingsWidgetRemote);
~DatabaseSettingsWidgetRemote() override;
public slots:
void initialize() override;
void uninitialize() override;
bool save() override;
private slots:
void saveCurrentSettings();
void removeCurrentSettings();
void editCurrentSettings();
void testDownload();
private:
void updateSettingsList();
QListWidgetItem* findItemByName(const QString& name);
void clearFields();
QScopedPointer<RemoteSettings> m_remoteSettings;
const QScopedPointer<Ui::DatabaseSettingsWidgetRemote> m_ui;
bool m_modified = false;
};
#endif // KEEPASSX_DATABASESETTINGSWIDGETREMOTE_H

View File

@ -0,0 +1,260 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DatabaseSettingsWidgetRemote</class>
<widget class="QWidget" name="DatabaseSettingsWidgetRemote">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>652</width>
<height>516</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>450</width>
<height>0</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,1,3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="MessageWidget" name="messageWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="syncCommandsGroupBox">
<property name="title">
<string>Sync Commands</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListWidget" name="settingsListWidget"/>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPushButton" name="removeSettingsButton">
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="commandSettingsGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Command Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="nameLineEdit"/>
</item>
<item>
<widget class="QPushButton" name="saveSettingsButton">
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QTabWidget" name="commandTabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="downloadTab">
<attribute name="title">
<string>Download</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="downloadCommandLabel">
<property name="text">
<string>Command:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="downloadCommand">
<property name="accessibleName">
<string>Download command field</string>
</property>
<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="QPushButton" name="testDownloadCommandButton">
<property name="text">
<string>Test</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" 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="1" column="1">
<widget class="QPlainTextEdit" name="inputForDownload">
<property name="accessibleName">
<string>Download input field</string>
</property>
<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>
</widget>
<widget class="QWidget" name="uploadTab">
<attribute name="title">
<string>Upload</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<item row="1" column="0">
<widget class="QLabel" name="uploadCommandInputLabel">
<property name="text">
<string>Input:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="uploadCommandLabel">
<property name="text">
<string>Command:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="uploadCommand">
<property name="accessibleName">
<string>Upload command field</string>
</property>
<property name="placeholderText">
<string>e.g.: &quot;sftp user@hostname&quot; or &quot;scp {TEMP_DATABASE} user@hostname:DatabaseOnRemote.kdbx&quot;</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPlainTextEdit" name="inputForUpload">
<property name="accessibleName">
<string>Upload input field</string>
</property>
<property name="placeholderText">
<string>e.g.:
put {TEMP_DATABASE} DatabaseOnRemote.kdbx
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>
</widget>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MessageWidget</class>
<extends>QWidget</extends>
<header>gui/MessageWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,145 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "RemoteHandler.h"
#include "RemoteProcess.h"
#include "RemoteSettings.h"
#include "core/AsyncTask.h"
#include "core/Database.h"
namespace
{
QString getTempFileLocation()
{
QString uuid = QUuid::createUuid().toString().remove(0, 1);
uuid.chop(1);
return QDir::toNativeSeparators(QDir::temp().absoluteFilePath("RemoteDatabase-" + uuid + ".kdbx"));
}
} // namespace
std::function<QScopedPointer<RemoteProcess>(QObject*)> RemoteHandler::m_createRemoteProcess([](QObject* parent) {
return QScopedPointer<RemoteProcess>(new RemoteProcess(parent));
});
RemoteHandler::RemoteHandler(QObject* parent)
: QObject(parent)
{
}
void RemoteHandler::setRemoteProcessFunc(std::function<QScopedPointer<RemoteProcess>(QObject*)> func)
{
m_createRemoteProcess = std::move(func);
}
RemoteHandler::RemoteResult RemoteHandler::download(const RemoteParams* params)
{
return AsyncTask::runAndWaitForFuture([params] {
RemoteResult result;
if (!params) {
result.success = false;
result.errorMessage = tr("Invalid download parameters provided.");
return result;
}
auto filePath = getTempFileLocation();
auto remoteProcess = m_createRemoteProcess(nullptr); // use nullptr parent, otherwise there is a warning
remoteProcess->setTempFileLocation(filePath);
remoteProcess->start(params->downloadCommand);
if (!params->downloadInput.isEmpty()) {
remoteProcess->write(params->downloadInput + "\n");
remoteProcess->waitForBytesWritten();
remoteProcess->closeWriteChannel();
}
bool finished = remoteProcess->waitForFinished(10000);
int statusCode = remoteProcess->exitCode();
// TODO: For future use
result.stdOutput = remoteProcess->readOutput();
result.stdError = remoteProcess->readError();
if (finished && statusCode == 0) {
// Check if the file actually downloaded
QFileInfo fileInfo(filePath);
if (!fileInfo.exists() || fileInfo.size() == 0) {
result.success = false;
result.errorMessage = tr("Command `%1` failed to download database.").arg(params->downloadCommand);
} else {
result.success = true;
result.filePath = filePath;
}
} else if (finished) {
result.success = false;
result.errorMessage =
tr("Command `%1` exited with status code: %2").arg(params->downloadCommand).arg(statusCode);
} else {
remoteProcess->kill();
result.success = false;
result.errorMessage =
tr("Command `%1` did not finish in time. Process was killed.").arg(params->downloadCommand);
}
return result;
});
}
RemoteHandler::RemoteResult RemoteHandler::upload(const QString& filePath, const RemoteParams* params)
{
return AsyncTask::runAndWaitForFuture([filePath, params] {
RemoteResult result;
if (!params) {
result.success = false;
result.errorMessage = tr("Invalid database pointer or upload parameters provided.");
return result;
}
auto remoteProcess = m_createRemoteProcess(nullptr); // use nullptr parent, otherwise there is a warning
remoteProcess->setTempFileLocation(filePath);
remoteProcess->start(params->uploadCommand);
if (!params->uploadInput.isEmpty()) {
remoteProcess->write(params->uploadInput + "\n");
remoteProcess->waitForBytesWritten();
remoteProcess->closeWriteChannel();
}
bool finished = remoteProcess->waitForFinished(10000);
int statusCode = remoteProcess->exitCode();
// TODO: For future use
result.stdOutput = remoteProcess->readOutput();
result.stdError = remoteProcess->readError();
if (finished && statusCode == 0) {
result.success = true;
} else if (finished) {
result.success = false;
result.errorMessage = tr("Failed to upload merged database. Command `%1` exited with status code: %2")
.arg(params->uploadCommand)
.arg(statusCode);
} else {
remoteProcess->kill();
result.success = false;
result.errorMessage =
tr("Failed to upload merged database. Command `%1` did not finish in time. Process was killed.")
.arg(params->uploadCommand);
}
return result;
});
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_REMOTEHANDLER_H
#define KEEPASSXC_REMOTEHANDLER_H
#include <QObject>
class Database;
class RemoteProcess;
struct RemoteParams;
class RemoteHandler : public QObject
{
Q_OBJECT
public:
explicit RemoteHandler(QObject* parent = nullptr);
~RemoteHandler() override = default;
struct RemoteResult
{
bool success;
QString errorMessage;
QString filePath;
QString stdOutput;
QString stdError;
};
RemoteResult download(const RemoteParams* params);
RemoteResult upload(const QString& filePath, const RemoteParams* params);
// Used for testing only
static void setRemoteProcessFunc(std::function<QScopedPointer<RemoteProcess>(QObject*)> func);
private:
static std::function<QScopedPointer<RemoteProcess>(QObject*)> m_createRemoteProcess;
static QString m_tempFileLocation;
Q_DISABLE_COPY(RemoteHandler)
};
#endif // KEEPASSXC_REMOTEHANDLER_H

View File

@ -0,0 +1,88 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "RemoteProcess.h"
#include <QTemporaryDir>
#include <QUuid>
RemoteProcess::RemoteProcess(QObject* parent)
: m_process(new QProcess(parent))
{
}
RemoteProcess::~RemoteProcess()
{
}
void RemoteProcess::setTempFileLocation(const QString& tempFile)
{
m_tempFileLocation = tempFile;
}
void RemoteProcess::start(const QString& command)
{
m_process->start(resolveTemplateVariables(command));
m_process->waitForStarted();
}
qint64 RemoteProcess::write(const QString& input)
{
auto resolved = resolveTemplateVariables(input);
return m_process->write(resolved.toUtf8());
}
bool RemoteProcess::waitForBytesWritten()
{
return m_process->waitForBytesWritten();
}
void RemoteProcess::closeWriteChannel()
{
m_process->closeWriteChannel();
}
bool RemoteProcess::waitForFinished(int msecs)
{
return m_process->waitForFinished(msecs);
}
int RemoteProcess::exitCode() const
{
return m_process->exitCode();
}
QString RemoteProcess::readOutput()
{
return m_process->readAllStandardOutput();
}
QString RemoteProcess::readError()
{
return m_process->readAllStandardError();
}
void RemoteProcess::kill() const
{
m_process->kill();
}
QString RemoteProcess::resolveTemplateVariables(const QString& input) const
{
QString resolved = input;
return resolved.replace("{TEMP_DATABASE}", m_tempFileLocation);
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_REMOTEPROCESS_H
#define KEEPASSXC_REMOTEPROCESS_H
#include <QProcess>
class RemoteProcess
{
public:
explicit RemoteProcess(QObject* parent);
virtual ~RemoteProcess();
virtual void setTempFileLocation(const QString& tempFile);
virtual void start(const QString& command);
virtual qint64 write(const QString& input);
virtual bool waitForBytesWritten();
virtual void closeWriteChannel();
virtual bool waitForFinished(int msecs);
virtual QString readOutput();
virtual QString readError();
virtual int exitCode() const;
void kill() const;
protected:
QString m_tempFileLocation;
private:
QString resolveTemplateVariables(const QString& input) const;
QScopedPointer<QProcess> m_process;
};
#endif // KEEPASSXC_REMOTEPROCESS_H

View File

@ -0,0 +1,116 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "RemoteSettings.h"
#include "core/Database.h"
#include "core/Metadata.h"
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
RemoteSettings::RemoteSettings(const QSharedPointer<Database>& db, QObject* parent)
: QObject(parent)
{
setDatabase(db);
}
RemoteSettings::~RemoteSettings() = default;
void RemoteSettings::setDatabase(const QSharedPointer<Database>& db)
{
m_remoteParams.clear();
m_db = db;
loadSettings();
}
void RemoteSettings::addRemoteParams(RemoteParams* params)
{
if (params->name.isEmpty()) {
qWarning() << "RemoteSettings::addRemoteParams: Remote parameters name is empty";
return;
}
m_remoteParams.insert(params->name, params);
}
void RemoteSettings::removeRemoteParams(const QString& name)
{
m_remoteParams.remove(name);
}
RemoteParams* RemoteSettings::getRemoteParams(const QString& name) const
{
if (m_remoteParams.contains(name)) {
return m_remoteParams.value(name);
}
return nullptr;
}
QList<RemoteParams*> RemoteSettings::getAllRemoteParams() const
{
return m_remoteParams.values();
}
void RemoteSettings::loadSettings()
{
if (m_db) {
fromConfig(m_db->metadata()->customData()->value(CustomData::RemoteProgramSettings));
}
}
void RemoteSettings::saveSettings() const
{
if (m_db) {
m_db->metadata()->customData()->set(CustomData::RemoteProgramSettings, toConfig());
}
}
QString RemoteSettings::toConfig() const
{
QJsonArray config;
for (const auto params : m_remoteParams.values()) {
QJsonObject object;
object["name"] = params->name;
object["downloadCommand"] = params->downloadCommand;
object["downloadCommandInput"] = params->downloadInput;
object["uploadCommand"] = params->uploadCommand;
object["uploadCommandInput"] = params->uploadInput;
config << object;
}
QJsonDocument doc(config);
return doc.toJson(QJsonDocument::Compact);
}
void RemoteSettings::fromConfig(const QString& data)
{
m_remoteParams.clear();
QJsonDocument json = QJsonDocument::fromJson(data.toUtf8());
for (const auto& item : json.array().toVariantList()) {
auto itemMap = item.toMap();
auto* params = new RemoteParams();
params->name = itemMap["name"].toString();
params->downloadCommand = itemMap["downloadCommand"].toString();
params->downloadInput = itemMap["downloadCommandInput"].toString();
params->uploadCommand = itemMap["uploadCommand"].toString();
params->uploadInput = itemMap["uploadCommandInput"].toString();
m_remoteParams.insert(params->name, params);
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_REMOTESETTINGS_H
#define KEEPASSXC_REMOTESETTINGS_H
#include <QObject>
#include <QSharedPointer>
class Database;
struct RemoteParams
{
QString name;
QString downloadCommand;
QString downloadInput;
QString uploadCommand;
QString uploadInput;
};
Q_DECLARE_METATYPE(RemoteParams)
class RemoteSettings : public QObject
{
Q_OBJECT
public:
explicit RemoteSettings(const QSharedPointer<Database>& db, QObject* parent = nullptr);
~RemoteSettings() override;
void setDatabase(const QSharedPointer<Database>& db);
void addRemoteParams(RemoteParams* params);
void removeRemoteParams(const QString& name);
RemoteParams* getRemoteParams(const QString& name) const;
QList<RemoteParams*> getAllRemoteParams() const;
void loadSettings();
void saveSettings() const;
private:
void fromConfig(const QString& data);
QString toConfig() const;
QHash<QString, RemoteParams*> m_remoteParams;
QSharedPointer<Database> m_db;
};
#endif // KEEPASSXC_REMOTESETTINGS_H

Binary file not shown.

Binary file not shown.

View File

@ -15,7 +15,7 @@
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..)
add_unit_test(NAME testgui SOURCES TestGui.cpp ../util/TemporaryFile.cpp LIBS ${TEST_LIBRARIES})
add_unit_test(NAME testgui SOURCES TestGui.cpp ../util/TemporaryFile.cpp ../mock/MockRemoteProcess.cpp LIBS ${TEST_LIBRARIES})
add_unit_test(NAME testguipixmaps SOURCES TestGuiPixmaps.cpp LIBS ${TEST_LIBRARIES})
if(WITH_XC_BROWSER)

View File

@ -21,6 +21,9 @@
#include <QCheckBox>
#include <QClipboard>
#include <QListWidget>
#include <QMenu>
#include <QMenuBar>
#include <QMimeData>
#include <QPlainTextEdit>
#include <QPushButton>
@ -56,9 +59,11 @@
#include "gui/group/EditGroupWidget.h"
#include "gui/group/GroupModel.h"
#include "gui/group/GroupView.h"
#include "gui/remote/RemoteHandler.h"
#include "gui/tag/TagsEdit.h"
#include "gui/wizard/NewDatabaseWizard.h"
#include "keys/FileKey.h"
#include "mock/MockRemoteProcess.h"
#define TEST_MODAL_NO_WAIT(TEST_CODE) \
bool dialogFinished = false; \
@ -370,6 +375,107 @@ void TestGui::testMergeDatabase()
QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
}
void TestGui::prepareAndTriggerRemoteSync(const QString& sourceToSync)
{
auto* menuRemoteSync = m_mainWindow->findChild<QMenu*>("menuRemoteSync");
QSignalSpy remoteAboutToShow(menuRemoteSync, &QMenu::aboutToShow);
QApplication::processEvents();
// create remote settings in settings dialog
triggerAction("actionDatabaseSettings");
auto* dbSettingsDialog = m_dbWidget->findChild<QWidget*>("databaseSettingsDialog");
auto* dbSettingsCategoryList = dbSettingsDialog->findChild<CategoryListWidget*>("categoryList");
auto* dbSettingsStackedWidget = dbSettingsDialog->findChild<QStackedWidget*>("stackedWidget");
dbSettingsCategoryList->setCurrentCategory(2); // go into remote category
auto name = "testCommand";
auto* nameEdit = dbSettingsStackedWidget->findChild<QLineEdit*>("nameLineEdit");
auto* downloadCommandEdit = dbSettingsStackedWidget->findChild<QLineEdit*>("downloadCommand");
QVERIFY(downloadCommandEdit != nullptr);
downloadCommandEdit->setText(sourceToSync);
nameEdit->setText(name);
auto* saveSettingsButton = dbSettingsStackedWidget->findChild<QPushButton*>("saveSettingsButton");
QVERIFY(saveSettingsButton != nullptr);
QTest::mouseClick(saveSettingsButton, Qt::LeftButton);
// find and click dialog OK button
auto buttons = dbSettingsDialog->findChild<QDialogButtonBox*>()->findChildren<QPushButton*>();
for (QPushButton* b : buttons) {
if (b->text() == "OK") {
QTest::mouseClick(b, Qt::LeftButton);
break;
}
}
QTRY_COMPARE(m_dbWidget->getRemoteParams().size(), 1);
// trigger aboutToShow to create remote actions
menuRemoteSync->popup(QPoint(0, 0));
QApplication::processEvents();
QTRY_COMPARE(remoteAboutToShow.count(), 1);
// close the opened menu
QTest::keyClick(menuRemoteSync, Qt::Key::Key_Escape);
// trigger remote sync action
for (auto* remoteAction : menuRemoteSync->actions()) {
if (remoteAction->text() == name) {
remoteAction->trigger();
break;
}
}
QApplication::processEvents();
}
void TestGui::testRemoteSyncDatabaseSameKey()
{
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")));
});
QSignalSpy dbSyncSpy(m_dbWidget.data(), &DatabaseWidget::databaseSyncCompleted);
prepareAndTriggerRemoteSync(sourceToSync);
QTRY_COMPARE(dbSyncSpy.count(), 1);
m_db = m_tabWidget->currentDatabaseWidget()->database();
// there are seven child groups of the root group
QCOMPARE(m_db->rootGroup()->children().size(), 7);
// the merged group should contain an entry
QCOMPARE(m_db->rootGroup()->children().at(6)->entries().size(), 1);
// the General group contains one entry merged from the other db
QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
}
void TestGui::testRemoteSyncDatabaseRequiresPassword()
{
QString sourceToSync = "sftp user@server:Database.kdbx";
RemoteHandler::setRemoteProcessFunc([sourceToSync](QObject* parent) {
return QScopedPointer<RemoteProcess>(new MockRemoteProcess(
parent, QString(KEEPASSX_TEST_DATA_DIR).append("/SyncDatabaseDifferentPassword.kdbx")));
});
QSignalSpy dbSyncSpy(m_dbWidget.data(), &DatabaseWidget::databaseSyncCompleted);
prepareAndTriggerRemoteSync(sourceToSync);
// need to process more events as opening with the same key did not work and more events have been fired
QApplication::processEvents(QEventLoop::WaitForMoreEvents);
QTRY_COMPARE(QApplication::focusWidget()->objectName(), QString("passwordEdit"));
auto* editPasswordSync = QApplication::focusWidget();
QVERIFY(editPasswordSync->isVisible());
QTest::keyClicks(editPasswordSync, "b");
QTest::keyClick(editPasswordSync, Qt::Key_Enter);
QTRY_COMPARE(dbSyncSpy.count(), 1);
m_db = m_tabWidget->currentDatabaseWidget()->database();
// there are seven child groups of the root group
QCOMPARE(m_db->rootGroup()->children().size(), 7);
// the merged group should contain an entry
QCOMPARE(m_db->rootGroup()->children().at(6)->entries().size(), 1);
// the General group contains one entry merged from the other db
QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
}
void TestGui::testAutoreloadDatabase()
{
config()->set(Config::AutoReloadOnChange, false);

View File

@ -40,6 +40,8 @@ private slots:
void testSettingsDefaultTabOrder();
void testCreateDatabase();
void testMergeDatabase();
void testRemoteSyncDatabaseSameKey();
void testRemoteSyncDatabaseRequiresPassword();
void testAutoreloadDatabase();
void testTabs();
void testEditEntry();
@ -85,6 +87,7 @@ private:
Qt::KeyboardModifiers stateKey = 0);
void checkSaveDatabase();
void checkStatusBarText(const QString& textFragment);
void prepareAndTriggerRemoteSync(const QString& sourceToSync);
QScopedPointer<MainWindow> m_mainWindow;
QPointer<QLabel> m_statusBarLabel;

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QFile>
#include "MockRemoteProcess.h"
MockRemoteProcess::MockRemoteProcess(QObject* parent, const QString& dbPath)
: RemoteProcess(parent)
, m_dbPath(dbPath)
{
}
void MockRemoteProcess::start(const QString&)
{
QFile ::copy(m_dbPath, m_tempFileLocation);
}
qint64 MockRemoteProcess::write(const QString& data)
{
return data.length();
}
bool MockRemoteProcess::waitForBytesWritten()
{
return true;
}
void MockRemoteProcess::closeWriteChannel()
{
// nothing to do
}
bool MockRemoteProcess::waitForFinished(int)
{
return true; // no need to wait
}
int MockRemoteProcess::exitCode() const
{
return 0; // always return success
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_MOCKREMOTEPROCESS_H
#define KEEPASSXC_MOCKREMOTEPROCESS_H
#include "gui/remote/RemoteProcess.h"
class MockRemoteProcess : public RemoteProcess
{
public:
explicit MockRemoteProcess(QObject* parent, const QString& dbPath);
~MockRemoteProcess() override = default;
void start(const QString& program) override;
qint64 write(const QString& data) override;
bool waitForBytesWritten() override;
void closeWriteChannel() override;
bool waitForFinished(int msecs) override;
[[nodiscard]] int exitCode() const override;
private:
QByteArray m_data;
QString m_dbPath;
};
#endif // KEEPASSXC_MOCKREMOTEPROCESS_H