Introduce synchronize merge method

* Create history-based merging that keeps older data in history instead of discarding or deleting it
* Extract merge logic into the Merger class
* Allows special merge behavior
* Improve handling of deletion and changes on groups
* Enable basic change tracking while merging
* Prevent unintended timestamp changes while merging
* Handle differences in timestamp precision
* Introduce comparison operators to allow for more sophisticated comparisons (ignore special properties, ...)
* Introduce Clock class to handle datetime across the app

Merge Strategies:
* Default (use inherited/fallback method)
* Duplicate (duplicate conflicting nodes, apply all deletions)
* KeepLocal (use local values, but apply all deletions)
* KeepRemote (use remote values, but apply all deletions)
* KeepNewer (merge history only)
* Synchronize (merge history, newest value stays on top, apply all deletions)
This commit is contained in:
Jonathan White
2018-09-30 08:45:06 -04:00
committed by Jonathan White
parent b40e5686dc
commit c1e9f45df9
43 changed files with 2777 additions and 585 deletions

View File

@@ -115,3 +115,21 @@ void AutoTypeAssociations::clear()
{
m_associations.clear();
}
bool AutoTypeAssociations::operator==(const AutoTypeAssociations& other) const
{
if (m_associations.count() != other.m_associations.count()) {
return false;
}
for (int i = 0; i < m_associations.count(); ++i) {
if (m_associations[i] != other.m_associations[i]) {
return false;
}
}
return true;
}
bool AutoTypeAssociations::operator!=(const AutoTypeAssociations& other) const
{
return !(*this == other);
}

View File

@@ -46,6 +46,9 @@ public:
int associationsSize() const;
void clear();
bool operator==(const AutoTypeAssociations& other) const;
bool operator!=(const AutoTypeAssociations& other) const;
private:
QList<AutoTypeAssociations::Association> m_associations;

109
src/core/Clock.cpp Normal file
View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2018 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 "Clock.h"
QSharedPointer<Clock> Clock::m_instance = QSharedPointer<Clock>();
QDateTime Clock::currentDateTimeUtc()
{
return instance().currentDateTimeUtcImpl();
}
QDateTime Clock::currentDateTime()
{
return instance().currentDateTimeImpl();
}
uint Clock::currentSecondsSinceEpoch()
{
return instance().currentDateTimeImpl().toTime_t();
}
QDateTime Clock::serialized(const QDateTime& dateTime)
{
auto time = dateTime.time();
if (time.isValid() && time.msec() != 0) {
return dateTime.addMSecs(-time.msec());
}
return dateTime;
}
QDateTime Clock::datetimeUtc(int year, int month, int day, int hour, int min, int second)
{
return QDateTime(QDate(year, month, day), QTime(hour, min, second), Qt::UTC);
}
QDateTime Clock::datetime(int year, int month, int day, int hour, int min, int second)
{
return QDateTime(QDate(year, month, day), QTime(hour, min, second), Qt::LocalTime);
}
QDateTime Clock::datetimeUtc(qint64 msecSinceEpoch)
{
return QDateTime::fromMSecsSinceEpoch(msecSinceEpoch, Qt::UTC);
}
QDateTime Clock::datetime(qint64 msecSinceEpoch)
{
return QDateTime::fromMSecsSinceEpoch(msecSinceEpoch, Qt::LocalTime);
}
QDateTime Clock::parse(const QString& text, Qt::DateFormat format)
{
return QDateTime::fromString(text, format);
}
QDateTime Clock::parse(const QString& text, const QString& format)
{
return QDateTime::fromString(text, format);
}
Clock::~Clock()
{
}
Clock::Clock()
{
}
QDateTime Clock::currentDateTimeUtcImpl() const
{
return QDateTime::currentDateTimeUtc();
}
QDateTime Clock::currentDateTimeImpl() const
{
return QDateTime::currentDateTime();
}
void Clock::resetInstance()
{
m_instance.clear();
}
void Clock::setInstance(Clock* clock)
{
m_instance = QSharedPointer<Clock>(clock);
}
const Clock& Clock::instance()
{
if (!m_instance) {
m_instance = QSharedPointer<Clock>(new Clock());
}
return *m_instance;
}

58
src/core/Clock.h Normal file
View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2018 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_CLOCK_H
#define KEEPASSXC_CLOCK_H
#include <QDateTime>
#include <QSharedPointer>
class Clock
{
public:
static QDateTime currentDateTimeUtc();
static QDateTime currentDateTime();
static uint currentSecondsSinceEpoch();
static QDateTime serialized(const QDateTime& dateTime);
static QDateTime datetimeUtc(int year, int month, int day, int hour, int min, int second);
static QDateTime datetime(int year, int month, int day, int hour, int min, int second);
static QDateTime datetimeUtc(qint64 msecSinceEpoch);
static QDateTime datetime(qint64 msecSinceEpoch);
static QDateTime parse(const QString& text, Qt::DateFormat format = Qt::TextDate);
static QDateTime parse(const QString& text, const QString& format);
virtual ~Clock();
protected:
Clock();
virtual QDateTime currentDateTimeUtcImpl() const;
virtual QDateTime currentDateTimeImpl() const;
static void resetInstance();
static void setInstance(Clock* clock);
static const Clock& instance();
private:
static QSharedPointer<Clock> m_instance;
};
#endif // KEEPASSX_ENTRY_H

38
src/core/Compare.cpp Normal file
View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2018 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 "Compare.h"
#include <QColor>
bool operator<(const QColor& lhs, const QColor& rhs)
{
const QColor adaptedLhs = lhs.toCmyk();
const QColor adaptedRhs = rhs.toCmyk();
const int iCyan = compare(adaptedLhs.cyanF(), adaptedRhs.cyanF());
if (iCyan != 0) {
return iCyan;
}
const int iMagenta = compare(adaptedLhs.magentaF(), adaptedRhs.magentaF());
if (iMagenta != 0) {
return iMagenta;
}
const int iYellow = compare(adaptedLhs.yellowF(), adaptedRhs.yellowF());
if (iYellow != 0) {
return iYellow;
}
return compare(adaptedLhs.blackF(), adaptedRhs.blackF()) < 0;
}

90
src/core/Compare.h Normal file
View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2018 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_COMPARE_H
#define KEEPASSXC_COMPARE_H
#include <QDateTime>
#include "core/Clock.h"
enum CompareItemOption
{
CompareItemDefault = 0,
CompareItemIgnoreMilliseconds = 0x4,
CompareItemIgnoreStatistics = 0x8,
CompareItemIgnoreDisabled = 0x10,
CompareItemIgnoreHistory = 0x20,
CompareItemIgnoreLocation = 0x40,
};
Q_DECLARE_FLAGS(CompareItemOptions, CompareItemOption)
Q_DECLARE_OPERATORS_FOR_FLAGS(CompareItemOptions)
class QColor;
/*!
* \return true when both color match
*
* Comparison converts both into the cmyk-model
*/
bool operator<(const QColor& lhs, const QColor& rhs);
template <typename Type> inline short compareGeneric(const Type& lhs, const Type& rhs, CompareItemOptions)
{
if (lhs != rhs) {
return lhs < rhs ? -1 : +1;
}
return 0;
}
template <typename Type>
inline short compare(const Type& lhs, const Type& rhs, CompareItemOptions options = CompareItemDefault)
{
return compareGeneric(lhs, rhs, options);
}
template <> inline short compare(const QDateTime& lhs, const QDateTime& rhs, CompareItemOptions options)
{
if (!options.testFlag(CompareItemIgnoreMilliseconds)) {
return compareGeneric(lhs, rhs, options);
}
return compareGeneric(Clock::serialized(lhs), Clock::serialized(rhs), options);
}
template <typename Type>
inline short compare(bool enabled, const Type& lhs, const Type& rhs, CompareItemOptions options = CompareItemDefault)
{
if (!enabled) {
return 0;
}
return compare(lhs, rhs, options);
}
template <typename Type>
inline short compare(bool lhsEnabled,
const Type& lhs,
bool rhsEnabled,
const Type& rhs,
CompareItemOptions options = CompareItemDefault)
{
const short enabled = compareGeneric(lhsEnabled, rhsEnabled, options);
if (enabled == 0 && (!options.testFlag(CompareItemIgnoreDisabled) || (lhsEnabled && rhsEnabled))) {
return compare(lhs, rhs, options);
}
return enabled;
}
#endif // KEEPASSX_COMPARE_H

View File

