From 054d7ceed2dfd6e6a4884405f6269c0f656273af Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Wed, 10 Nov 2021 23:11:03 +0100 Subject: [PATCH] Implement KDBX 4.1 item-level modification date We keep the old merging behaviour for now, since deleting a CustomData entry does not create DeletedObject. --- src/core/CustomData.cpp | 78 ++++++++++++++++++++++++++---------- src/core/CustomData.h | 29 +++++++++++--- src/core/Merger.cpp | 7 ++-- src/format/KdbxXmlReader.cpp | 5 ++- src/format/KdbxXmlWriter.cpp | 15 ++++--- src/format/KdbxXmlWriter.h | 6 ++- 6 files changed, 101 insertions(+), 39 deletions(-) diff --git a/src/core/CustomData.cpp b/src/core/CustomData.cpp index 1fa2cb107b..fd582ea61e 100644 --- a/src/core/CustomData.cpp +++ b/src/core/CustomData.cpp @@ -42,6 +42,11 @@ bool CustomData::hasKey(const QString& key) const } QString CustomData::value(const QString& key) const +{ + return m_data.value(key).value; +} + +CustomData::CustomDataItem CustomData::item(const QString& key) const { return m_data.value(key); } @@ -53,20 +58,28 @@ bool CustomData::contains(const QString& key) const bool CustomData::containsValue(const QString& value) const { - return asConst(m_data).values().contains(value); + for (auto i = m_data.constBegin(); i != m_data.constEnd(); ++i) { + if (i.value().value == value) { + return true; + } + } + return false; } -void CustomData::set(const QString& key, const QString& value) +void CustomData::set(const QString& key, const QString& value, QDateTime lastModified) { bool addAttribute = !m_data.contains(key); - bool changeValue = !addAttribute && (m_data.value(key) != value); + bool changeValue = !addAttribute && (m_data.value(key).value != value); if (addAttribute) { emit aboutToBeAdded(key); } + if (!lastModified.isValid()) { + Clock::currentDateTimeUtc(); + } if (addAttribute || changeValue) { - m_data.insert(key, value); + m_data.insert(key, CustomDataItem{value, lastModified}); updateLastModified(); emitModified(); } @@ -98,11 +111,12 @@ void CustomData::rename(const QString& oldKey, const QString& newKey) return; } - QString data = value(oldKey); + CustomDataItem data = m_data.value(oldKey); emit aboutToRename(oldKey, newKey); m_data.remove(oldKey); + data.lastModified = Clock::currentDateTimeUtc(); m_data.insert(newKey, data); updateLastModified(); @@ -119,21 +133,52 @@ void CustomData::copyDataFrom(const CustomData* other) emit aboutToBeReset(); m_data = other->m_data; + auto i = m_data.begin(); + while (i != m_data.end()) { + i.value().lastModified = Clock::currentDateTimeUtc(); + ++i; + } updateLastModified(); emit reset(); emitModified(); } -QDateTime CustomData::getLastModified() const +QDateTime CustomData::lastModified() const { if (m_data.contains(LastModified)) { - return Clock::parse(m_data.value(LastModified)); + return Clock::parse(m_data.value(LastModified).value); + } + + // Try to find the latest modification time in items as a fallback + QDateTime modified; + for (auto i = m_data.constBegin(); i != m_data.constEnd(); ++i) { + if (i->lastModified.isValid() && (!modified.isValid() || i->lastModified > modified)) { + modified = i->lastModified; + } + } + return modified; +} + +QDateTime CustomData::lastModified(const QString& key) const +{ + return m_data.value(key).lastModified; +} + +void CustomData::updateLastModified(QDateTime lastModified) +{ + if (m_data.isEmpty() || (m_data.size() == 1 && m_data.contains(LastModified))) { + m_data.remove(LastModified); + return; } - return {}; + + if (!lastModified.isValid()) { + lastModified = Clock::currentDateTimeUtc(); + } + m_data.insert(LastModified, {lastModified.toString(), lastModified}); } -bool CustomData::isProtectedCustomData(const QString& key) const +bool CustomData::isProtected(const QString& key) const { return key.startsWith(CustomData::BrowserKeyPrefix) || key.startsWith(CustomData::Created); } @@ -172,20 +217,11 @@ int CustomData::dataSize() const { int size = 0; - QHashIterator i(m_data); + QHashIterator i(m_data); while (i.hasNext()) { i.next(); - size += i.key().toUtf8().size() + i.value().toUtf8().size(); + size += i.key().toUtf8().size() + i.value().value.toUtf8().size() + + i.value().lastModified.toString(Qt::ISODate).size(); } return size; } - -void CustomData::updateLastModified() -{ - if (m_data.isEmpty() || (m_data.size() == 1 && m_data.contains(LastModified))) { - m_data.remove(LastModified); - return; - } - - m_data.insert(LastModified, Clock::currentDateTimeUtc().toString()); -} diff --git a/src/core/CustomData.h b/src/core/CustomData.h index 37d8ef55ec..2f01340336 100644 --- a/src/core/CustomData.h +++ b/src/core/CustomData.h @@ -18,6 +18,7 @@ #ifndef KEEPASSXC_CUSTOMDATA_H #define KEEPASSXC_CUSTOMDATA_H +#include #include #include @@ -28,13 +29,28 @@ class CustomData : public ModifiableObject Q_OBJECT public: + struct CustomDataItem + { + QString value; + QDateTime lastModified; + + bool operator==(const CustomDataItem& other) const + { + return value == other.value && lastModified == other.lastModified; + } + }; + explicit CustomData(QObject* parent = nullptr); QList keys() const; bool hasKey(const QString& key) const; QString value(const QString& key) const; + CustomDataItem item(const QString& key) const; bool contains(const QString& key) const; bool containsValue(const QString& value) const; - void set(const QString& key, const QString& value); + QDateTime lastModified() const; + QDateTime lastModified(const QString& key) const; + bool isProtected(const QString& key) const; + void set(const QString& key, const QString& value, QDateTime lastModified = {}); void remove(const QString& key); void rename(const QString& oldKey, const QString& newKey); void clear(); @@ -42,16 +58,17 @@ class CustomData : public ModifiableObject int size() const; int dataSize() const; void copyDataFrom(const CustomData* other); - QDateTime getLastModified() const; - bool isProtectedCustomData(const QString& key) const; bool operator==(const CustomData& other) const; bool operator!=(const CustomData& other) const; + // Pre-defined keys static const QString LastModified; static const QString Created; static const QString BrowserKeyPrefix; static const QString BrowserLegacyKeyPrefix; - static const QString ExcludeFromReportsLegacy; // Pre-KDBX 4.1 + + // Pre-KDBX 4.1 + static const QString ExcludeFromReportsLegacy; signals: void aboutToBeAdded(const QString& key); @@ -64,10 +81,10 @@ class CustomData : public ModifiableObject void reset(); private slots: - void updateLastModified(); + void updateLastModified(QDateTime lastModified = {}); private: - QHash m_data; + QHash m_data; }; #endif // KEEPASSXC_CUSTOMDATA_H diff --git a/src/core/Merger.cpp b/src/core/Merger.cpp index 556836b991..0280ca7df7 100644 --- a/src/core/Merger.cpp +++ b/src/core/Merger.cpp @@ -618,8 +618,8 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context) } // Merge Custom Data if source is newer - const auto targetCustomDataModificationTime = targetMetadata->customData()->getLastModified(); - const auto sourceCustomDataModificationTime = sourceMetadata->customData()->getLastModified(); + const auto targetCustomDataModificationTime = targetMetadata->customData()->lastModified(); + const auto sourceCustomDataModificationTime = sourceMetadata->customData()->lastModified(); if (!targetMetadata->customData()->contains(CustomData::LastModified) || (targetCustomDataModificationTime.isValid() && sourceCustomDataModificationTime.isValid() && targetCustomDataModificationTime < sourceCustomDataModificationTime)) { @@ -629,8 +629,7 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context) // Check missing keys from source. Remove those from target for (const auto& key : targetCustomDataKeys) { // Do not remove protected custom data - if (!sourceMetadata->customData()->contains(key) - && !sourceMetadata->customData()->isProtectedCustomData(key)) { + if (!sourceMetadata->customData()->contains(key) && !sourceMetadata->customData()->isProtected(key)) { auto value = targetMetadata->customData()->value(key); targetMetadata->customData()->remove(key); changes << tr("Removed custom data %1 [%2]").arg(key, value); diff --git a/src/format/KdbxXmlReader.cpp b/src/format/KdbxXmlReader.cpp index 0b2aac83f4..f2aed398c6 100644 --- a/src/format/KdbxXmlReader.cpp +++ b/src/format/KdbxXmlReader.cpp @@ -421,6 +421,7 @@ void KdbxXmlReader::parseCustomDataItem(CustomData* customData) QString key; QString value; + QDateTime lastModified; bool keySet = false; bool valueSet = false; @@ -431,13 +432,15 @@ void KdbxXmlReader::parseCustomDataItem(CustomData* customData) } else if (m_xml.name() == "Value") { value = readString(); valueSet = true; + } else if (m_xml.name() == "LastModificationTime") { + lastModified = readDateTime(); } else { skipCurrentElement(); } } if (keySet && valueSet) { - customData->set(key, value); + customData->set(key, value, lastModified); return; } diff --git a/src/format/KdbxXmlWriter.cpp b/src/format/KdbxXmlWriter.cpp index d870f346be..68e0e2b723 100644 --- a/src/format/KdbxXmlWriter.cpp +++ b/src/format/KdbxXmlWriter.cpp @@ -131,7 +131,7 @@ void KdbxXmlWriter::writeMetadata() if (m_kdbxVersion < KeePass2::FILE_VERSION_4) { writeBinaries(); } - writeCustomData(m_meta->customData()); + writeCustomData(m_meta->customData(), true); m_xml.writeEndElement(); } @@ -228,7 +228,7 @@ void KdbxXmlWriter::writeBinaries() m_xml.writeEndElement(); } -void KdbxXmlWriter::writeCustomData(const CustomData* customData) +void KdbxXmlWriter::writeCustomData(const CustomData* customData, bool writeItemLastModified) { if (customData->isEmpty()) { return; @@ -237,18 +237,23 @@ void KdbxXmlWriter::writeCustomData(const CustomData* customData) const QList keyList = customData->keys(); for (const QString& key : keyList) { - writeCustomDataItem(key, customData->value(key)); + writeCustomDataItem(key, customData->item(key), writeItemLastModified); } m_xml.writeEndElement(); } -void KdbxXmlWriter::writeCustomDataItem(const QString& key, const QString& value) +void KdbxXmlWriter::writeCustomDataItem(const QString& key, + const CustomData::CustomDataItem& item, + bool writeLastModified) { m_xml.writeStartElement("Item"); writeString("Key", key); - writeString("Value", value); + writeString("Value", item.value); + if (writeLastModified && m_kdbxVersion >= KeePass2::FILE_VERSION_4 && item.lastModified.isValid()) { + writeDateTime("LastModificationTime", item.lastModified); + } m_xml.writeEndElement(); } diff --git a/src/format/KdbxXmlWriter.h b/src/format/KdbxXmlWriter.h index af131a63a1..3bdad34b4b 100644 --- a/src/format/KdbxXmlWriter.h +++ b/src/format/KdbxXmlWriter.h @@ -20,6 +20,7 @@ #include +#include "core/CustomData.h" #include "core/Group.h" #include "core/Metadata.h" @@ -48,8 +49,9 @@ class KdbxXmlWriter void writeCustomIcons(); void writeIcon(const QUuid& uuid, const Metadata::CustomIconData& iconData); void writeBinaries(); - void writeCustomData(const CustomData* customData); - void writeCustomDataItem(const QString& key, const QString& value); + void writeCustomData(const CustomData* customData, bool writeItemLastModified = false); + void + writeCustomDataItem(const QString& key, const CustomData::CustomDataItem& item, bool writeLastModified = false); void writeRoot(); void writeGroup(const Group* group); void writeTimes(const TimeInfo& ti);