diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 235bcc6f7..5a050ffe6 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -1462,6 +1462,9 @@ void Entry::setGroup(Group* group, bool trackPrevious) setPreviousParentGroup(nullptr); m_group->database()->addDeletedObject(m_uuid); + // Resolve references before moving to a different database + resolveReferencesBeforeDatabaseMove(); + // copy custom icon to the new database if (!iconUuid().isNull() && group->database() && m_group->database()->metadata()->hasCustomIcon(iconUuid()) && !group->database()->metadata()->hasCustomIcon(iconUuid())) { @@ -1504,6 +1507,44 @@ Database* Entry::database() return nullptr; } +void Entry::resolveReferencesBeforeDatabaseMove() +{ + if (!m_group || !m_group->database()) { + return; + } + + // Resolve references in all default attributes + for (const QString& key : EntryAttributes::DefaultAttributes) { + if (m_attributes->contains(key) && m_attributes->isReference(key)) { + QString originalValue = m_attributes->value(key); + QString resolvedValue = resolveMultiplePlaceholdersRecursive(originalValue, 10); + + // Only replace if the resolution produced a different value and it's not empty + // Empty resolution means the reference couldn't be resolved, so keep original + if (!resolvedValue.isEmpty() && resolvedValue != originalValue) { + bool isProtected = m_attributes->isProtected(key); + m_attributes->set(key, resolvedValue, isProtected); + } + } + } + + // Resolve references in custom attributes + const QList customKeys = m_attributes->customKeys(); + for (const QString& key : customKeys) { + if (m_attributes->isReference(key)) { + QString originalValue = m_attributes->value(key); + QString resolvedValue = resolveMultiplePlaceholdersRecursive(originalValue, 10); + + // Only replace if the resolution produced a different value and it's not empty + // Empty resolution means the reference couldn't be resolved, so keep original + if (!resolvedValue.isEmpty() && resolvedValue != originalValue) { + bool isProtected = m_attributes->isProtected(key); + m_attributes->set(key, resolvedValue, isProtected); + } + } + } +} + QString Entry::maskPasswordPlaceholders(const QString& str) const { return QString{str}.replace(QStringLiteral("{PASSWORD}"), QStringLiteral("******"), Qt::CaseInsensitive); diff --git a/src/core/Entry.h b/src/core/Entry.h index 4874f5937..294e24acc 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -282,6 +282,8 @@ public: bool canUpdateTimeinfo() const; void setUpdateTimeinfo(bool value); + void resolveReferencesBeforeDatabaseMove(); + signals: /** * Emitted when a default attribute has been changed. diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index 18567001e..9ee78c0dc 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -20,6 +20,9 @@ #include "TestEntry.h" #include "core/Clock.h" +#include "core/Database.h" +#include "core/Entry.h" +#include "core/EntryAttributes.h" #include "core/Group.h" #include "core/Metadata.h" #include "core/TimeInfo.h" @@ -726,6 +729,85 @@ void TestEntry::testResolveClonedEntry() QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->password()), original->password()); } +void TestEntry::testCrossDatabaseReferences() +{ + // Test that references are resolved when moving entries between databases + Database db1; + auto* root1 = db1.rootGroup(); + Database db2; + auto* root2 = db2.rootGroup(); + + // Create original entry in database 1 + auto* originalEntry = new Entry(); + originalEntry->setGroup(root1); + originalEntry->setUuid(QUuid::createUuid()); + originalEntry->setTitle("OriginalTitle"); + originalEntry->setUsername("OriginalUsername"); + originalEntry->setPassword("OriginalPassword"); + originalEntry->setUrl("http://original.com"); + originalEntry->setNotes("OriginalNotes"); + + // Create entry with references to original entry in database 1 + auto* refEntry = new Entry(); + refEntry->setGroup(root1); + refEntry->setUuid(QUuid::createUuid()); + refEntry->setTitle(QString("{REF:T@I:%1}").arg(originalEntry->uuidToHex())); + refEntry->setUsername(QString("{REF:U@I:%1}").arg(originalEntry->uuidToHex())); + refEntry->setPassword(QString("{REF:P@I:%1}").arg(originalEntry->uuidToHex())); + refEntry->setUrl(QString("{REF:A@I:%1}").arg(originalEntry->uuidToHex())); + refEntry->setNotes(QString("{REF:N@I:%1}").arg(originalEntry->uuidToHex())); + + // Add custom attribute with reference + refEntry->attributes()->set("CustomRef", QString("{REF:T@I:%1}").arg(originalEntry->uuidToHex())); + + // Verify references work within same database + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->title()), QString("OriginalTitle")); + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->username()), QString("OriginalUsername")); + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->password()), QString("OriginalPassword")); + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->url()), QString("http://original.com")); + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->notes()), QString("OriginalNotes")); + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->attributes()->value("CustomRef")), + QString("OriginalTitle")); + + // Verify the attributes still contain references (not yet resolved) + QVERIFY(refEntry->attributes()->isReference(EntryAttributes::TitleKey)); + QVERIFY(refEntry->attributes()->isReference(EntryAttributes::UserNameKey)); + QVERIFY(refEntry->attributes()->isReference(EntryAttributes::PasswordKey)); + QVERIFY(refEntry->attributes()->isReference(EntryAttributes::URLKey)); + QVERIFY(refEntry->attributes()->isReference(EntryAttributes::NotesKey)); + QVERIFY(refEntry->attributes()->isReference("CustomRef")); + + // Move the referenced entry to database 2 + // This should resolve the references before the move + refEntry->setGroup(root2); + + // After move, the entry should have resolved values instead of references + QCOMPARE(refEntry->title(), QString("OriginalTitle")); + QCOMPARE(refEntry->username(), QString("OriginalUsername")); + QCOMPARE(refEntry->password(), QString("OriginalPassword")); + QCOMPARE(refEntry->url(), QString("http://original.com")); + QCOMPARE(refEntry->notes(), QString("OriginalNotes")); + QCOMPARE(refEntry->attributes()->value("CustomRef"), QString("OriginalTitle")); + + // Verify that the references have been replaced with actual values + QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::TitleKey)); + QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::UserNameKey)); + QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::PasswordKey)); + QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::URLKey)); + QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::NotesKey)); + QVERIFY(!refEntry->attributes()->isReference("CustomRef")); + + // Test case where original entry doesn't exist (should keep the reference string) + auto* orphanEntry = new Entry(); + orphanEntry->setGroup(root1); + orphanEntry->setUuid(QUuid::createUuid()); + orphanEntry->setTitle("{REF:T@I:NONEXISTENTUUID}"); + + // Move orphan entry - the unresolvable reference should remain unchanged + orphanEntry->setGroup(root2); + QCOMPARE(orphanEntry->title(), QString("{REF:T@I:NONEXISTENTUUID}")); +} + void TestEntry::testIsRecycled() { auto entry = new Entry(); diff --git a/tests/TestEntry.h b/tests/TestEntry.h index 953a7ce7b..54535b0da 100644 --- a/tests/TestEntry.h +++ b/tests/TestEntry.h @@ -40,6 +40,7 @@ private slots: void testResolveConversionPlaceholders(); void testResolveReplacePlaceholders(); void testResolveClonedEntry(); + void testCrossDatabaseReferences(); void testIsRecycled(); void testMoveUpDown(); void testPreviousParentGroup();