Add Merge database utility function (#47)

Thank you to @TheZ3ro and @monomon for there major contributions to this PR!
This commit is contained in:
Jonathan White 2016-11-07 22:37:42 -05:00 committed by GitHub
parent e00c6f9c77
commit e25cd9ba48
16 changed files with 382 additions and 3 deletions

View File

@ -282,6 +282,12 @@ void Database::recycleGroup(Group* group)
} }
} }
void Database::merge(const Database* other)
{
m_rootGroup->merge(other->rootGroup());
Q_EMIT modified();
}
void Database::setEmitModified(bool value) void Database::setEmitModified(bool value)
{ {
if (m_emitModified && !value) { if (m_emitModified && !value) {

View File

@ -105,6 +105,7 @@ public:
void recycleGroup(Group* group); void recycleGroup(Group* group);
void setEmitModified(bool value); void setEmitModified(bool value);
void copyAttributesFrom(const Database* other); void copyAttributesFrom(const Database* other);
void merge(const Database* other);
/** /**
* Returns a unique id that is only valid as long as the Database exists. * Returns a unique id that is only valid as long as the Database exists.

View File

@ -32,6 +32,7 @@ Group::Group()
m_data.isExpanded = true; m_data.isExpanded = true;
m_data.autoTypeEnabled = Inherit; m_data.autoTypeEnabled = Inherit;
m_data.searchingEnabled = Inherit; m_data.searchingEnabled = Inherit;
m_data.mergeMode = ModeInherit;
} }
Group::~Group() Group::~Group()
@ -196,6 +197,19 @@ Group::TriState Group::searchingEnabled() const
return m_data.searchingEnabled; return m_data.searchingEnabled;
} }
Group::MergeMode Group::mergeMode() const
{
if (m_data.mergeMode == Group::MergeMode::ModeInherit) {
if (m_parent) {
return m_parent->mergeMode();
} else {
return Group::MergeMode::KeepNewer; // fallback
}
} else {
return m_data.mergeMode;
}
}
Entry* Group::lastTopVisibleEntry() const Entry* Group::lastTopVisibleEntry() const
{ {
return m_lastTopVisibleEntry; return m_lastTopVisibleEntry;
@ -303,6 +317,11 @@ void Group::setExpiryTime(const QDateTime& dateTime)
} }
} }
void Group::setMergeMode(MergeMode newMode)
{
set(m_data.mergeMode, newMode);
}
Group* Group::parentGroup() Group* Group::parentGroup()
{ {
return m_parent; return m_parent;
@ -440,6 +459,18 @@ QList<Entry*> Group::entriesRecursive(bool includeHistoryItems) const
return entryList; return entryList;
} }
Entry* Group::findEntry(const Uuid& uuid)
{
Q_ASSERT(!uuid.isNull());
for (Entry* entry : asConst(m_entries)) {
if (entry->uuid() == uuid) {
return entry;
}
}
return nullptr;
}
QList<const Group*> Group::groupsRecursive(bool includeSelf) const QList<const Group*> Group::groupsRecursive(bool includeSelf) const
{ {
QList<const Group*> groupList; QList<const Group*> groupList;
@ -490,6 +521,44 @@ QSet<Uuid> Group::customIconsRecursive() const
return result; return result;
} }
void Group::merge(const Group* other)
{
// merge entries
const QList<Entry*> dbEntries = other->entries();
for (Entry* entry : dbEntries) {
// entries are searched by uuid
if (!findEntry(entry->uuid())) {
entry->clone(Entry::CloneNoFlags)->setGroup(this);
} else {
resolveConflict(this->findEntry(entry->uuid()), entry);
}
}
// merge groups (recursively)
const QList<Group*> dbChildren = other->children();
for (Group* group : dbChildren) {
// groups are searched by name instead of uuid
if (this->findChildByName(group->name())) {
this->findChildByName(group->name())->merge(group);
} else {
group->setParent(this);
}
}
Q_EMIT modified();
}
Group* Group::findChildByName(const QString& name)
{
for (Group* group : asConst(m_children)) {
if (group->name() == name) {
return group;
}
}
return nullptr;
}
Group* Group::clone(Entry::CloneFlags entryFlags) const Group* Group::clone(Entry::CloneFlags entryFlags) const
{ {
Group* clonedGroup = new Group(); Group* clonedGroup = new Group();
@ -624,6 +693,14 @@ void Group::recCreateDelObjects()
} }
} }
void Group::markOlderEntry(Entry* entry)
{
entry->attributes()->set(
"merged",
QString("older entry merged from database \"%1\"").arg(entry->group()->database()->metadata()->name())
);
}
bool Group::resolveSearchingEnabled() const bool Group::resolveSearchingEnabled() const
{ {
switch (m_data.searchingEnabled) { switch (m_data.searchingEnabled) {
@ -663,3 +740,39 @@ bool Group::resolveAutoTypeEnabled() const
return false; return false;
} }
} }
void Group::resolveConflict(Entry* existingEntry, Entry* otherEntry)
{
const QDateTime timeExisting = existingEntry->timeInfo().lastModificationTime();
const QDateTime timeOther = otherEntry->timeInfo().lastModificationTime();
Entry* clonedEntry;
switch(this->mergeMode()) {
case KeepBoth:
// if one entry is newer, create a clone and add it to the group
if (timeExisting > timeOther) {
clonedEntry = otherEntry->clone(Entry::CloneNoFlags);
clonedEntry->setGroup(this);
this->markOlderEntry(clonedEntry);
} else if (timeExisting < timeOther) {
clonedEntry = otherEntry->clone(Entry::CloneNoFlags);
clonedEntry->setGroup(this);
this->markOlderEntry(existingEntry);
}
break;
case KeepNewer:
if (timeExisting < timeOther) {
// only if other entry is newer, replace existing one
this->removeEntry(existingEntry);
this->addEntry(otherEntry);
}
break;
case KeepExisting:
break;
default:
// do nothing
break;
}
}

View File

@ -34,6 +34,7 @@ class Group : public QObject
public: public:
enum TriState { Inherit, Enable, Disable }; enum TriState { Inherit, Enable, Disable };
enum MergeMode { ModeInherit, KeepBoth, KeepNewer, KeepExisting };
struct GroupData struct GroupData
{ {
@ -46,6 +47,7 @@ public:
QString defaultAutoTypeSequence; QString defaultAutoTypeSequence;
Group::TriState autoTypeEnabled; Group::TriState autoTypeEnabled;
Group::TriState searchingEnabled; Group::TriState searchingEnabled;
Group::MergeMode mergeMode;
}; };
Group(); Group();
@ -66,6 +68,7 @@ public:
QString defaultAutoTypeSequence() const; QString defaultAutoTypeSequence() const;
Group::TriState autoTypeEnabled() const; Group::TriState autoTypeEnabled() const;
Group::TriState searchingEnabled() const; Group::TriState searchingEnabled() const;
Group::MergeMode mergeMode() const;
bool resolveSearchingEnabled() const; bool resolveSearchingEnabled() const;
bool resolveAutoTypeEnabled() const; bool resolveAutoTypeEnabled() const;
Entry* lastTopVisibleEntry() const; Entry* lastTopVisibleEntry() const;
@ -74,6 +77,8 @@ public:
static const int DefaultIconNumber; static const int DefaultIconNumber;
static const int RecycleBinIconNumber; static const int RecycleBinIconNumber;
Entry* findEntry(const Uuid& uuid);
Group* findChildByName(const QString& name);
void setUuid(const Uuid& uuid); void setUuid(const Uuid& uuid);
void setName(const QString& name); void setName(const QString& name);
void setNotes(const QString& notes); void setNotes(const QString& notes);
@ -87,6 +92,7 @@ public:
void setLastTopVisibleEntry(Entry* entry); void setLastTopVisibleEntry(Entry* entry);
void setExpires(bool value); void setExpires(bool value);
void setExpiryTime(const QDateTime& dateTime); void setExpiryTime(const QDateTime& dateTime);
void setMergeMode(MergeMode newMode);
void setUpdateTimeinfo(bool value); void setUpdateTimeinfo(bool value);
@ -113,6 +119,7 @@ public:
*/ */
Group* clone(Entry::CloneFlags entryFlags = Entry::CloneNewUuid | Entry::CloneResetTimeInfo) const; Group* clone(Entry::CloneFlags entryFlags = Entry::CloneNewUuid | Entry::CloneResetTimeInfo) const;
void copyDataFrom(const Group* other); void copyDataFrom(const Group* other);
void merge(const Group* other);
Q_SIGNALS: Q_SIGNALS:
void dataChanged(Group* group); void dataChanged(Group* group);
@ -142,6 +149,8 @@ private:
void addEntry(Entry* entry); void addEntry(Entry* entry);
void removeEntry(Entry* entry); void removeEntry(Entry* entry);
void setParent(Database* db); void setParent(Database* db);
void markOlderEntry(Entry* entry);
void resolveConflict(Entry* existingEntry, Entry* otherEntry);
void recSetDatabase(Database* db); void recSetDatabase(Database* db);
void cleanupParent(); void cleanupParent();

View File

@ -24,6 +24,7 @@
#include "autotype/AutoType.h" #include "autotype/AutoType.h"
#include "core/Config.h" #include "core/Config.h"
#include "core/Global.h"
#include "core/Database.h" #include "core/Database.h"
#include "core/Group.h" #include "core/Group.h"
#include "core/Metadata.h" #include "core/Metadata.h"
@ -192,6 +193,21 @@ void DatabaseTabWidget::openDatabase(const QString& fileName, const QString& pw,
} }
} }
void DatabaseTabWidget::mergeDatabase()
{
QString filter = QString("%1 (*.kdbx);;%2 (*)").arg(tr("KeePass 2 Database"), tr("All files"));
const QString fileName = fileDialog()->getOpenFileName(this, tr("Merge database"), QString(),
filter);
if (!fileName.isEmpty()) {
mergeDatabase(fileName);
}
}
void DatabaseTabWidget::mergeDatabase(const QString& fileName)
{
currentDatabaseWidget()->switchToOpenMergeDatabase(fileName);
}
void DatabaseTabWidget::importKeePass1Database() void DatabaseTabWidget::importKeePass1Database()
{ {
QString fileName = fileDialog()->getOpenFileName(this, tr("Open KeePass 1 database"), QString(), QString fileName = fileDialog()->getOpenFileName(this, tr("Open KeePass 1 database"), QString(),

View File

@ -55,6 +55,7 @@ public:
~DatabaseTabWidget(); ~DatabaseTabWidget();
void openDatabase(const QString& fileName, const QString& pw = QString(), void openDatabase(const QString& fileName, const QString& pw = QString(),
const QString& keyFile = QString()); const QString& keyFile = QString());
void mergeDatabase(const QString& fileName);
DatabaseWidget* currentDatabaseWidget(); DatabaseWidget* currentDatabaseWidget();
bool hasLockableDatabases() const; bool hasLockableDatabases() const;
@ -63,6 +64,7 @@ public:
public Q_SLOTS: public Q_SLOTS:
void newDatabase(); void newDatabase();
void openDatabase(); void openDatabase();
void mergeDatabase();
void importKeePass1Database(); void importKeePass1Database();
bool saveDatabase(int index = -1); bool saveDatabase(int index = -1);
bool saveDatabaseAs(int index = -1); bool saveDatabaseAs(int index = -1);

View File

@ -118,6 +118,8 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
m_databaseSettingsWidget->setObjectName("databaseSettingsWidget"); m_databaseSettingsWidget->setObjectName("databaseSettingsWidget");
m_databaseOpenWidget = new DatabaseOpenWidget(); m_databaseOpenWidget = new DatabaseOpenWidget();
m_databaseOpenWidget->setObjectName("databaseOpenWidget"); m_databaseOpenWidget->setObjectName("databaseOpenWidget");
m_databaseOpenMergeWidget = new DatabaseOpenWidget();
m_databaseOpenMergeWidget->setObjectName("databaseOpenMergeWidget");
m_keepass1OpenWidget = new KeePass1OpenWidget(); m_keepass1OpenWidget = new KeePass1OpenWidget();
m_keepass1OpenWidget->setObjectName("keepass1OpenWidget"); m_keepass1OpenWidget->setObjectName("keepass1OpenWidget");
m_unlockDatabaseWidget = new UnlockDatabaseWidget(); m_unlockDatabaseWidget = new UnlockDatabaseWidget();
@ -129,6 +131,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
addWidget(m_databaseSettingsWidget); addWidget(m_databaseSettingsWidget);
addWidget(m_historyEditEntryWidget); addWidget(m_historyEditEntryWidget);
addWidget(m_databaseOpenWidget); addWidget(m_databaseOpenWidget);
addWidget(m_databaseOpenMergeWidget);
addWidget(m_keepass1OpenWidget); addWidget(m_keepass1OpenWidget);
addWidget(m_unlockDatabaseWidget); addWidget(m_unlockDatabaseWidget);
@ -147,6 +150,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
connect(m_changeMasterKeyWidget, SIGNAL(editFinished(bool)), SLOT(updateMasterKey(bool))); connect(m_changeMasterKeyWidget, SIGNAL(editFinished(bool)), SLOT(updateMasterKey(bool)));
connect(m_databaseSettingsWidget, SIGNAL(editFinished(bool)), SLOT(switchToView(bool))); connect(m_databaseSettingsWidget, SIGNAL(editFinished(bool)), SLOT(switchToView(bool)));
connect(m_databaseOpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool))); connect(m_databaseOpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool)));
connect(m_databaseOpenMergeWidget, SIGNAL(editFinished(bool)), SLOT(mergeDatabase(bool)));
connect(m_keepass1OpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool))); connect(m_keepass1OpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool)));
connect(m_unlockDatabaseWidget, SIGNAL(editFinished(bool)), SLOT(unlockDatabase(bool))); connect(m_unlockDatabaseWidget, SIGNAL(editFinished(bool)), SLOT(unlockDatabase(bool)));
connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged())); connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged()));
@ -663,6 +667,28 @@ void DatabaseWidget::openDatabase(bool accepted)
} }
} }
void DatabaseWidget::mergeDatabase(bool accepted)
{
if (accepted) {
if (!m_db) {
MessageBox::critical(this, tr("Error"), tr("No current database."));
return;
}
Database* srcDb = static_cast<DatabaseOpenWidget*>(sender())->database();
if (!srcDb) {
MessageBox::critical(this, tr("Error"), tr("No source database, nothing to do."));
return;
}
m_db->merge(srcDb);
}
setCurrentWidget(m_mainWidget);
Q_EMIT databaseMerged(m_db);
}
void DatabaseWidget::unlockDatabase(bool accepted) void DatabaseWidget::unlockDatabase(bool accepted)
{ {
if (!accepted) { if (!accepted) {
@ -745,6 +771,19 @@ void DatabaseWidget::switchToOpenDatabase(const QString& fileName, const QString
m_databaseOpenWidget->enterKey(password, keyFile); m_databaseOpenWidget->enterKey(password, keyFile);
} }
void DatabaseWidget::switchToOpenMergeDatabase(const QString& fileName)
{
m_databaseOpenMergeWidget->load(fileName);
setCurrentWidget(m_databaseOpenMergeWidget);
}
void DatabaseWidget::switchToOpenMergeDatabase(const QString& fileName, const QString& password,
const QString& keyFile)
{
switchToOpenMergeDatabase(fileName);
m_databaseOpenMergeWidget->enterKey(password, keyFile);
}
void DatabaseWidget::switchToImportKeepass1(const QString& fileName) void DatabaseWidget::switchToImportKeepass1(const QString& fileName)
{ {
updateFilename(fileName); updateFilename(fileName);
@ -856,6 +895,12 @@ bool DatabaseWidget::isInSearchMode() const
return m_entryView->inEntryListMode(); return m_entryView->inEntryListMode();
} }
Group* DatabaseWidget::currentGroup() const
{
return isInSearchMode() ? m_lastGroup
: m_groupView->currentGroup();
}
void DatabaseWidget::clearLastGroup(Group* group) void DatabaseWidget::clearLastGroup(Group* group)
{ {
if (group) { if (group) {
@ -956,3 +1001,11 @@ bool DatabaseWidget::currentEntryHasNotes()
} }
return !currentEntry->notes().isEmpty(); return !currentEntry->notes().isEmpty();
} }
GroupView* DatabaseWidget::groupView() {
return m_groupView;
}
EntryView* DatabaseWidget::entryView() {
return m_entryView;
}

View File

@ -62,6 +62,7 @@ public:
bool canDeleteCurrentGroup() const; bool canDeleteCurrentGroup() const;
bool isInSearchMode() const; bool isInSearchMode() const;
QString getCurrentSearch(); QString getCurrentSearch();
Group* currentGroup() const;
int addWidget(QWidget* w); int addWidget(QWidget* w);
void setCurrentIndex(int index); void setCurrentIndex(int index);
void setCurrentWidget(QWidget* widget); void setCurrentWidget(QWidget* widget);
@ -83,6 +84,8 @@ public:
bool currentEntryHasPassword(); bool currentEntryHasPassword();
bool currentEntryHasUrl(); bool currentEntryHasUrl();
bool currentEntryHasNotes(); bool currentEntryHasNotes();
GroupView* groupView();
EntryView* entryView();
Q_SIGNALS: Q_SIGNALS:
void closeRequest(); void closeRequest();
@ -90,6 +93,7 @@ Q_SIGNALS:
void groupChanged(); void groupChanged();
void entrySelectionChanged(); void entrySelectionChanged();
void databaseChanged(Database* newDb); void databaseChanged(Database* newDb);
void databaseMerged(Database* mergedDb);
void groupContextMenuRequested(const QPoint& globalPos); void groupContextMenuRequested(const QPoint& globalPos);
void entryContextMenuRequested(const QPoint& globalPos); void entryContextMenuRequested(const QPoint& globalPos);
void unlockedDatabase(); void unlockedDatabase();
@ -116,12 +120,15 @@ public Q_SLOTS:
void openUrlForEntry(Entry* entry); void openUrlForEntry(Entry* entry);
void createGroup(); void createGroup();
void deleteGroup(); void deleteGroup();
void switchToView(bool accepted);
void switchToEntryEdit(); void switchToEntryEdit();
void switchToGroupEdit(); void switchToGroupEdit();
void switchToMasterKeyChange(); void switchToMasterKeyChange();
void switchToDatabaseSettings(); void switchToDatabaseSettings();
void switchToOpenDatabase(const QString& fileName); void switchToOpenDatabase(const QString& fileName);
void switchToOpenDatabase(const QString& fileName, const QString& password, const QString& keyFile); void switchToOpenDatabase(const QString& fileName, const QString& password, const QString& keyFile);
void switchToOpenMergeDatabase(const QString& fileName);
void switchToOpenMergeDatabase(const QString& fileName, const QString& password, const QString& keyFile);
void switchToImportKeepass1(const QString& fileName); void switchToImportKeepass1(const QString& fileName);
// Search related slots // Search related slots
void search(const QString& searchtext); void search(const QString& searchtext);
@ -132,7 +139,6 @@ public Q_SLOTS:
private Q_SLOTS: private Q_SLOTS:
void entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column); void entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column);
void switchBackToEntryEdit(); void switchBackToEntryEdit();
void switchToView(bool accepted);
void switchToHistoryView(Entry* entry); void switchToHistoryView(Entry* entry);
void switchToEntryEdit(Entry* entry); void switchToEntryEdit(Entry* entry);
void switchToEntryEdit(Entry* entry, bool create); void switchToEntryEdit(Entry* entry, bool create);
@ -141,6 +147,7 @@ private Q_SLOTS:
void emitEntryContextMenuRequested(const QPoint& pos); void emitEntryContextMenuRequested(const QPoint& pos);
void updateMasterKey(bool accepted); void updateMasterKey(bool accepted);
void openDatabase(bool accepted); void openDatabase(bool accepted);
void mergeDatabase(bool accepted);
void unlockDatabase(bool accepted); void unlockDatabase(bool accepted);
void emitCurrentModeChanged(); void emitCurrentModeChanged();
void clearLastGroup(Group* group); void clearLastGroup(Group* group);
@ -158,6 +165,7 @@ private:
ChangeMasterKeyWidget* m_changeMasterKeyWidget; ChangeMasterKeyWidget* m_changeMasterKeyWidget;
DatabaseSettingsWidget* m_databaseSettingsWidget; DatabaseSettingsWidget* m_databaseSettingsWidget;
DatabaseOpenWidget* m_databaseOpenWidget; DatabaseOpenWidget* m_databaseOpenWidget;
DatabaseOpenWidget* m_databaseOpenMergeWidget;
KeePass1OpenWidget* m_keepass1OpenWidget; KeePass1OpenWidget* m_keepass1OpenWidget;
UnlockDatabaseWidget* m_unlockDatabaseWidget; UnlockDatabaseWidget* m_unlockDatabaseWidget;
QSplitter* m_splitter; QSplitter* m_splitter;

View File

@ -213,6 +213,8 @@ MainWindow::MainWindow()
SLOT(saveDatabaseAs())); SLOT(saveDatabaseAs()));
connect(m_ui->actionDatabaseClose, SIGNAL(triggered()), m_ui->tabWidget, connect(m_ui->actionDatabaseClose, SIGNAL(triggered()), m_ui->tabWidget,
SLOT(closeDatabase())); SLOT(closeDatabase()));
connect(m_ui->actionDatabaseMerge, SIGNAL(triggered()), m_ui->tabWidget,
SLOT(mergeDatabase()));
connect(m_ui->actionChangeMasterKey, SIGNAL(triggered()), m_ui->tabWidget, connect(m_ui->actionChangeMasterKey, SIGNAL(triggered()), m_ui->tabWidget,
SLOT(changeMasterKey())); SLOT(changeMasterKey()));
connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget,
@ -378,6 +380,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionDatabaseSave->setEnabled(true); m_ui->actionDatabaseSave->setEnabled(true);
m_ui->actionDatabaseSaveAs->setEnabled(true); m_ui->actionDatabaseSaveAs->setEnabled(true);
m_ui->actionExportCsv->setEnabled(true); m_ui->actionExportCsv->setEnabled(true);
m_ui->actionDatabaseMerge->setEnabled(m_ui->tabWidget->currentIndex() != -1);
m_searchWidgetAction->setEnabled(true); m_searchWidgetAction->setEnabled(true);
break; break;
@ -405,6 +408,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionDatabaseSave->setEnabled(false); m_ui->actionDatabaseSave->setEnabled(false);
m_ui->actionDatabaseSaveAs->setEnabled(false); m_ui->actionDatabaseSaveAs->setEnabled(false);
m_ui->actionExportCsv->setEnabled(false); m_ui->actionExportCsv->setEnabled(false);
m_ui->actionDatabaseMerge->setEnabled(false);
m_searchWidgetAction->setEnabled(false); m_searchWidgetAction->setEnabled(false);
break; break;
@ -437,6 +441,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionDatabaseSaveAs->setEnabled(false); m_ui->actionDatabaseSaveAs->setEnabled(false);
m_ui->actionDatabaseClose->setEnabled(false); m_ui->actionDatabaseClose->setEnabled(false);
m_ui->actionExportCsv->setEnabled(false); m_ui->actionExportCsv->setEnabled(false);
m_ui->actionDatabaseMerge->setEnabled(false);
m_searchWidgetAction->setEnabled(false); m_searchWidgetAction->setEnabled(false);
} }
@ -446,6 +451,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
m_ui->actionImportKeePass1->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionImportKeePass1->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
m_ui->actionDatabaseMerge->setEnabled(inDatabaseTabWidget);
m_ui->actionRepairDatabase->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionRepairDatabase->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);
m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases());