@@ -27,7 +27,9 @@
#include <QXmlStreamReader>
#include "cli/Utils.h"
#include "core/Clock.h"
#include "core/Group.h"
#include "core/Merger.h"
#include "core/Metadata.h"
#include "crypto/kdf/AesKdf.h"
#include "format/KeePass2.h"
@@ -40,6 +42,7 @@ QHash<QUuid, Database*> Database::m_uuidMap;
Database::Database()
: m_metadata(new Metadata(this))
, m_rootGroup(nullptr)
, m_timer(new QTimer(this))
, m_emitModified(false)
, m_uuid(QUuid::createUuid())
@@ -216,6 +219,39 @@ QList<DeletedObject> Database::deletedObjects()
return m_deletedObjects;
}
const QList<DeletedObject>& Database::deletedObjects() const
{
return m_deletedObjects;
}
bool Database::containsDeletedObject(const QUuid& uuid) const
{
for (const DeletedObject& currentObject : m_deletedObjects) {
if (currentObject.uuid == uuid) {
return true;
}
}
return false;
}
bool Database::containsDeletedObject(const DeletedObject& object) const
{
for (const DeletedObject& currentObject : m_deletedObjects) {
if (currentObject.uuid == object.uuid) {
return true;
}
}
return false;
}
void Database::setDeletedObjects(const QList<DeletedObject>& delObjs)
{
if (m_deletedObjects == delObjs) {
return;
}
m_deletedObjects = delObjs;
}
void Database::addDeletedObject(const DeletedObject& delObj)
{
Q_ASSERT(delObj.deletionTime.timeSpec() == Qt::UTC);
@@ -225,7 +261,7 @@ void Database::addDeletedObject(const DeletedObject& delObj)
void Database::addDeletedObject(const QUuid& uuid)
{
DeletedObject delObj;
delObj.deletionTime = QDateTime::currentDateTimeUtc();
delObj.deletionTime = Clock::currentDateTimeUtc();
delObj.uuid = uuid;
addDeletedObject(delObj);
@@ -303,7 +339,7 @@ bool Database::setKey(QSharedPointer<const CompositeKey> key, bool updateChanged
m_data.transformedMasterKey = transformedMasterKey;
m_data.hasKey = true;
if (updateChangedTime) {
m_metadata->setMasterKeyChanged(QDateTime::currentDateTimeUtc());
m_metadata->setMasterKeyChanged(Clock::currentDateTimeUtc());
}
if (oldTransformedMasterKey != m_data.transformedMasterKey) {
@@ -401,21 +437,6 @@ void Database::emptyRecycleBin()
}
}
void Database::merge(const Database* other)
{
m_rootGroup->merge(other->rootGroup());
for (const QUuid& customIconId : other->metadata()->customIcons().keys()) {
QImage customIcon = other->metadata()->customIcon(customIconId);
if (!this->metadata()->containsCustomIcon(customIconId)) {
qDebug() << QString("Adding custom icon %1 to database.").arg(customIconId.toString());
this->metadata()->addCustomIcon(customIconId, customIcon);
}
}
emit modified();
}
void Database::setEmitModified(bool value)
{
if (m_emitModified && !value) {
@@ -425,6 +446,11 @@ void Database::setEmitModified(bool value)
m_emitModified = value;
}
void Database::markAsModified()
{
emit modified();
}
const QUuid& Database::uuid()
{
return m_uuid;
@@ -467,7 +493,6 @@ Database* Database::openDatabaseFile(const QString& fileName, QSharedPointer<con
KeePass2Reader reader;
Database* db = reader.readDatabase(&dbFile, key);
if (reader.hasError()) {
qCritical("Error while parsing the database: %s", qPrintable(reader.errorString()));
return nullptr;
@@ -600,7 +625,6 @@ QString Database::writeDatabase(QIODevice* device)
* @param filePath Path to the file to backup
* @return
*/
bool Database::backupDatabase(QString filePath)
{
QString backupFilePath = filePath;

View File

@@ -37,6 +37,10 @@ struct DeletedObject
{
QUuid uuid;
QDateTime deletionTime;
bool operator==(const DeletedObject& other) const
{
return uuid == other.uuid && deletionTime == other.deletionTime;
}
};
Q_DECLARE_TYPEINFO(DeletedObject, Q_MOVABLE_TYPE);
@@ -88,8 +92,12 @@ public:
Entry* resolveEntry(const QString& text, EntryReferenceType referenceType);
Group* resolveGroup(const QUuid& uuid);
QList<DeletedObject> deletedObjects();
const QList<DeletedObject>& deletedObjects() const;
void addDeletedObject(const DeletedObject& delObj);
void addDeletedObject(const QUuid& uuid);
bool containsDeletedObject(const QUuid& uuid) const;
bool containsDeletedObject(const DeletedObject& uuid) const;
void setDeletedObjects(const QList<DeletedObject>& delObjs);
const QUuid& cipher() const;
Database::CompressionAlgorithm compressionAlgo() const;
@@ -112,7 +120,7 @@ public:
void recycleGroup(Group* group);
void emptyRecycleBin();
void setEmitModified(bool value);
void merge(const Database* other);
void markAsModified();
QString saveToFile(QString filePath, bool atomic = true, bool backup = false);
/**

View File

@@ -19,6 +19,7 @@
#include "config-keepassx.h"
#include "core/Clock.h"
#include "core/Database.h"
#include "core/DatabaseIcons.h"
#include "core/Group.h"
@@ -60,6 +61,7 @@ Entry::Entry()
Entry::~Entry()
{
setUpdateTimeinfo(false);
if (m_group) {
m_group->removeEntry(this);
@@ -77,19 +79,23 @@ template <class T> inline bool Entry::set(T& property, const T& value)
property = value;
emit modified();
return true;
} else {
return false;
}
return false;
}
void Entry::updateTimeinfo()
{
if (m_updateTimeinfo) {
m_data.timeInfo.setLastModificationTime(QDateTime::currentDateTimeUtc());
m_data.timeInfo.setLastAccessTime(QDateTime::currentDateTimeUtc());
m_data.timeInfo.setLastModificationTime(Clock::currentDateTimeUtc());
m_data.timeInfo.setLastAccessTime(Clock::currentDateTimeUtc());
}
}
bool Entry::canUpdateTimeinfo() const
{
return m_updateTimeinfo;
}
void Entry::setUpdateTimeinfo(bool value)
{
m_updateTimeinfo = value;
@@ -123,6 +129,11 @@ const QUuid& Entry::uuid() const
return m_uuid;
}
const QString Entry::uuidToHex() const
{
return QString::fromLatin1(m_uuid.toRfc4122().toHex());
}
QImage Entry::icon() const
{
if (m_data.customIcon.isNull()) {
@@ -142,15 +153,13 @@ QPixmap Entry::iconPixmap() const
{
if (m_data.customIcon.isNull()) {
return databaseIcons()->iconPixmap(m_data.iconNumber);
} else {
Q_ASSERT(database());
if (database()) {
return database()->metadata()->customIconPixmap(m_data.customIcon);
} else {
return QPixmap();
}
}
Q_ASSERT(database());
if (database()) {
return database()->metadata()->customIconPixmap(m_data.customIcon);
}
return QPixmap();
}
QPixmap Entry::iconScaledPixmap() const
@@ -158,11 +167,9 @@ QPixmap Entry::iconScaledPixmap() const
if (m_data.customIcon.isNull()) {
// built-in icons are 16x16 so don't need to be scaled
return databaseIcons()->iconPixmap(m_data.iconNumber);
} else {
Q_ASSERT(database());
return database()->metadata()->customIconScaledPixmap(m_data.customIcon);
}
Q_ASSERT(database());
return database()->metadata()->customIconScaledPixmap(m_data.customIcon);
}
int Entry::iconNumber() const
@@ -195,7 +202,7 @@ QString Entry::tags() const
return m_data.tags;
}
TimeInfo Entry::timeInfo() const
const TimeInfo& Entry::timeInfo() const
{
return m_data.timeInfo;
}
@@ -300,7 +307,7 @@ QString Entry::notes() const
bool Entry::isExpired() const
{
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < QDateTime::currentDateTimeUtc();
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTimeUtc();
}
bool Entry::hasReferences() const
@@ -532,7 +539,7 @@ void Entry::removeHistoryItems(const QList<Entry*>& historyEntries)
for (Entry* entry : historyEntries) {
Q_ASSERT(!entry->parent());
Q_ASSERT(entry->uuid() == uuid());
Q_ASSERT(entry->uuid().isNull() || entry->uuid() == uuid());
Q_ASSERT(m_history.contains(entry));
m_history.removeOne(entry);
@@ -597,6 +604,42 @@ void Entry::truncateHistory()
}
}
bool Entry::equals(const Entry* other, CompareItemOptions options) const
{
if (!other) {
return false;
}
if (m_uuid != other->uuid()) {
return false;
}
if (!m_data.equals(other->m_data, options)) {
return false;
}
if (*m_customData != *other->m_customData) {
return false;
}
if (*m_attributes != *other->m_attributes) {
return false;
}
if (*m_attachments != *other->m_attachments) {
return false;
}
if (*m_autoTypeAssociations != *other->m_autoTypeAssociations) {
return false;
}
if (!options.testFlag(CompareItemIgnoreHistory)) {
if (m_history.count() != other->m_history.count()) {
return false;
}
for (int i = 0; i < m_history.count(); ++i) {
if (!m_history[i]->equals(other->m_history[i], options)) {
return false;
}
}
}
return true;
}
Entry* Entry::clone(CloneFlags flags) const
{
Entry* entry = new Entry();
@@ -613,12 +656,12 @@ Entry* Entry::clone(CloneFlags flags) const
if (flags & CloneUserAsRef) {
// Build the username reference
QString username = "{REF:U@I:" + m_uuid.toRfc4122().toHex() + "}";
QString username = "{REF:U@I:" + uuidToHex() + "}";
entry->m_attributes->set(EntryAttributes::UserNameKey, username.toUpper(), m_attributes->isProtected(EntryAttributes::UserNameKey));
}
if (flags & ClonePassAsRef) {
QString password = "{REF:P@I:" + m_uuid.toRfc4122().toHex() + "}";
QString password = "{REF:P@I:" + uuidToHex() + "}";
entry->m_attributes->set(EntryAttributes::PasswordKey, password.toUpper(), m_attributes->isProtected(EntryAttributes::PasswordKey));
}
@@ -635,7 +678,7 @@ Entry* Entry::clone(CloneFlags flags) const
entry->setUpdateTimeinfo(true);
if (flags & CloneResetTimeInfo) {
QDateTime now = QDateTime::currentDateTimeUtc();
QDateTime now = Clock::currentDateTimeUtc();
entry->m_data.timeInfo.setCreationTime(now);
entry->m_data.timeInfo.setLastModificationTime(now);
entry->m_data.timeInfo.setLastAccessTime(now);
@@ -835,7 +878,7 @@ QString Entry::referenceFieldValue(EntryReferenceType referenceType) const
case EntryReferenceType::Notes:
return notes();
case EntryReferenceType::QUuid:
return uuid().toRfc4122().toHex();
return uuidToHex();
default:
break;
}
@@ -880,7 +923,7 @@ void Entry::setGroup(Group* group)
QObject::setParent(group);
if (m_updateTimeinfo) {
m_data.timeInfo.setLocationChanged(QDateTime::currentDateTimeUtc());
m_data.timeInfo.setLocationChanged(Clock::currentDateTimeUtc());
}
}
@@ -893,9 +936,16 @@ const Database* Entry::database() const
{
if (m_group) {
return m_group->database();
} else {
return nullptr;
}
return nullptr;
}
Database* Entry::database()
{
if (m_group) {
return m_group->database();
}
return nullptr;
}
QString Entry::maskPasswordPlaceholders(const QString& str) const
@@ -955,9 +1005,11 @@ Entry::PlaceholderType Entry::placeholderType(const QString& placeholder) const
{
if (!placeholder.startsWith(QLatin1Char('{')) || !placeholder.endsWith(QLatin1Char('}'))) {
return PlaceholderType::NotPlaceholder;
} else if (placeholder.startsWith(QLatin1Literal("{S:"))) {
}
if (placeholder.startsWith(QLatin1Literal("{S:"))) {
return PlaceholderType::CustomAttribute;
} else if (placeholder.startsWith(QLatin1Literal("{REF:"))) {
}
if (placeholder.startsWith(QLatin1Literal("{REF:"))) {
return PlaceholderType::Reference;
}
@@ -1020,3 +1072,64 @@ QString Entry::resolveUrl(const QString& url) const
// No valid http URL's found
return QString("");
}
bool EntryData::operator==(const EntryData& other) const
{
return equals(other, CompareItemDefault);
}
bool EntryData::operator!=(const EntryData& other) const
{
return !(*this == other);
}
bool EntryData::equals(const EntryData& other, CompareItemOptions options) const
{
if (::compare(iconNumber, other.iconNumber, options) != 0) {
return false;
}
if (::compare(customIcon, other.customIcon, options) != 0) {
return false;
}
if (::compare(foregroundColor, other.foregroundColor, options) != 0) {
return false;
}
if (::compare(backgroundColor, other.backgroundColor, options) != 0) {
return false;
}
if (::compare(overrideUrl, other.overrideUrl, options) != 0) {
return false;
}
if (::compare(tags, other.tags, options) != 0) {
return false;
}
if (::compare(autoTypeEnabled, other.autoTypeEnabled, options) != 0) {
return false;
}
if (::compare(autoTypeObfuscation, other.autoTypeObfuscation, options) != 0) {
return false;
}
if (::compare(defaultAutoTypeSequence, other.defaultAutoTypeSequence, options) != 0) {
return false;
}
if (!timeInfo.equals(other.timeInfo, options)) {
return false;
}
if (!totpSettings.isNull() && !other.totpSettings.isNull()) {
// Both have TOTP settings, compare them
if (::compare(totpSettings->key, other.totpSettings->key, options) != 0) {
return false;
}
if (::compare(totpSettings->digits, other.totpSettings->digits, options) != 0) {
return false;
}
if (::compare(totpSettings->step, other.totpSettings->step, options) != 0) {
return false;
}
} else if (totpSettings.isNull() != other.totpSettings.isNull()) {
// The existance of TOTP has changed between these entries
return false;
}
return true;
}

View File

@@ -65,6 +65,10 @@ struct EntryData
QString defaultAutoTypeSequence;
TimeInfo timeInfo;
QSharedPointer<Totp::Settings> totpSettings;
bool operator==(const EntryData& other) const;
bool operator!=(const EntryData& other) const;
bool equals(const EntryData& other, CompareItemOptions options) const;
};
class Entry : public QObject
@@ -75,6 +79,7 @@ public:
Entry();
~Entry();
const QUuid& uuid() const;
const QString uuidToHex() const;
QImage icon() const;
QPixmap iconPixmap() const;
QPixmap iconScaledPixmap() const;
@@ -84,7 +89,7 @@ public:
QColor backgroundColor() const;
QString overrideUrl() const;
QString tags() const;
TimeInfo timeInfo() const;
const TimeInfo& timeInfo() const;
bool autoTypeEnabled() const;
int autoTypeObfuscation() const;
QString defaultAutoTypeSequence() const;
@@ -143,6 +148,8 @@ public:
void removeHistoryItems(const QList<Entry*>& historyEntries);
void truncateHistory();
bool equals(const Entry* other, CompareItemOptions options = CompareItemDefault) const;
enum CloneFlag
{
CloneNoFlags = 0,
@@ -204,7 +211,10 @@ public:
Group* group();
const Group* group() const;
void setGroup(Group* group);
const Database* database() const;
Database* database();
bool canUpdateTimeinfo() const;
void setUpdateTimeinfo(bool value);
signals:
@@ -229,7 +239,6 @@ private:
static EntryReferenceType referenceType(const QString& referenceStr);
const Database* database() const;
template <class T> bool set(T& property, const T& value);
QUuid m_uuid;
@@ -238,8 +247,8 @@ private:
QPointer<EntryAttachments> m_attachments;
QPointer<AutoTypeAssociations> m_autoTypeAssociations;
QPointer<CustomData> m_customData;
QList<Entry*> m_history; // Items sorted from oldest to newest
QList<Entry*> m_history;
Entry* m_tmpHistoryItem;
bool m_modifiedSinceBegin;
QPointer<Group> m_group;

View File

@@ -18,6 +18,7 @@
#include "Group.h"
#include "core/Clock.h"
#include "core/Config.h"
#include "core/DatabaseIcons.h"
#include "core/Global.h"
@@ -40,7 +41,7 @@ Group::Group()
m_data.isExpanded = true;
m_data.autoTypeEnabled = Inherit;
m_data.searchingEnabled = Inherit;
m_data.mergeMode = ModeInherit;
m_data.mergeMode = Default;
connect(m_customData, SIGNAL(modified()), this, SIGNAL(modified()));
connect(this, SIGNAL(modified()), SLOT(updateTimeinfo()));
@@ -48,6 +49,7 @@ Group::Group()
Group::~Group()
{
setUpdateTimeinfo(false);
// Destroy entries and children manually so DeletedObjects can be added
// to database.
const QList<Entry*> entries = m_entries;
@@ -62,7 +64,7 @@ Group::~Group()
if (m_db && m_parent) {
DeletedObject delGroup;
delGroup.deletionTime = QDateTime::currentDateTimeUtc();
delGroup.deletionTime = Clock::currentDateTimeUtc();
delGroup.uuid = m_uuid;
m_db->addDeletedObject(delGroup);
}
@@ -92,11 +94,16 @@ template <class P, class V> inline bool Group::set(P& property, const V& value)
}
}
bool Group::canUpdateTimeinfo() const
{
return m_updateTimeinfo;
}
void Group::updateTimeinfo()
{
if (m_updateTimeinfo) {
m_data.timeInfo.setLastModificationTime(QDateTime::currentDateTimeUtc());
m_data.timeInfo.setLastAccessTime(QDateTime::currentDateTimeUtc());
m_data.timeInfo.setLastModificationTime(Clock::currentDateTimeUtc());
m_data.timeInfo.setLastAccessTime(Clock::currentDateTimeUtc());
}
}
@@ -110,6 +117,11 @@ const QUuid& Group::uuid() const
return m_uuid;
}
const QString Group::uuidToHex() const
{
return QString::fromLatin1(m_uuid.toRfc4122().toHex());
}
QString Group::name() const
{
return m_data.name;
@@ -176,7 +188,7 @@ const QUuid& Group::iconUuid() const
return m_data.customIcon;
}
TimeInfo Group::timeInfo() const
const TimeInfo& Group::timeInfo() const
{
return m_data.timeInfo;
}
@@ -228,15 +240,13 @@ Group::TriState Group::searchingEnabled() const
Group::MergeMode Group::mergeMode() const
{
if (m_data.mergeMode == Group::MergeMode::ModeInherit) {
if (m_data.mergeMode == Group::MergeMode::Default) {
if (m_parent) {
return m_parent->mergeMode();
} else {
return Group::MergeMode::KeepNewer; // fallback
}
} else {
return m_data.mergeMode;
return Group::MergeMode::KeepNewer; // fallback
}
return m_data.mergeMode;
}
Entry* Group::lastTopVisibleEntry() const
@@ -246,7 +256,7 @@ Entry* Group::lastTopVisibleEntry() const
bool Group::isExpired() const
{
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < QDateTime::currentDateTimeUtc();
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTimeUtc();
}
CustomData* Group::customData()
@@ -259,6 +269,39 @@ const CustomData* Group::customData() const
return m_customData;
}
bool Group::equals(const Group* other, CompareItemOptions options) const
{
if (!other) {
return false;
}
if (m_uuid != other->m_uuid) {
return false;
}
if (!m_data.equals(other->m_data, options)) {
return false;
}
if (m_customData != other->m_customData) {
return false;
}
if (m_children.count() != other->m_children.count()) {
return false;
}
if (m_entries.count() != other->m_entries.count()) {
return false;
}
for (int i = 0; i < m_children.count(); ++i) {
if (m_children[i]->uuid() != other->m_children[i]->uuid()) {
return false;
}
}
for (int i = 0; i < m_entries.count(); ++i) {
if (m_entries[i]->uuid() != other->m_entries[i]->uuid()) {
return false;
}
}
return true;
}
void Group::setUuid(const QUuid& uuid)
{
set(m_uuid, uuid);
@@ -418,7 +461,7 @@ void Group::setParent(Group* parent, int index)
}
if (m_updateTimeinfo) {
m_data.timeInfo.setLocationChanged(QDateTime::currentDateTimeUtc());
m_data.timeInfo.setLocationChanged(Clock::currentDateTimeUtc());
}
emit modified();
@@ -536,7 +579,7 @@ Entry* Group::findEntry(QString entryId)
return nullptr;
}
Entry* Group::findEntryByUuid(const QUuid& uuid)
Entry* Group::findEntryByUuid(const QUuid& uuid) const
{
Q_ASSERT(!uuid.isNull());
for (Entry* entry : entriesRecursive(false)) {
@@ -683,61 +726,7 @@ QSet<QUuid> Group::customIconsRecursive() const
return result;
}
void Group::merge(const Group* other)
{
Group* rootGroup = this;
while (rootGroup->parentGroup()) {
rootGroup = rootGroup->parentGroup();
}
// merge entries
const QList<Entry*> dbEntries = other->entries();
for (Entry* entry : dbEntries) {
Entry* existingEntry = rootGroup->findEntryByUuid(entry->uuid());
if (!existingEntry) {
// This entry does not exist at all. Create it.
qDebug("New entry %s detected. Creating it.", qPrintable(entry->title()));
entry->clone(Entry::CloneIncludeHistory)->setGroup(this);
} else {
// Entry is already present in the database. Update it.
bool locationChanged = existingEntry->timeInfo().locationChanged() < entry->timeInfo().locationChanged();
if (locationChanged && existingEntry->group() != this) {
existingEntry->setGroup(this);
qDebug("Location changed for entry %s. Updating it", qPrintable(existingEntry->title()));
}
resolveEntryConflict(existingEntry, entry);
}
}
// merge groups recursively
const QList<Group*> dbChildren = other->children();
for (Group* group : dbChildren) {
Group* existingGroup = rootGroup->findChildByUuid(group->uuid());
if (!existingGroup) {
qDebug("New group %s detected. Creating it.", qPrintable(group->name()));
Group* newGroup = group->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
newGroup->setParent(this);
newGroup->merge(group);
} else {
bool locationChanged = existingGroup->timeInfo().locationChanged() < group->timeInfo().locationChanged();
if (locationChanged && existingGroup->parent() != this) {
existingGroup->setParent(this);
qDebug("Location changed for group %s. Updating it", qPrintable(existingGroup->name()));
}
resolveGroupConflict(existingGroup, group);
existingGroup->merge(group);
}
}
emit modified();
}
Group* Group::findChildByUuid(const QUuid& uuid)
Group* Group::findGroupByUuid(const QUuid& uuid)
{
Q_ASSERT(!uuid.isNull());
for (Group* group : groupsRecursive(true)) {
@@ -792,7 +781,7 @@ Group* Group::clone(Entry::CloneFlags entryFlags, Group::CloneFlags groupFlags)
clonedGroup->setUpdateTimeinfo(true);
if (groupFlags & Group::CloneResetTimeInfo) {
QDateTime now = QDateTime::currentDateTimeUtc();
QDateTime now = Clock::currentDateTimeUtc();
clonedGroup->m_data.timeInfo.setCreationTime(now);
clonedGroup->m_data.timeInfo.setLastModificationTime(now);
clonedGroup->m_data.timeInfo.setLastAccessTime(now);
@@ -828,7 +817,9 @@ void Group::addEntry(Entry* entry)
void Group::removeEntry(Entry* entry)
{
Q_ASSERT(m_entries.contains(entry));
Q_ASSERT_X(m_entries.contains(entry),
Q_FUNC_INFO,
QString("Group %1 does not contain %2").arg(this->name()).arg(entry->title()).toLatin1());
emit entryAboutToRemove(entry);
@@ -905,12 +896,6 @@ void Group::recCreateDelObjects()
}
}
void Group::markOlderEntry(Entry* entry)
{
entry->attributes()->set(
"merged", tr("older entry merged from database \"%1\"").arg(entry->group()->database()->metadata()->name()));
}
bool Group::resolveSearchingEnabled() const
{
switch (m_data.searchingEnabled) {
@@ -949,63 +934,6 @@ bool Group::resolveAutoTypeEnabled() const
}
}
void Group::resolveEntryConflict(Entry* existingEntry, Entry* otherEntry)
{
const QDateTime timeExisting = existingEntry->timeInfo().lastModificationTime();
const QDateTime timeOther = otherEntry->timeInfo().lastModificationTime();
Entry* clonedEntry;
switch (mergeMode()) {
case KeepBoth:
// if one entry is newer, create a clone and add it to the group
if (timeExisting > timeOther) {
clonedEntry = otherEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory);
clonedEntry->setGroup(this);
markOlderEntry(clonedEntry);
} else if (timeExisting < timeOther) {
clonedEntry = otherEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory);
clonedEntry->setGroup(this);
markOlderEntry(existingEntry);
}
break;
case KeepNewer:
if (timeExisting < timeOther) {
qDebug("Updating entry %s.", qPrintable(existingEntry->title()));
// only if other entry is newer, replace existing one
Group* currentGroup = existingEntry->group();
currentGroup->removeEntry(existingEntry);
otherEntry->clone(Entry::CloneIncludeHistory)->setGroup(currentGroup);
}
break;
case KeepExisting:
break;
default:
// do nothing
break;
}
}
void Group::resolveGroupConflict(Group* existingGroup, Group* otherGroup)
{
const QDateTime timeExisting = existingGroup->timeInfo().lastModificationTime();
const QDateTime timeOther = otherGroup->timeInfo().lastModificationTime();
// only if the other group is newer, update the existing one.
if (timeExisting < timeOther) {
qDebug("Updating group %s.", qPrintable(existingGroup->name()));
existingGroup->setName(otherGroup->name());
existingGroup->setNotes(otherGroup->notes());
if (otherGroup->iconNumber() == 0) {
existingGroup->setIcon(otherGroup->iconUuid());
} else {
existingGroup->setIcon(otherGroup->iconNumber());
}
existingGroup->setExpiryTime(otherGroup->timeInfo().expiryTime());
}
}
QStringList Group::locate(QString locateTerm, QString currentPath)
{
Q_ASSERT(!locateTerm.isNull());
@@ -1054,3 +982,49 @@ Entry* Group::addEntryWithPath(QString entryPath)
return entry;
}
bool Group::GroupData::operator==(const Group::GroupData& other) const
{
return equals(other, CompareItemDefault);
}
bool Group::GroupData::operator!=(const Group::GroupData& other) const
{
return !(*this == other);
}
bool Group::GroupData::equals(const Group::GroupData& other, CompareItemOptions options) const
{
if (::compare(name, other.name, options) != 0) {
return false;
}
if (::compare(notes, other.notes, options) != 0) {
return false;
}
if (::compare(iconNumber, other.iconNumber) != 0) {
return false;
}
if (::compare(customIcon, other.customIcon) != 0) {
return false;
}
if (timeInfo.equals(other.timeInfo, options) != 0) {
return false;
}
// TODO HNH: Some properties are configurable - should they be ignored?
if (::compare(isExpanded, other.isExpanded, options) != 0) {
return false;
}
if (::compare(defaultAutoTypeSequence, other.defaultAutoTypeSequence, options) != 0) {
return false;
}
if (::compare(autoTypeEnabled, other.autoTypeEnabled, options) != 0) {
return false;
}
if (::compare(searchingEnabled, other.searchingEnabled, options) != 0) {
return false;
}
if (::compare(mergeMode, other.mergeMode, options) != 0) {
return false;
}
return true;
}

View File

@@ -42,10 +42,12 @@ public:
};
enum MergeMode
{
ModeInherit,
KeepBoth,
KeepNewer,
KeepExisting
Default, // Determine merge strategy from parent or fallback (Synchronize)
Duplicate, // lossy strategy regarding deletions, duplicate older changes in a new entry
KeepLocal, // merge history forcing local as top regardless of age
KeepRemote, // merge history forcing remote as top regardless of age
KeepNewer, // merge history
Synchronize, // merge history keeping most recent as top entry and appling deletions
};
enum CloneFlag
@@ -69,6 +71,10 @@ public:
Group::TriState autoTypeEnabled;
Group::TriState searchingEnabled;
Group::MergeMode mergeMode;
bool operator==(const GroupData& other) const;
bool operator!=(const GroupData& other) const;
bool equals(const GroupData& other, CompareItemOptions options) const;
};
Group();
@@ -77,6 +83,7 @@ public:
static Group* createRecycleBin();
const QUuid& uuid() const;
const QString uuidToHex() const;
QString name() const;
QString notes() const;
QImage icon() const;
@@ -84,7 +91,7 @@ public:
QPixmap iconScaledPixmap() const;
int iconNumber() const;
const QUuid& iconUuid() const;
TimeInfo timeInfo() const;
const TimeInfo& timeInfo() const;
bool isExpanded() const;
QString defaultAutoTypeSequence() const;
QString effectiveAutoTypeSequence() const;
@@ -98,6 +105,8 @@ public:
CustomData* customData();
const CustomData* customData() const;
bool equals(const Group* other, CompareItemOptions options) const;
static const int DefaultIconNumber;
static const int RecycleBinIconNumber;
static CloneFlags DefaultCloneFlags;
@@ -105,10 +114,10 @@ public:
static const QString RootAutoTypeSequence;
Group* findChildByName(const QString& name);
Group* findChildByUuid(const QUuid& uuid);
Entry* findEntry(QString entryId);
Entry* findEntryByUuid(const QUuid& uuid);
Entry* findEntryByUuid(const QUuid& uuid) const;
Entry* findEntryByPath(QString entryPath, QString basePath = QString(""));
Group* findGroupByUuid(const QUuid& uuid);
Group* findGroupByPath(QString groupPath);
QStringList locate(QString locateTerm, QString currentPath = QString("/"));
Entry* addEntryWithPath(QString entryPath);
@@ -127,6 +136,7 @@ public:
void setExpiryTime(const QDateTime& dateTime);
void setMergeMode(MergeMode newMode);
bool canUpdateTimeinfo() const;
void setUpdateTimeinfo(bool value);
Group* parentGroup();
@@ -153,9 +163,10 @@ public:
CloneFlags groupFlags = DefaultCloneFlags) const;
void copyDataFrom(const Group* other);
void merge(const Group* other);
QString print(bool recursive = false, int depth = 0);
void addEntry(Entry* entry);
void removeEntry(Entry* entry);
signals:
void dataChanged(Group* group);
@@ -184,12 +195,7 @@ private slots:
private:
template <class P, class V> bool set(P& property, const V& value);
void addEntry(Entry* entry);
void removeEntry(Entry* entry);
void setParent(Database* db);
void markOlderEntry(Entry* entry);
void resolveEntryConflict(Entry* existingEntry, Entry* otherEntry);
void resolveGroupConflict(Group* existingGroup, Group* otherGroup);
void recSetDatabase(Database* db);
void cleanupParent();

613
src/core/Merger.cpp Normal file
View File

@@ -0,0 +1,613 @@
/*
* Copyright (C) 2018 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 "Merger.h"
#include "core/Clock.h"
#include "core/Database.h"
#include "core/Entry.h"
#include "core/Metadata.h"
Merger::Merger(const Database* sourceDb, Database* targetDb)
: m_mode(Group::Default)
{
if (!sourceDb || !targetDb) {
Q_ASSERT(sourceDb && targetDb);
return;
}
m_context = MergeContext{
sourceDb, targetDb, sourceDb->rootGroup(), targetDb->rootGroup(), sourceDb->rootGroup(), targetDb->rootGroup()};
}
Merger::Merger(const Group* sourceGroup, Group* targetGroup)
: m_mode(Group::Default)
{
if (!sourceGroup || !targetGroup) {
Q_ASSERT(sourceGroup && targetGroup);
return;
}
m_context = MergeContext{sourceGroup->database(),
targetGroup->database(),
sourceGroup->database()->rootGroup(),
targetGroup->database()->rootGroup(),
sourceGroup,
targetGroup};
}
void Merger::setForcedMergeMode(Group::MergeMode mode)
{
m_mode = mode;
}
void Merger::resetForcedMergeMode()
{
m_mode = Group::Default;
}
bool Merger::merge()
{
// Order of merge steps is important - it is possible that we
// create some items before deleting them afterwards
ChangeList changes;
changes << mergeGroup(m_context);
changes << mergeDeletions(m_context);
changes << mergeMetadata(m_context);
// qDebug("Merged %s", qPrintable(changes.join("\n\t")));
// At this point we have a list of changes we may want to show the user
if (!changes.isEmpty()) {
m_context.m_targetDb->markAsModified();
return true;
}
return false;
}
Merger::ChangeList Merger::mergeGroup(const MergeContext& context)
{
ChangeList changes;
// merge entries
const QList<Entry*> sourceEntries = context.m_sourceGroup->entries();
for (Entry* sourceEntry : sourceEntries) {
Entry* targetEntry = context.m_targetRootGroup->findEntryByUuid(sourceEntry->uuid());
if (!targetEntry) {
changes << tr("Creating missing %1 [%2]").arg(sourceEntry->title(), sourceEntry->uuidToHex());
// This entry does not exist at all. Create it.
targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
moveEntry(targetEntry, context.m_targetGroup);
} else {
// Entry is already present in the database. Update it.
const bool locationChanged = targetEntry->timeInfo().locationChanged() < sourceEntry->timeInfo().locationChanged();
if (locationChanged && targetEntry->group() != context.m_targetGroup) {
changes << tr("Relocating %1 [%2]").arg(sourceEntry->title()).arg(sourceEntry->uuidToHex());
moveEntry(targetEntry, context.m_targetGroup);
}
changes << resolveEntryConflict(context, sourceEntry, targetEntry);
}
}
// merge groups recursively
const QList<Group*> sourceChildGroups = context.m_sourceGroup->children();
for (Group* sourceChildGroup : sourceChildGroups) {
Group* targetChildGroup = context.m_targetRootGroup->findGroupByUuid(sourceChildGroup->uuid());
if (!targetChildGroup) {
changes << tr("Creating missing %1 [%2]").arg(sourceChildGroup->name()).arg(sourceChildGroup->uuidToHex());
targetChildGroup = sourceChildGroup->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
moveGroup(targetChildGroup, context.m_targetGroup);
TimeInfo timeinfo = targetChildGroup->timeInfo();
timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged());
targetChildGroup->setTimeInfo(timeinfo);
} else {
bool locationChanged =
targetChildGroup->timeInfo().locationChanged() < sourceChildGroup->timeInfo().locationChanged();
if (locationChanged && targetChildGroup->parent() != context.m_targetGroup) {
changes << tr("Relocating %1 [%2]").arg(sourceChildGroup->name()).arg(sourceChildGroup->uuidToHex());
moveGroup(targetChildGroup, context.m_targetGroup);
TimeInfo timeinfo = targetChildGroup->timeInfo();
timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged());
targetChildGroup->setTimeInfo(timeinfo);
}
changes << resolveGroupConflict(context, sourceChildGroup, targetChildGroup);
}
MergeContext subcontext{context.m_sourceDb,
context.m_targetDb,
context.m_sourceRootGroup,
context.m_targetRootGroup,
sourceChildGroup,
targetChildGroup};
changes << mergeGroup(subcontext);
}
return changes;
}
Merger::ChangeList Merger::resolveGroupConflict(const MergeContext& context, const Group* sourceChildGroup, Group* targetChildGroup)
{
Q_UNUSED(context);
ChangeList changes;
const QDateTime timeExisting = targetChildGroup->timeInfo().lastModificationTime();
const QDateTime timeOther = sourceChildGroup->timeInfo().lastModificationTime();
// only if the other group is newer, update the existing one.
if (timeExisting < timeOther) {
changes << tr("Overwriting %1 [%2]").arg(sourceChildGroup->name()).arg(sourceChildGroup->uuidToHex());
targetChildGroup->setName(sourceChildGroup->name());
targetChildGroup->setNotes(sourceChildGroup->notes());
if (sourceChildGroup->iconNumber() == 0) {
targetChildGroup->setIcon(sourceChildGroup->iconUuid());
} else {
targetChildGroup->setIcon(sourceChildGroup->iconNumber());
}
targetChildGroup->setExpiryTime(sourceChildGroup->timeInfo().expiryTime());
TimeInfo timeInfo = targetChildGroup->timeInfo();
timeInfo.setLastModificationTime(timeOther);
targetChildGroup->setTimeInfo(timeInfo);
}
return changes;
}
bool Merger::markOlderEntry(Entry* entry)
{
entry->attributes()->set(
"merged", tr("older entry merged from database \"%1\"").arg(entry->group()->database()->metadata()->name()));
return true;
}
void Merger::moveEntry(Entry* entry, Group* targetGroup)
{
Q_ASSERT(entry);
Group* sourceGroup = entry->group();
if (sourceGroup == targetGroup) {
return;
}
const bool sourceGroupUpdateTimeInfo = sourceGroup ? sourceGroup->canUpdateTimeinfo() : false;
if (sourceGroup) {
sourceGroup->setUpdateTimeinfo(false);
}
const bool targetGroupUpdateTimeInfo = targetGroup ? targetGroup->canUpdateTimeinfo() : false;
if (targetGroup) {
targetGroup->setUpdateTimeinfo(false);
}
const bool entryUpdateTimeInfo = entry->canUpdateTimeinfo();
entry->setUpdateTimeinfo(false);
entry->setGroup(targetGroup);
entry->setUpdateTimeinfo(entryUpdateTimeInfo);
if (targetGroup) {
targetGroup->setUpdateTimeinfo(targetGroupUpdateTimeInfo);
}
if (sourceGroup) {
sourceGroup->setUpdateTimeinfo(sourceGroupUpdateTimeInfo);
}
}
void Merger::moveGroup(Group* group, Group* targetGroup)
{
Q_ASSERT(group);
Group* sourceGroup = group->parentGroup();
if (sourceGroup == targetGroup) {
return;
}
const bool sourceGroupUpdateTimeInfo = sourceGroup ? sourceGroup->canUpdateTimeinfo() : false;
if (sourceGroup) {
sourceGroup->setUpdateTimeinfo(false);
}
const bool targetGroupUpdateTimeInfo = targetGroup ? targetGroup->canUpdateTimeinfo() : false;
if (targetGroup) {
targetGroup->setUpdateTimeinfo(false);
}
const bool groupUpdateTimeInfo = group->canUpdateTimeinfo();
group->setUpdateTimeinfo(false);
group->setParent(targetGroup);
group->setUpdateTimeinfo(groupUpdateTimeInfo);
if (targetGroup) {
targetGroup->setUpdateTimeinfo(targetGroupUpdateTimeInfo);
}
if (sourceGroup) {
sourceGroup->setUpdateTimeinfo(sourceGroupUpdateTimeInfo);
}
}
void Merger::eraseEntry(Entry* entry)
{
Database* database = entry->database();
// most simple method to remove an item from DeletedObjects :(
const QList<DeletedObject> deletions = database->deletedObjects();
Group* parentGroup = entry->group();
const bool groupUpdateTimeInfo = parentGroup ? parentGroup->canUpdateTimeinfo() : false;
if (parentGroup) {
parentGroup->setUpdateTimeinfo(false);
}
delete entry;
if (parentGroup) {
parentGroup->setUpdateTimeinfo(groupUpdateTimeInfo);
}
database->setDeletedObjects(deletions);
}
void Merger::eraseGroup(Group* group)
{
Database* database = group->database();
// most simple method to remove an item from DeletedObjects :(
const QList<DeletedObject> deletions = database->deletedObjects();
Group* parentGroup = group->parentGroup();
const bool groupUpdateTimeInfo = parentGroup ? parentGroup->canUpdateTimeinfo() : false;
if (parentGroup) {
parentGroup->setUpdateTimeinfo(false);
}
delete group;
if (parentGroup) {
parentGroup->setUpdateTimeinfo(groupUpdateTimeInfo);
}
database->setDeletedObjects(deletions);
}
Merger::ChangeList Merger::resolveEntryConflict_Duplicate(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry)
{
ChangeList changes;
const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds);
// if one entry is newer, create a clone and add it to the group
if (comparison < 0) {
Entry* clonedEntry = sourceEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory);
moveEntry(clonedEntry, context.m_targetGroup);
markOlderEntry(targetEntry);
changes << tr("Adding backup for older target %1 [%2]")
.arg(targetEntry->title())
.arg(targetEntry->uuidToHex());
} else if (comparison > 0) {
Entry* clonedEntry = sourceEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory);
moveEntry(clonedEntry, context.m_targetGroup);
markOlderEntry(clonedEntry);
changes << tr("Adding backup for older source %1 [%2]")
.arg(sourceEntry->title())
.arg(sourceEntry->uuidToHex());
}
return changes;
}
Merger::ChangeList Merger::resolveEntryConflict_KeepLocal(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry)
{
Q_UNUSED(context);
ChangeList changes;
const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds);
if (comparison < 0) {
// we need to make our older entry "newer" than the new entry - therefore
// we just create a new history entry without any changes - this preserves
// the old state before merging the new state and updates the timestamp
// the merge takes care, that the newer entry is sorted inbetween both entries
// this type of merge changes the database timestamp since reapplying the
// old entry is an active change of the database!
changes << tr("Reapplying older target entry on top of newer source %1 [%2]")
.arg(targetEntry->title())
.arg(targetEntry->uuidToHex());
Entry* agedTargetEntry = targetEntry->clone(Entry::CloneNoFlags);
targetEntry->addHistoryItem(agedTargetEntry);
}
return changes;
}
Merger::ChangeList Merger::resolveEntryConflict_KeepRemote(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry)
{
Q_UNUSED(context);
ChangeList changes;
const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds);
if (comparison > 0) {
// we need to make our older entry "newer" than the new entry - therefore
// we just create a new history entry without any changes - this preserves
// the old state before merging the new state and updates the timestamp
// the merge takes care, that the newer entry is sorted inbetween both entries
// this type of merge changes the database timestamp since reapplying the
// old entry is an active change of the database!
changes << tr("Reapplying older source entry on top of newer target %1 [%2]")
.arg(targetEntry->title())
.arg(targetEntry->uuidToHex());
targetEntry->beginUpdate();
targetEntry->copyDataFrom(sourceEntry);
targetEntry->endUpdate();
// History item is created by endUpdate since we should have changes
}
return changes;
}
Merger::ChangeList Merger::resolveEntryConflict_MergeHistories(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod)
{
Q_UNUSED(context);
ChangeList changes;
const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds);
if (comparison < 0) {
Group* currentGroup = targetEntry->group();
Entry* clonedEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
qDebug("Merge %s/%s with alien on top under %s",
qPrintable(targetEntry->title()),
qPrintable(sourceEntry->title()),
qPrintable(currentGroup->name()));
changes << tr("Synchronizing from newer source %1 [%2]")
.arg(targetEntry->title())
.arg(targetEntry->uuidToHex());
moveEntry(clonedEntry, currentGroup);
mergeHistory(targetEntry, clonedEntry, mergeMethod);
eraseEntry(targetEntry);
} else {
qDebug("Merge %s/%s with local on top/under %s",
qPrintable(targetEntry->title()),
qPrintable(sourceEntry->title()),
qPrintable(targetEntry->group()->name()));
const bool changed = mergeHistory(sourceEntry, targetEntry, mergeMethod);
if (changed) {
changes << tr("Synchronizing from older source %1 [%2]")
.arg(targetEntry->title())
.arg(targetEntry->uuidToHex());
}
}
return changes;
}
Merger::ChangeList Merger::resolveEntryConflict(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry)
{
ChangeList changes;
// We need to cut off the milliseconds since the persistent format only supports times down to seconds
// so when we import data from a remote source, it may represent the (or even some msec newer) data
// which may be discarded due to higher runtime precision
Group::MergeMode mergeMode = m_mode == Group::Default ? context.m_targetGroup->mergeMode() : m_mode;
switch (mergeMode) {
case Group::Duplicate:
changes << resolveEntryConflict_Duplicate(context, sourceEntry, targetEntry);
break;
case Group::KeepLocal:
changes << resolveEntryConflict_KeepLocal(context, sourceEntry, targetEntry);
changes << resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode);
break;
case Group::KeepRemote:
changes << resolveEntryConflict_KeepRemote(context, sourceEntry, targetEntry);
changes << resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode);
break;
case Group::Synchronize:
case Group::KeepNewer:
// nothing special to do since resolveEntryConflictMergeHistories takes care to use the newest entry
changes << resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode);
break;
default:
// do nothing
break;
}
return changes;
}
bool Merger::mergeHistory(const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod)
{
Q_UNUSED(mergeMethod);
const auto targetHistoryItems = targetEntry->historyItems();
const auto sourceHistoryItems = sourceEntry->historyItems();
const int comparison = compare(sourceEntry->timeInfo().lastModificationTime(), targetEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds);
const bool preferLocal = mergeMethod == Group::KeepLocal || comparison < 0;
const bool preferRemote = mergeMethod == Group::KeepRemote || comparison > 0;
QMap<QDateTime, Entry*> merged;
for (Entry* historyItem : targetHistoryItems) {
const QDateTime modificationTime = Clock::serialized(historyItem->timeInfo().lastModificationTime());
if (merged.contains(modificationTime) && !merged[modificationTime]->equals(historyItem, CompareItemIgnoreMilliseconds)) {
::qWarning("Inconsistent history entry of %s[%s] at %s contains conflicting changes - conflict resolution may lose data!",
qPrintable(sourceEntry->title()),
qPrintable(sourceEntry->uuidToHex()),
qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz")));
}
merged[modificationTime] = historyItem->clone(Entry::CloneNoFlags);
}
for (Entry* historyItem : sourceHistoryItems) {
// Items with same modification-time changes will be regarded as same (like KeePass2)
const QDateTime modificationTime = Clock::serialized(historyItem->timeInfo().lastModificationTime());
if (merged.contains(modificationTime) && !merged[modificationTime]->equals(historyItem, CompareItemIgnoreMilliseconds)) {
::qWarning("History entry of %s[%s] at %s contains conflicting changes - conflict resolution may lose data!",
qPrintable(sourceEntry->title()),
qPrintable(sourceEntry->uuidToHex()),
qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz")));
}
if (preferRemote && merged.contains(modificationTime)) {
// forcefully apply the remote history item
delete merged.take(modificationTime);
}
if (!merged.contains(modificationTime)) {
merged[modificationTime] = historyItem->clone(Entry::CloneNoFlags);
}
}
const QDateTime targetModificationTime = Clock::serialized(targetEntry->timeInfo().lastModificationTime());
const QDateTime sourceModificationTime = Clock::serialized(sourceEntry->timeInfo().lastModificationTime());
if (targetModificationTime == sourceModificationTime && !targetEntry->equals(sourceEntry, CompareItemIgnoreMilliseconds | CompareItemIgnoreHistory | CompareItemIgnoreLocation)) {
::qWarning("Entry of %s[%s] contains conflicting changes - conflict resolution may lose data!",
qPrintable(sourceEntry->title()),
qPrintable(sourceEntry->uuidToHex()));
}
if (targetModificationTime < sourceModificationTime) {
if (preferLocal && merged.contains(targetModificationTime)) {
// forcefully apply the local history item
delete merged.take(targetModificationTime);
}
if (!merged.contains(targetModificationTime)) {
merged[targetModificationTime] = targetEntry->clone(Entry::CloneNoFlags);
}
} else if (targetModificationTime > sourceModificationTime) {
if (preferRemote && !merged.contains(sourceModificationTime)) {
// forcefully apply the remote history item
delete merged.take(sourceModificationTime);
}
if (!merged.contains(sourceModificationTime)) {
merged[sourceModificationTime] = sourceEntry->clone(Entry::CloneNoFlags);
}
}
bool changed = false;
const int maxItems = targetEntry->database()->metadata()->historyMaxItems();
const auto updatedHistoryItems = merged.values();
for (int i = 0; i < maxItems; ++i) {
const Entry* oldEntry = targetHistoryItems.value(targetHistoryItems.count() - i);
const Entry* newEntry = updatedHistoryItems.value(updatedHistoryItems.count() - i);
if (!oldEntry && !newEntry) {
continue;
}
if (oldEntry && newEntry && oldEntry->equals(newEntry, CompareItemIgnoreMilliseconds)) {
continue;
}
changed = true;
break;
}
if (!changed) {
qDeleteAll(updatedHistoryItems);
return false;
}
// We need to prevent any modification to the database since every change should be tracked either
// in a clone history item or in the Entry itself
const TimeInfo timeInfo = targetEntry->timeInfo();
const bool blockedSignals = targetEntry->blockSignals(true);
bool updateTimeInfo = targetEntry->canUpdateTimeinfo();
targetEntry->setUpdateTimeinfo(false);
targetEntry->removeHistoryItems(targetHistoryItems);
for (Entry* historyItem : merged.values()) {
Q_ASSERT(!historyItem->parent());
targetEntry->addHistoryItem(historyItem);
}
targetEntry->truncateHistory();
targetEntry->blockSignals(blockedSignals);
targetEntry->setUpdateTimeinfo(updateTimeInfo);
Q_ASSERT(timeInfo == targetEntry->timeInfo());
Q_UNUSED(timeInfo);
return true;
}
Merger::ChangeList Merger::mergeDeletions(const MergeContext& context)
{
ChangeList changes;
Group::MergeMode mergeMode = m_mode == Group::Default ? context.m_targetGroup->mergeMode() : m_mode;
if (mergeMode != Group::Synchronize) {
// no deletions are applied for any other strategy!
return changes;
}
const auto targetDeletions = context.m_targetDb->deletedObjects();
const auto sourceDeletions = context.m_sourceDb->deletedObjects();
QList<DeletedObject> deletions;
QMap<QUuid, DeletedObject> mergedDeletions;
QList<Entry*> entries;
QList<Group*> groups;
for (const auto& object : (targetDeletions + sourceDeletions)) {
if (!mergedDeletions.contains(object.uuid)) {
mergedDeletions[object.uuid] = object;
auto* entry = context.m_targetRootGroup->findEntryByUuid(object.uuid);
if (entry) {
entries << entry;
continue;
}
auto* group = context.m_targetRootGroup->findGroupByUuid(object.uuid);
if (group) {
groups << group;
continue;
}
deletions << object;
continue;
}
if (mergedDeletions[object.uuid].deletionTime > object.deletionTime) {
mergedDeletions[object.uuid] = object;
}
}
while (!entries.isEmpty()) {
auto* entry = entries.takeFirst();
const auto& object = mergedDeletions[entry->uuid()];
if (entry->timeInfo().lastModificationTime() > object.deletionTime) {
// keep deleted entry since it was changed after deletion date
continue;
}
deletions << object;
if (entry->group()) {
changes << tr("Deleting child %1 [%2]").arg(entry->title()).arg(entry->uuidToHex());
} else {
changes << tr("Deleting orphan %1 [%2]").arg(entry->title()).arg(entry->uuidToHex());
}
// Entry is inserted into deletedObjects after deletions are processed
eraseEntry(entry);
}
while (!groups.isEmpty()) {
auto* group = groups.takeFirst();
if (!(group->children().toSet() & groups.toSet()).isEmpty()) {
// we need to finish all children before we are able to determine if the group can be removed
groups << group;
continue;
}
const auto& object = mergedDeletions[group->uuid()];
if (group->timeInfo().lastModificationTime() > object.deletionTime) {
// keep deleted group since it was changed after deletion date
continue;
}
if (!group->entriesRecursive(false).isEmpty() || !group->groupsRecursive(false).isEmpty()) {
// keep deleted group since it contains undeleted content
continue;
}
deletions << object;
if (group->parentGroup()) {
changes << tr("Deleting child %1 [%2]").arg(group->name()).arg(group->uuidToHex());
} else {
changes << tr("Deleting orphan %1 [%2]").arg(group->name()).arg(group->uuidToHex());
}
eraseGroup(group);
}
// Put every deletion to the earliest date of deletion
if (deletions != context.m_targetDb->deletedObjects()) {
changes << tr("Changed deleted objects");
}
context.m_targetDb->setDeletedObjects(deletions);
return changes;
}
Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
{
// TODO HNH: missing handling of recycle bin, names, templates for groups and entries,
// public data (entries of newer dict override keys of older dict - ignoring
// their own age - it is enough if one entry of the whole dict is newer) => possible lost update
// TODO HNH: CustomData is merged with entries of the new customData overwrite entries
// of the older CustomData - the dict with the newest entry is considered
// newer regardless of the age of the other entries => possible lost update
ChangeList changes;
auto* sourceMetadata = context.m_sourceDb->metadata();
auto* targetMetadata = context.m_targetDb->metadata();
for (QUuid customIconId : sourceMetadata->customIcons().keys()) {
QImage customIcon = sourceMetadata->customIcon(customIconId);
if (!targetMetadata->containsCustomIcon(customIconId)) {
targetMetadata->addCustomIcon(customIconId, customIcon);
changes << tr("Adding missing icon %1").arg(QString::fromLatin1(customIconId.toRfc4122().toHex()));
}
}
return changes;
}

72
src/core/Merger.h Normal file
View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2018 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_MERGER_H
#define KEEPASSXC_MERGER_H
#include "core/Group.h"
#include <QObject>
#include <QPointer>
class Database;
class Entry;
class Merger : public QObject
{
Q_OBJECT
public:
Merger(const Database* sourceDb, Database* targetDb);
Merger(const Group* sourceGroup, Group* targetGroup);
void setForcedMergeMode(Group::MergeMode mode);
void resetForcedMergeMode();
bool merge();
private:
typedef QString Change;
typedef QStringList ChangeList;
struct MergeContext
{
QPointer<const Database> m_sourceDb;
QPointer<Database> m_targetDb;
QPointer<const Group> m_sourceRootGroup;
QPointer<Group> m_targetRootGroup;
QPointer<const Group> m_sourceGroup;
QPointer<Group> m_targetGroup;
};
ChangeList mergeGroup(const MergeContext& context);
ChangeList mergeDeletions(const MergeContext& context);
ChangeList mergeMetadata(const MergeContext& context);
bool markOlderEntry(Entry* entry);
bool mergeHistory(const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod);
void moveEntry(Entry* entry, Group* targetGroup);
void moveGroup(Group* group, Group* targetGroup);
void eraseEntry(Entry* entry); // remove an entry without a trace in the deletedObjects - needed for elemination cloned entries
void eraseGroup(Group* group); // remove an entry without a trace in the deletedObjects - needed for elemination cloned entries
ChangeList resolveEntryConflict(const MergeContext& context, const Entry* existingEntry, Entry* otherEntry);
ChangeList resolveGroupConflict(const MergeContext& context, const Group* existingGroup, Group* otherGroup);
Merger::ChangeList resolveEntryConflict_Duplicate(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry);
Merger::ChangeList resolveEntryConflict_KeepLocal(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry);
Merger::ChangeList resolveEntryConflict_KeepRemote(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry);
Merger::ChangeList resolveEntryConflict_MergeHistories(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod);
private:
MergeContext m_context;
Group::MergeMode m_mode;
};
#endif // KEEPASSXC_MERGER_H

View File

@@ -18,6 +18,7 @@
#include "Metadata.h"
#include <QtCore/QCryptographicHash>
#include "core/Clock.h"
#include "core/Entry.h"
#include "core/Group.h"
#include "core/Tools.h"
@@ -43,7 +44,7 @@ Metadata::Metadata(QObject* parent)
m_data.protectUrl = false;
m_data.protectNotes = false;
QDateTime now = QDateTime::currentDateTimeUtc();
QDateTime now = Clock::currentDateTimeUtc();
m_data.nameChanged = now;
m_data.descriptionChanged = now;
m_data.defaultUserNameChanged = now;
@@ -71,7 +72,7 @@ template <class P, class V> bool Metadata::set(P& property, const V& value, QDat
if (property != value) {
property = value;
if (m_updateDatetime) {
dateTime = QDateTime::currentDateTimeUtc();
dateTime = Clock::currentDateTimeUtc();
}
emit modified();
return true;

View File

@@ -17,11 +17,13 @@
#include "TimeInfo.h"
#include "core/Clock.h"
TimeInfo::TimeInfo()
: m_expires(false)
, m_usageCount(0)
{
QDateTime now = QDateTime::currentDateTimeUtc();
QDateTime now = Clock::currentDateTimeUtc();
m_lastModificationTime = now;
m_creationTime = now;
m_lastAccessTime = now;
@@ -103,3 +105,38 @@ void TimeInfo::setLocationChanged(const QDateTime& dateTime)
Q_ASSERT(dateTime.timeSpec() == Qt::UTC);
m_locationChanged = dateTime;
}
bool TimeInfo::operator==(const TimeInfo& other) const
{
return equals(other, CompareItemDefault);
}
bool TimeInfo::operator!=(const TimeInfo& other) const
{
return !this->operator==(other);
}
bool TimeInfo::equals(const TimeInfo& other, CompareItemOptions options) const
{
if (::compare(m_lastModificationTime, other.m_lastModificationTime, options) != 0) {
return false;
}
if (::compare(m_creationTime, other.m_creationTime, options) != 0) {
return false;
}
if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_lastAccessTime, other.m_lastAccessTime, options)
!= 0) {
return false;
}
if (::compare(m_expires, m_expiryTime, other.m_expires, other.expiryTime(), options) != 0) {
return false;
}
if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_usageCount, other.m_usageCount, options) != 0) {
return false;
}
if (::compare(!options.testFlag(CompareItemIgnoreLocation), m_locationChanged, other.m_locationChanged, options)
!= 0) {
return false;
}
return true;
}

View File

@@ -19,6 +19,9 @@
#define KEEPASSX_TIMEINFO_H
#include <QDateTime>
#include <QFlag>
#include "core/Compare.h"
class TimeInfo
{
@@ -33,6 +36,10 @@ public:
int usageCount() const;
QDateTime locationChanged() const;
bool operator==(const TimeInfo& other) const;
bool operator!=(const TimeInfo& other) const;
bool equals(const TimeInfo& other, CompareItemOptions options = CompareItemDefault) const;
void setLastModificationTime(const QDateTime& dateTime);
void setCreationTime(const QDateTime& dateTime);
void setLastAccessTime(const QDateTime& dateTime);

View File

@@ -21,7 +21,6 @@
#include "core/Global.h"
#include <QDateTime>
#include <QObject>
#include <QString>