From 4fffb2288cb8bf9d87fa30679ebaafd40936711b Mon Sep 17 00:00:00 2001 From: osx user Date: Sun, 1 Aug 2021 00:01:45 +0300 Subject: [PATCH] Show what changed between entry history items * Also show what is changed on the current state * Closes #2621 --- share/translations/keepassxc_en.ts | 98 ++++++++++++++++++ src/core/Tools.cpp | 32 ++++++ src/core/Tools.h | 2 + src/gui/entry/EditEntryWidget.cpp | 18 ++-- src/gui/entry/EntryHistoryModel.cpp | 154 +++++++++++++++++++++++----- src/gui/entry/EntryHistoryModel.h | 6 +- 6 files changed, 278 insertions(+), 32 deletions(-) diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 3202960289..c69ebe60e2 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -3614,6 +3614,62 @@ Would you like to overwrite the existing attachment? URL URL + + Age + + + + Difference + + + + Size + Size + + + Password + Password + + + Notes + Notes + + + Custom Attributes + + + + Icon + Icon + + + Color + + + + Expiration + Expiration + + + TOTP + TOTP + + + Custom Data + + + + Attachments + Attachments + + + Auto-Type + Auto-Type + + + Current (%1) + + EntryModel @@ -7633,6 +7689,48 @@ Please consider generating a new key file. KeeShare + + over %1 year(s) + + + + + + + about %1 month(s) + + + + + + + %1 week(s) + + + + + + + %1 day(s) + + + + + + + %1 hour(s) + + + + + + + %1 minute(s) + + + + + QtIOCompressor diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 59ddf9e303..0cbe2b4fb4 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #ifdef Q_OS_WIN #include // for Sleep() @@ -133,6 +134,37 @@ namespace Tools return QString("%1 %2").arg(QLocale().toString(size, 'f', precision), units.at(i)); } + QString humanReadableTimeDifference(qint64 seconds) + { + constexpr double secondsInHour = 3600; + constexpr double secondsInDay = secondsInHour * 24; + constexpr double secondsInWeek = secondsInDay * 7; + constexpr double secondsInMonth = secondsInDay * 30; // Approximation + constexpr double secondsInYear = secondsInDay * 365; + + seconds = abs(seconds); + + if (seconds >= secondsInYear) { + auto years = std::floor(seconds / secondsInYear); + return QObject::tr("over %1 year(s)", nullptr, years).arg(years); + } else if (seconds >= secondsInMonth) { + auto months = std::round(seconds / secondsInMonth); + return QObject::tr("about %1 month(s)", nullptr, months).arg(months); + } else if (seconds >= secondsInWeek) { + auto weeks = std::round(seconds / secondsInWeek); + return QObject::tr("%1 week(s)", nullptr, weeks).arg(weeks); + } else if (seconds >= secondsInDay) { + auto days = std::floor(seconds / secondsInDay); + return QObject::tr("%1 day(s)", nullptr, days).arg(days); + } else if (seconds >= secondsInHour) { + auto hours = std::floor(seconds / secondsInHour); + return QObject::tr("%1 hour(s)", nullptr, hours).arg(hours); + } + + auto minutes = std::floor(seconds / 60); + return QObject::tr("%1 minute(s)", nullptr, minutes).arg(minutes); + } + bool readFromDevice(QIODevice* device, QByteArray& data, int size) { QByteArray buffer; diff --git a/src/core/Tools.h b/src/core/Tools.h index 2c22e74273..c605143b7b 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -21,6 +21,7 @@ #include "core/Global.h" +#include #include class QIODevice; @@ -30,6 +31,7 @@ namespace Tools { QString debugInfo(); QString humanReadableFileSize(qint64 bytes, quint32 precision = 2); + QString humanReadableTimeDifference(qint64 seconds); bool readFromDevice(QIODevice* device, QByteArray& data, int size = 16384); bool readAllFromDevice(QIODevice* device, QByteArray& data); bool isHex(const QByteArray& ba); diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index f9527966bf..78d8b48cd8 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -504,7 +504,9 @@ void EditEntryWidget::emitHistoryEntryActivated(const QModelIndex& index) Q_ASSERT(!m_history); Entry* entry = m_historyModel->entryFromIndex(index); - emit historyEntryActivated(entry); + if (entry) { + emit historyEntryActivated(entry); + } } void EditEntryWidget::histEntryActivated(const QModelIndex& index) @@ -521,7 +523,7 @@ void EditEntryWidget::updateHistoryButtons(const QModelIndex& current, const QMo { Q_UNUSED(previous); - if (current.isValid()) { + if (m_historyModel->entryFromIndex(current)) { m_historyUi->showButton->setEnabled(true); m_historyUi->restoreButton->setEnabled(true); m_historyUi->deleteButton->setEnabled(true); @@ -1025,7 +1027,7 @@ void EditEntryWidget::setForms(Entry* entry, bool restore) m_editWidgetProperties->setFields(entry->timeInfo(), entry->uuid()); if (!m_history && !restore) { - m_historyModel->setEntries(entry->historyItems()); + m_historyModel->setEntries(entry->historyItems(), entry); m_historyUi->historyView->sortByColumn(0, Qt::DescendingOrder); } if (m_historyModel->rowCount() > 0) { @@ -1129,7 +1131,8 @@ bool EditEntryWidget::commitEntry() m_entry->endUpdate(); } - m_historyModel->setEntries(m_entry->historyItems()); + m_historyModel->setEntries(m_entry->historyItems(), m_entry); + m_advancedUi->attachmentsWidget->linkAttachments(m_entry->attachments()); showMessage(tr("Entry updated successfully."), MessageWidget::Positive); setModified(false); @@ -1538,8 +1541,9 @@ void EditEntryWidget::showHistoryEntry() void EditEntryWidget::restoreHistoryEntry() { QModelIndex index = m_sortModel->mapToSource(m_historyUi->historyView->currentIndex()); - if (index.isValid()) { - setForms(m_historyModel->entryFromIndex(index), true); + auto entry = m_historyModel->entryFromIndex(index); + if (entry) { + setForms(entry, true); setModified(true); } } @@ -1547,7 +1551,7 @@ void EditEntryWidget::restoreHistoryEntry() void EditEntryWidget::deleteHistoryEntry() { QModelIndex index = m_sortModel->mapToSource(m_historyUi->historyView->currentIndex()); - if (index.isValid()) { + if (m_historyModel->entryFromIndex(index)) { m_historyModel->deleteIndex(index); if (m_historyModel->rowCount() > 0) { m_historyUi->deleteAllButton->setEnabled(true); diff --git a/src/gui/entry/EntryHistoryModel.cpp b/src/gui/entry/EntryHistoryModel.cpp index 2506e06d78..beadce6f09 100644 --- a/src/gui/entry/EntryHistoryModel.cpp +++ b/src/gui/entry/EntryHistoryModel.cpp @@ -17,8 +17,12 @@ #include "EntryHistoryModel.h" +#include "core/Clock.h" #include "core/Entry.h" #include "core/Global.h" +#include "core/Tools.h" + +#include EntryHistoryModel::EntryHistoryModel(QObject* parent) : QAbstractTableModel(parent) @@ -27,8 +31,11 @@ EntryHistoryModel::EntryHistoryModel(QObject* parent) Entry* EntryHistoryModel::entryFromIndex(const QModelIndex& index) const { - Q_ASSERT(index.isValid() && index.row() < m_historyEntries.size()); - return m_historyEntries.at(index.row()); + if (!index.isValid() || index.row() >= m_historyEntries.size()) { + return nullptr; + } + auto entry = m_historyEntries.at(index.row()); + return entry == m_parentEntry ? nullptr : entry; } int EntryHistoryModel::columnCount(const QModelIndex& parent) const @@ -48,31 +55,50 @@ int EntryHistoryModel::rowCount(const QModelIndex& parent) const QVariant EntryHistoryModel::data(const QModelIndex& index, int role) const { - if (!index.isValid()) { - return QVariant(); + if (index.row() >= m_historyEntries.size()) { + return {}; } + const auto entry = m_historyEntries[index.row()]; if (role == Qt::DisplayRole || role == Qt::UserRole) { - Entry* entry = entryFromIndex(index); - const TimeInfo& timeInfo = entry->timeInfo(); - QDateTime lastModificationLocalTime = timeInfo.lastModificationTime().toLocalTime(); + QDateTime lastModified = entry->timeInfo().lastModificationTime().toLocalTime(); + QDateTime now = Clock::currentDateTime(); + switch (index.column()) { case 0: if (role == Qt::DisplayRole) { - return lastModificationLocalTime.toString(Qt::SystemLocaleShortDate); + return lastModified.toString(Qt::SystemLocaleShortDate); } else { - return lastModificationLocalTime; + return lastModified; } - case 1: - return entry->title(); + case 1: { + const auto seconds = lastModified.secsTo(now); + if (role == Qt::DisplayRole) { + if (entry == m_parentEntry) { + return tr("Current (%1)").arg(Tools::humanReadableTimeDifference(seconds)); + } + return Tools::humanReadableTimeDifference(seconds); + } + return seconds; + } case 2: - return entry->username(); + if (index.row() < m_historyModifications.size()) { + return m_historyModifications[index.row()]; + } + return {}; case 3: - return entry->url(); + if (role == Qt::DisplayRole) { + return Tools::humanReadableFileSize(entry->size(), 0); + } + return entry->size(); } + } else if (role == Qt::FontRole && entry == m_parentEntry) { + QFont font; + font.setBold(true); + return font; } - return QVariant(); + return {}; } QVariant EntryHistoryModel::headerData(int section, Qt::Orientation orientation, int role) const @@ -82,24 +108,29 @@ QVariant EntryHistoryModel::headerData(int section, Qt::Orientation orientation, case 0: return tr("Last modified"); case 1: - return tr("Title"); + return tr("Age"); case 2: - return tr("Username"); + return tr("Difference"); case 3: - return tr("URL"); + return tr("Size"); } } - return QVariant(); + return {}; } -void EntryHistoryModel::setEntries(const QList& entries) +void EntryHistoryModel::setEntries(const QList& entries, Entry* parentEntry) { beginResetModel(); - + m_parentEntry = parentEntry; m_historyEntries = entries; + m_historyEntries << parentEntry; + // Sort the entries by last modified (newest -> oldest) so we can calculate the differences + std::sort(m_historyEntries.begin(), m_historyEntries.end(), [](const Entry* lhs, const Entry* rhs) { + return lhs->timeInfo().lastModificationTime() > rhs->timeInfo().lastModificationTime(); + }); m_deletedHistoryEntries.clear(); - + calculateHistoryModifications(); endResetModel(); } @@ -125,8 +156,8 @@ QList EntryHistoryModel::deletedEntries() void EntryHistoryModel::deleteIndex(QModelIndex index) { - if (index.isValid()) { - Entry* entry = entryFromIndex(index); + auto entry = entryFromIndex(index); + if (entry) { beginRemoveRows(QModelIndex(), m_historyEntries.indexOf(entry), m_historyEntries.indexOf(entry)); m_historyEntries.removeAll(entry); m_deletedHistoryEntries << entry; @@ -141,8 +172,83 @@ void EntryHistoryModel::deleteAll() beginRemoveRows(QModelIndex(), 0, m_historyEntries.size() - 1); for (Entry* entry : asConst(m_historyEntries)) { - m_deletedHistoryEntries << entry; + if (entry != m_parentEntry) { + m_deletedHistoryEntries << entry; + } } m_historyEntries.clear(); endRemoveRows(); } + +void EntryHistoryModel::calculateHistoryModifications() +{ + m_historyModifications.clear(); + + Entry* compare = nullptr; + for (const auto curr : m_historyEntries) { + if (!compare) { + compare = curr; + continue; + } + + QStringList modifiedFields; + + if (*curr->attributes() != *compare->attributes()) { + bool foundAttribute = false; + + if (curr->title() != compare->title()) { + modifiedFields << tr("Title"); + foundAttribute = true; + } + if (curr->username() != compare->username()) { + modifiedFields << tr("Username"); + foundAttribute = true; + } + if (curr->password() != compare->password()) { + modifiedFields << tr("Password"); + foundAttribute = true; + } + if (curr->url() != compare->url()) { + modifiedFields << tr("URL"); + foundAttribute = true; + } + if (curr->notes() != compare->notes()) { + modifiedFields << tr("Notes"); + foundAttribute = true; + } + + if (!foundAttribute) { + modifiedFields << tr("Custom Attributes"); + } + } + if (curr->iconNumber() != compare->iconNumber() || curr->iconUuid() != compare->iconUuid()) { + modifiedFields << tr("Icon"); + } + if (curr->foregroundColor() != compare->foregroundColor() + || curr->backgroundColor() != compare->backgroundColor()) { + modifiedFields << tr("Color"); + } + if (curr->timeInfo().expires() != compare->timeInfo().expires() + || curr->timeInfo().expiryTime() != compare->timeInfo().expiryTime()) { + modifiedFields << tr("Expiration"); + } + if (curr->totpSettingsString() != compare->totpSettingsString()) { + modifiedFields << tr("TOTP"); + } + if (*curr->customData() != *compare->customData()) { + modifiedFields << tr("Custom Data"); + } + if (*curr->attachments() != *compare->attachments()) { + modifiedFields << tr("Attachments"); + } + if (*curr->autoTypeAssociations() != *compare->autoTypeAssociations() + || curr->autoTypeEnabled() != compare->autoTypeEnabled() + || curr->defaultAutoTypeSequence() != compare->defaultAutoTypeSequence()) { + modifiedFields << tr("Auto-Type"); + } + + m_historyModifications << modifiedFields.join(", "); + + compare = curr; + } +} diff --git a/src/gui/entry/EntryHistoryModel.h b/src/gui/entry/EntryHistoryModel.h index 6d186f0493..21897ec073 100644 --- a/src/gui/entry/EntryHistoryModel.h +++ b/src/gui/entry/EntryHistoryModel.h @@ -35,7 +35,7 @@ class EntryHistoryModel : public QAbstractTableModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - void setEntries(const QList& entries); + void setEntries(const QList& entries, Entry* parentEntry); void clear(); void clearDeletedEntries(); QList deletedEntries(); @@ -43,8 +43,12 @@ class EntryHistoryModel : public QAbstractTableModel void deleteAll(); private: + void calculateHistoryModifications(); + QList m_historyEntries; QList m_deletedHistoryEntries; + QStringList m_historyModifications; + const Entry* m_parentEntry; }; #endif // KEEPASSX_ENTRYHISTORYMODEL_H