Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions share/icons/badges/3_Modified.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions share/icons/icons.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -195,5 +195,6 @@
<file>badges/0_ShareActive.svg</file>
<file>badges/1_ShareInactive.svg</file>
<file>badges/2_Expired.svg</file>
<file>badges/3_Modified.svg</file>
</qresource>
</RCC>
14 changes: 11 additions & 3 deletions share/translations/keepassxc_en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4362,6 +4362,10 @@ Would you like to overwrite the existing attachment?</source>
<source>Group Path</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>This entry has unsaved changes</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EntryPreviewWidget</name>
Expand Down Expand Up @@ -10334,15 +10338,19 @@ This option is deprecated, use --set-key-file instead.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Expired</source>
<source>Weak Passwords</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Weak Passwords</source>
<source>TOTP Entries</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>TOTP Entries</source>
<source>Unsaved Changes</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Expired Entries</source>
<translation type="unfinished"></translation>
</message>
</context>
Expand Down
18 changes: 18 additions & 0 deletions src/core/Database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -712,13 +712,19 @@ void Database::setFilePath(const QString& filePath)
if (filePath != m_data.filePath) {
QString oldPath = m_data.filePath;
m_data.filePath = filePath;
updateSavedFileModifiedTime();
// Don't watch for changes until the next open or save operation
m_fileWatcher->stop();
m_ignoreFileChangesUntilSaved = false;
emit filePathChanged(oldPath, filePath);
}
}

QDateTime Database::savedFileModifiedTime() const
{
return m_savedFileModifiedTime;
}

const QByteArray& Database::fileBlockHash() const
{
return m_fileBlockHash;
Expand Down Expand Up @@ -1057,11 +1063,23 @@ void Database::markAsClean()
m_modified = false;
stopModifiedTimer();
m_hasNonDataChange = false;
updateSavedFileModifiedTime();
if (emitSignal) {
emit databaseSaved();
}
}

void Database::updateSavedFileModifiedTime()
{
if (m_data.filePath.isEmpty()) {
m_savedFileModifiedTime = {};
return;
}

const QFileInfo databaseFile(m_data.filePath);
m_savedFileModifiedTime = databaseFile.exists() ? databaseFile.lastModified().toUTC() : QDateTime{};
}

void Database::markNonDataChange()
{
m_hasNonDataChange = true;
Expand Down
3 changes: 3 additions & 0 deletions src/core/Database.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ class Database : public ModifiableObject
QString filePath() const;
QString canonicalFilePath() const;
void setFilePath(const QString& filePath);
QDateTime savedFileModifiedTime() const;

const QByteArray& fileBlockHash() const;
void setIgnoreFileChangesUntilSaved(bool ignore);
Expand Down Expand Up @@ -239,6 +240,7 @@ public slots:

void startModifiedTimer();
void stopModifiedTimer();
void updateSavedFileModifiedTime();

QByteArray m_fileBlockHash;
bool m_ignoreFileChangesUntilSaved;
Expand All @@ -249,6 +251,7 @@ public slots:
QTimer m_modifiedTimer;
QMutex m_saveMutex;
QPointer<FileWatcher> m_fileWatcher;
QDateTime m_savedFileModifiedTime;
bool m_modified = false;
bool m_hasNonDataChange = false;
QString m_keyError;
Expand Down
12 changes: 12 additions & 0 deletions src/core/Entry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,18 @@ bool Entry::hasValidTotp() const
return error.isEmpty();
}

bool Entry::hasUnsavedChanges() const
{
auto db = database();
// Basic checks to avoid more expensive file checks later
if (!db || !db->isModified() || db->filePath().isEmpty()) {
return false;
}

const auto saved = db->savedFileModifiedTime();
return saved.isValid() && timeInfo().lastModificationTime() > saved;
}

bool Entry::hasPasskey() const
{
return m_attributes->hasPasskey();
Expand Down
1 change: 1 addition & 0 deletions src/core/Entry.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class Entry : public ModifiableObject
const QSharedPointer<PasswordHealth> passwordHealth() const;
bool excludeFromReports() const;
void setExcludeFromReports(bool state);
bool hasUnsavedChanges() const;

bool hasPasskey() const;
void removePasskey();
Expand Down
3 changes: 3 additions & 0 deletions src/core/EntrySearcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
break;
}
}
} else if (term.word.compare("modified", Qt::CaseInsensitive) == 0) {
found = entry->hasUnsavedChanges();
break;
}
found = false;
break;
Expand Down
3 changes: 2 additions & 1 deletion src/gui/DatabaseIcons.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ class DatabaseIcons
{
ShareActive = 0,
ShareInactive,
Expired
Expired,
Modified
};