View File

@ -119,6 +119,7 @@
<addaction name="actionChangeMasterKey"/> <addaction name="actionChangeMasterKey"/>
<addaction name="actionChangeDatabaseSettings"/> <addaction name="actionChangeDatabaseSettings"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionDatabaseMerge"/>
<addaction name="actionImportKeePass1"/> <addaction name="actionImportKeePass1"/>
<addaction name="actionExportCsv"/> <addaction name="actionExportCsv"/>
<addaction name="actionRepairDatabase"/> <addaction name="actionRepairDatabase"/>
@ -243,6 +244,11 @@
<string>&amp;New database</string> <string>&amp;New database</string>
</property> </property>
</action> </action>
<action name="actionDatabaseMerge">
<property name="text">
<string>Merge from KeePassX database</string>
</property>
</action>
<action name="actionEntryNew"> <action name="actionEntryNew">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>

View File

@ -201,7 +201,7 @@ Only entries with the same scheme (http://, https://, ftp://, ...) are returned<
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout_1">
<item> <item>
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="sizePolicy"> <property name="sizePolicy">
@ -225,7 +225,7 @@ Only entries with the same scheme (http://, https://, ftp://, ...) are returned<
</layout> </layout>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
<property name="sizePolicy"> <property name="sizePolicy">

View File

@ -19,6 +19,7 @@
#include <QPointer> #include <QPointer>
#include <QSignalSpy> #include <QSignalSpy>
#include <QDebug>
#include <QTest> #include <QTest>
#include "core/Database.h" #include "core/Database.h"
@ -449,3 +450,120 @@ void TestGroup::testCopyCustomIcons()
delete dbTarget; delete dbTarget;
delete dbSource; delete dbSource;
} }
void TestGroup::testMerge()
{
Group* group1 = new Group();
group1->setName("group 1");
Group* group2 = new Group();
group2->setName("group 2");
Entry* entry1 = new Entry();
Entry* entry2 = new Entry();
entry1->setGroup(group1);
entry1->setUuid(Uuid::random());
entry2->setGroup(group1);
entry2->setUuid(Uuid::random());
group2->merge(group1);
QCOMPARE(group1->entries().size(), 2);
QCOMPARE(group2->entries().size(), 2);
}
void TestGroup::testMergeDatabase()
{
Database* dbSource = createMergeTestDatabase();
Database* dbDest = new Database();
dbDest->merge(dbSource);
QCOMPARE(dbDest->rootGroup()->children().size(), 2);
QCOMPARE(dbDest->rootGroup()->children().at(0)->entries().size(), 2);
delete dbDest;
delete dbSource;
}
void TestGroup::testMergeConflict()
{
Database* dbSource = createMergeTestDatabase();
// test merging updated entries
// falls back to KeepBoth mode
Database* dbCopy = new Database();
dbCopy->setRootGroup(dbSource->rootGroup()->clone(Entry::CloneNoFlags));
// sanity check
QCOMPARE(dbCopy->rootGroup()->children().at(0)->entries().size(), 2);
// make this entry newer than in original db
Entry* updatedEntry = dbCopy->rootGroup()->children().at(0)->entries().at(0);
TimeInfo updatedTimeInfo = updatedEntry->timeInfo();
updatedTimeInfo.setLastModificationTime(updatedTimeInfo.lastModificationTime().addYears(1));
updatedEntry->setTimeInfo(updatedTimeInfo);
dbCopy->merge(dbSource);
// one entry is duplicated because of mode
QCOMPARE(dbCopy->rootGroup()->children().at(0)->entries().size(), 2);
delete dbSource;
delete dbCopy;
}
void TestGroup::testMergeConflictKeepBoth()
{
Database* dbSource = createMergeTestDatabase();
// test merging updated entries
// falls back to KeepBoth mode
Database* dbCopy = new Database();
dbCopy->setRootGroup(dbSource->rootGroup()->clone(Entry::CloneNoFlags));
// sanity check
QCOMPARE(dbCopy->rootGroup()->children().at(0)->entries().size(), 2);
// make this entry newer than in original db
Entry* updatedEntry = dbCopy->rootGroup()->children().at(0)->entries().at(0);
TimeInfo updatedTimeInfo = updatedEntry->timeInfo();
updatedTimeInfo.setLastModificationTime(updatedTimeInfo.lastModificationTime().addYears(1));
updatedEntry->setTimeInfo(updatedTimeInfo);
dbCopy->rootGroup()->setMergeMode(Group::MergeMode::KeepBoth);
dbCopy->merge(dbSource);
// one entry is duplicated because of mode
QCOMPARE(dbCopy->rootGroup()->children().at(0)->entries().size(), 3);
// the older entry was merged from the other db as last in the group
Entry* olderEntry = dbCopy->rootGroup()->children().at(0)->entries().at(2);
QVERIFY2(olderEntry->attributes()->hasKey("merged"), "older entry is marked with an attribute \"merged\"");
delete dbSource;
delete dbCopy;
}
Database* TestGroup::createMergeTestDatabase()
{
Database* db = new Database();
Group* group1 = new Group();
group1->setName("group 1");
Group* group2 = new Group();
group2->setName("group 2");
Entry* entry1 = new Entry();
Entry* entry2 = new Entry();
entry1->setGroup(group1);
entry1->setUuid(Uuid::random());
entry2->setGroup(group1);
entry2->setUuid(Uuid::random());
group1->setParent(db->rootGroup());
group2->setParent(db->rootGroup());
return db;
}

