mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-09-07 18:02:02 +00:00
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:
parent
ad8a00d56b
commit
1ca607792d
1
COPYING
1
COPYING
@ -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
|
||||
|
BIN
docs/images/sync_remote_settings.png
Normal file
BIN
docs/images/sync_remote_settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
@ -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
|
||||
|
1
share/icons/application/scalable/actions/remote-sync.svg
Normal file
1
share/icons/application/scalable/actions/remote-sync.svg
Normal 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 |
@ -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>
|
||||
|
@ -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.: "sftp user@hostname" or "scp user@hostname:DatabaseOnRemote.kdbx {TEMP_DATABASE}"</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.: "sftp user@hostname" or "scp {TEMP_DATABASE} user@hostname:DatabaseOnRemote.kdbx"</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 '%1' completed successfully!</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Remote sync '%1' 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&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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -239,6 +239,12 @@
|
||||
<addaction name="actionExportHtml"/>
|
||||
<addaction name="actionExportXML"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuRemoteSync">
|
||||
<property name="title">
|
||||
<string>Remote S&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"/>
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
200
src/gui/remote/DatabaseSettingsWidgetRemote.cpp
Normal file
200
src/gui/remote/DatabaseSettingsWidgetRemote.cpp
Normal 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);
|
||||
}
|
64
src/gui/remote/DatabaseSettingsWidgetRemote.h
Normal file
64
src/gui/remote/DatabaseSettingsWidgetRemote.h
Normal 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
|
260
src/gui/remote/DatabaseSettingsWidgetRemote.ui
Normal file
260
src/gui/remote/DatabaseSettingsWidgetRemote.ui
Normal 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.: "sftp user@hostname" or "scp user@hostname:DatabaseOnRemote.kdbx {TEMP_DATABASE}"</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.: "sftp user@hostname" or "scp {TEMP_DATABASE} user@hostname:DatabaseOnRemote.kdbx"</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>
|
145
src/gui/remote/RemoteHandler.cpp
Normal file
145
src/gui/remote/RemoteHandler.cpp
Normal 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;
|
||||
});
|
||||
}
|
57
src/gui/remote/RemoteHandler.h
Normal file
57
src/gui/remote/RemoteHandler.h
Normal 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
|
88
src/gui/remote/RemoteProcess.cpp
Normal file
88
src/gui/remote/RemoteProcess.cpp
Normal 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);
|
||||
}
|
50
src/gui/remote/RemoteProcess.h
Normal file
50
src/gui/remote/RemoteProcess.h
Normal 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
|
116
src/gui/remote/RemoteSettings.cpp
Normal file
116
src/gui/remote/RemoteSettings.cpp
Normal 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);
|
||||
}
|
||||
}
|
61
src/gui/remote/RemoteSettings.h
Normal file
61
src/gui/remote/RemoteSettings.h
Normal 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
|
BIN
tests/data/SyncDatabase.kdbx
Normal file
BIN
tests/data/SyncDatabase.kdbx
Normal file
Binary file not shown.
BIN
tests/data/SyncDatabaseDifferentPassword.kdbx
Normal file
BIN
tests/data/SyncDatabaseDifferentPassword.kdbx
Normal file
Binary file not shown.
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
56
tests/mock/MockRemoteProcess.cpp
Normal file
56
tests/mock/MockRemoteProcess.cpp
Normal 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
|
||||
}
|
41
tests/mock/MockRemoteProcess.h
Normal file
41
tests/mock/MockRemoteProcess.h
Normal 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
|
Loading…
Reference in New Issue
Block a user