QPixmap icon(int index, IconSize size = IconSize::Default);
Expand Down
2 changes: 2 additions & 0 deletions src/gui/Icons.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ QPixmap Icons::entryIconPixmap(const Entry* entry, IconSize size)

if (entry->isExpired()) {
icon = databaseIcons()->applyBadge(icon, DatabaseIcons::Badges::Expired);
} else if (entry->hasUnsavedChanges()) {
icon = databaseIcons()->applyBadge(icon, DatabaseIcons::Badges::Modified);
}

return icon;
Expand Down
5 changes: 5 additions & 0 deletions src/gui/entry/EntryModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
if (entry->isExpired()) {
font.setStrikeOut(true);
}
if (entry->hasUnsavedChanges()) {
font.setItalic(true);
}
return font;
} else if (role == Qt::ForegroundRole) {

Expand Down Expand Up @@ -360,6 +363,8 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
} else if (role == Qt::ToolTipRole) {
if (index.column() == PasswordStrength && !entry->password().isEmpty() && !entry->excludeFromReports()) {
return entry->passwordHealth()->scoreReason();
} else if (index.column() == Title && entry->hasUnsavedChanges()) {
return tr("This entry has unsaved changes");
}
}

Expand Down
9 changes: 6 additions & 3 deletions src/gui/tag/TagModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,21 @@
#include "core/Database.h"
#include "core/Metadata.h"
#include "gui/Icons.h"
#include "gui/MessageBox.h"

#include <QApplication>
#include <QMenu>

TagModel::TagModel(QObject* parent)
: QAbstractListModel(parent)
{
m_defaultSearches << qMakePair(tr("Clear Search"), QString("")) << qMakePair(tr("All Entries"), QString("*"))
<< qMakePair(tr("Expired"), QString("is:expired"))
// clang-format off
m_defaultSearches << qMakePair(tr("Clear Search"), QString(""))
<< qMakePair(tr("All Entries"), QString("*"))
<< qMakePair(tr("Unsaved Changes"), QString("is:modified"))
<< qMakePair(tr("Expired Entries"), QString("is:expired"))
<< qMakePair(tr("Weak Passwords"), QString("is:weak"))
<< qMakePair(tr("TOTP Entries"), QString("has:totp"));
// clang-format on
}

TagModel::~TagModel() = default;
Expand Down
42 changes: 42 additions & 0 deletions tests/TestEntryModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@

#include "TestEntryModel.h"

#include <QDateTime>
#include <QFileDevice>
#include <QFont>
#include <QSignalSpy>
#include <QTemporaryFile>
#include <QTest>

#include "core/Config.h"
#include "core/Database.h"
#include "core/Entry.h"
#include "core/Group.h"
#include "crypto/Crypto.h"
Expand Down Expand Up @@ -132,6 +138,42 @@ void TestEntryModel::test()
delete model;
}

void TestEntryModel::testUnsavedEntryFont()
{
const auto originalAutoSave = config()->get(Config::AutoSaveAfterEveryChange);

QTemporaryFile dbFile;
QVERIFY(dbFile.open());
QVERIFY(dbFile.setFileTime(QDateTime::currentDateTimeUtc().addSecs(-60), QFileDevice::FileModificationTime));

auto db = new Database();
db->setFilePath(dbFile.fileName());

auto entry = new Entry();
entry->setTitle("Unsaved");
entry->setGroup(db->rootGroup());
db->markAsModified();

auto model = new EntryModel(this);
model->setGroup(db->rootGroup());

auto font = model->data(model->index(0, EntryModel::Title), Qt::FontRole).value<QFont>();
QVERIFY(font.italic());

config()->set(Config::AutoSaveAfterEveryChange, true);
font = model->data(model->index(0, EntryModel::Title), Qt::FontRole).value<QFont>();
QVERIFY(font.italic());

config()->set(Config::AutoSaveAfterEveryChange, false);
db->markAsClean();
font = model->data(model->index(0, EntryModel::Title), Qt::FontRole).value<QFont>();
QVERIFY(!font.italic());

config()->set(Config::AutoSaveAfterEveryChange, originalAutoSave);
delete model;
delete db;
}

void TestEntryModel::testAttachmentsModel()
{
auto entryAttachments = new EntryAttachments(this);
Expand Down
1 change: 1 addition & 0 deletions tests/TestEntryModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class TestEntryModel : public QObject
private slots:
void initTestCase();
void test();
void testUnsavedEntryFont();
void testAttachmentsModel();
void testAttributesModel();
void testDefaultIconModel();
Expand Down
Loading