View File

@ -19,6 +19,7 @@
#define KEEPASSX_TESTGROUP_H #define KEEPASSX_TESTGROUP_H
#include <QObject> #include <QObject>
#include "core/Database.h"
class TestGroup : public QObject class TestGroup : public QObject
{ {
@ -33,6 +34,13 @@ private Q_SLOTS:
void testCopyCustomIcon(); void testCopyCustomIcon();
void testClone(); void testClone();
void testCopyCustomIcons(); void testCopyCustomIcons();
void testMerge();
void testMergeConflict();
void testMergeDatabase();
void testMergeConflictKeepBoth();
private:
Database* createMergeTestDatabase();
}; };
#endif // KEEPASSX_TESTGROUP_H #endif // KEEPASSX_TESTGROUP_H

Binary file not shown.

View File

@ -29,6 +29,7 @@
#include <QToolBar> #include <QToolBar>
#include <QToolButton> #include <QToolButton>
#include <QTimer> #include <QTimer>
#include <QSignalSpy>
#include "config-keepassx-tests.h" #include "config-keepassx-tests.h"
#include "core/Config.h" #include "core/Config.h"
@ -107,6 +108,37 @@ void TestGui::cleanup()
m_dbWidget = nullptr; m_dbWidget = nullptr;
} }
void TestGui::testMergeDatabase()
{
// this triggers a warning. Perhaps similar to https://bugreports.qt.io/browse/QTBUG-49623 ?
QSignalSpy dbMergeSpy(m_tabWidget->currentWidget(), SIGNAL(databaseMerged(Database*)));
// set file to merge from
fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx"));
triggerAction("actionDatabaseMerge");
QWidget* databaseOpenMergeWidget = m_mainWindow->findChild<QWidget*>("databaseOpenMergeWidget");
QLineEdit* editPasswordMerge = databaseOpenMergeWidget->findChild<QLineEdit*>("editPassword");
QVERIFY(editPasswordMerge->isVisible());
m_tabWidget->currentDatabaseWidget()->setCurrentWidget(databaseOpenMergeWidget);
QTest::keyClicks(editPasswordMerge, "a");
QTest::keyClick(editPasswordMerge, Qt::Key_Enter);
QTRY_COMPARE(dbMergeSpy.count(), 1);
QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).contains("*"));
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::testTabs() void TestGui::testTabs()
{ {
QCOMPARE(m_tabWidget->count(), 1); QCOMPARE(m_tabWidget->count(), 1);

View File

@ -38,6 +38,7 @@ private Q_SLOTS:
void cleanup(); void cleanup();
void cleanupTestCase(); void cleanupTestCase();
void testMergeDatabase();
void testTabs(); void testTabs();
void testEditEntry(); void testEditEntry();
void testAddEntry(); void testAddEntry();