diff options
author | Jonathan White <support@dmapps.us> | 2018-09-30 15:45:06 +0300 |
---|---|---|
committer | Jonathan White <droidmonkey@users.noreply.github.com> | 2018-09-30 16:36:39 +0300 |
commit | c1e9f45df9f21b7697241037643770a2862bb7ef (patch) | |
tree | bbb7a840c0199613203d2de90ece7dd47e87038e /src/core | |
parent | b40e5686dccfb9f60abc186120e9db17edfa81c0 (diff) |
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)
Diffstat (limited to 'src/core')
-rw-r--r-- | src/core/AutoTypeAssociations.cpp | 18 | ||||
-rw-r--r-- | src/core/AutoTypeAssociations.h | 3 | ||||
-rw-r--r-- | src/core/Clock.cpp | 109 | ||||
-rw-r--r-- | src/core/Clock.h | 58 | ||||
-rw-r--r-- | src/core/Compare.cpp | 38 | ||||
-rw-r--r-- | src/core/Compare.h | 90 | ||||
-rw-r--r-- | src/core/Database.cpp | 62 | ||||
-rw-r--r-- | src/core/Database.h | 10 | ||||
-rw-r--r-- | src/core/Entry.cpp | 167 | ||||
-rw-r--r-- | src/core/Entry.h | 15 | ||||
-rw-r--r-- | src/core/Group.cpp | 240 | ||||
-rw-r--r-- | src/core/Group.h | 32 | ||||
-rw-r--r-- | src/core/Merger.cpp | 613 | ||||
-rw-r--r-- | src/core/Merger.h | 72 | ||||
-rw-r--r-- | src/core/Metadata.cpp | 5 | ||||
-rw-r--r-- | src/core/TimeInfo.cpp | 39 | ||||
-rw-r--r-- | src/core/TimeInfo.h | 7 | ||||
-rw-r--r-- | src/core/Tools.h | 1 |
18 files changed, 1379 insertions, 200 deletions
diff --git a/src/core/AutoTypeAssociations.cpp b/src/core/AutoTypeAssociations.cpp index 730e38ca1..a9ecc0db1 100644 --- a/src/core/AutoTypeAssociations.cpp +++ b/src/core/AutoTypeAssociations.cpp @@ -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); +} diff --git a/src/core/AutoTypeAssociations.h b/src/core/AutoTypeAssociations.h index 31e58cda0..17d5c3bcd 100644 --- a/src/core/AutoTypeAssociations.h +++ b/src/core/AutoTypeAssociations.h @@ -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; diff --git a/src/core/Clock.cpp b/src/core/Clock.cpp new file mode 100644 index 000000000..02c2ae1bc --- /dev/null +++ b/src/core/Clock.cpp @@ -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; +} diff --git a/src/core/Clock.h b/src/core/Clock.h new file mode 100644 index 000000000..8f81b0961 --- /dev/null +++ b/src/core/Clock.h @@ -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 diff --git a/src/core/Compare.cpp b/src/core/Compare.cpp new file mode 100644 index 000000000..12e5029b7 --- /dev/null +++ b/src/core/Compare.cpp @@ -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; +} diff --git a/src/core/Compare.h b/src/core/Compare.h new file mode 100644 index 000000000..5124caf6e --- /dev/null +++ b/src/core/Compare.h @@ -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 diff --git a/src/core/Database.cpp b/src/core/Database.cpp index bc0a1b302..5b7a3c07d 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -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; diff --git a/src/core/Database.h b/src/core/Database.h index 912f95073..a5ae3effa 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -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); /** diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index bc394b227..929447f9c 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -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; +} diff --git a/src/core/Entry.h b/src/core/Entry.h index aa2426c5e..05ed30bc0 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -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; diff --git a/src/core/Group.cpp b/src/core/Group.cpp index f5338533b..4ff6e5b68 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -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; +} diff --git a/src/core/Group.h b/src/core/Group.h index 35619d938..89343e829 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -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(); diff --git a/src/core/Merger.cpp b/src/core/Merger.cpp new file mode 100644 index 000000000..9b87a6ac3 --- /dev/null +++ b/src/core/Merger.cpp @@ -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; +} diff --git a/src/core/Merger.h b/src/core/Merger.h new file mode 100644 index 000000000..1f16fe026 --- /dev/null +++ b/src/core/Merger.h @@ -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 diff --git a/src/core/Metadata.cpp b/src/core/Metadata.cpp index 54f99de22..ac9d38fda 100644 --- a/src/core/Metadata.cpp +++ b/src/core/Metadata.cpp @@ -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; diff --git a/src/core/TimeInfo.cpp b/src/core/TimeInfo.cpp index 85c53a567..c774a7c81 100644 --- a/src/core/TimeInfo.cpp +++ b/src/core/TimeInfo.cpp @@ -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; +} diff --git a/src/core/TimeInfo.h b/src/core/TimeInfo.h index 455c002cd..de8a37593 100644 --- a/src/core/TimeInfo.h +++ b/src/core/TimeInfo.h @@ -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); diff --git a/src/core/Tools.h b/src/core/Tools.h index 9fd497995..4f75b750b 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -21,7 +21,6 @@ #include "core/Global.h" -#include <QDateTime> #include <QObject> #include <QString> |