Implement {UUID} placeholder and nested reference support (#12511)

* Closes #12509
* Implement the {UUID} placeholder
* Implement nested placeholder de-referencing when resolving entry references to support a reference like {REF:U@I:{UUID}} which is equivalent to {USERNAME} placeholder.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: droidmonkey <2809491+droidmonkey@users.noreply.github.com>
Co-authored-by: Jonathan White <support@dmapps.us>
This commit is contained in:
Copilot 2025-09-27 19:08:22 -04:00 committed by GitHub
parent fd2a729677
commit 5736c6379c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 54 additions and 2 deletions

View file

@ -1167,6 +1167,8 @@ QString Entry::resolvePlaceholderRecursive(const QString& placeholder, int maxDe
return resolveMultiplePlaceholdersRecursive(notes(), maxDepth); return resolveMultiplePlaceholdersRecursive(notes(), maxDepth);
case PlaceholderType::Url: case PlaceholderType::Url:
return resolveMultiplePlaceholdersRecursive(url(), maxDepth); return resolveMultiplePlaceholdersRecursive(url(), maxDepth);
case PlaceholderType::Uuid:
return uuidToHex();
case PlaceholderType::DbDir: { case PlaceholderType::DbDir: {
QFileInfo fileInfo(database()->filePath()); QFileInfo fileInfo(database()->filePath());
return fileInfo.absoluteDir().absolutePath(); return fileInfo.absoluteDir().absolutePath();
@ -1362,7 +1364,10 @@ QString Entry::resolveReferencePlaceholderRecursive(const QString& placeholder,
QString result; QString result;
const QString searchIn = match.captured(EntryAttributes::SearchInGroupName); const QString searchIn = match.captured(EntryAttributes::SearchInGroupName);
const QString searchText = match.captured(EntryAttributes::SearchTextGroupName); QString searchText = match.captured(EntryAttributes::SearchTextGroupName);
// Resolve placeholders in the search text (e.g., {UUID} -> actual UUID)
searchText = resolvePlaceholder(searchText);
const EntryReferenceType searchInType = Entry::referenceType(searchIn); const EntryReferenceType searchInType = Entry::referenceType(searchIn);
@ -1573,6 +1578,7 @@ Entry::PlaceholderType Entry::placeholderType(const QString& placeholder) const
{QStringLiteral("{NOTES}"), PlaceholderType::Notes}, {QStringLiteral("{NOTES}"), PlaceholderType::Notes},
{QStringLiteral("{TOTP}"), PlaceholderType::Totp}, {QStringLiteral("{TOTP}"), PlaceholderType::Totp},
{QStringLiteral("{URL}"), PlaceholderType::Url}, {QStringLiteral("{URL}"), PlaceholderType::Url},
{QStringLiteral("{UUID}"), PlaceholderType::Uuid},
{QStringLiteral("{URL:RMVSCM}"), PlaceholderType::UrlWithoutScheme}, {QStringLiteral("{URL:RMVSCM}"), PlaceholderType::UrlWithoutScheme},
{QStringLiteral("{URL:WITHOUTSCHEME}"), PlaceholderType::UrlWithoutScheme}, {QStringLiteral("{URL:WITHOUTSCHEME}"), PlaceholderType::UrlWithoutScheme},
{QStringLiteral("{URL:SCM}"), PlaceholderType::UrlScheme}, {QStringLiteral("{URL:SCM}"), PlaceholderType::UrlScheme},

View file

@ -210,6 +210,7 @@ public:
Notes, Notes,
Totp, Totp,
Url, Url,
Uuid,
UrlWithoutScheme, UrlWithoutScheme,
UrlScheme, UrlScheme,
UrlHost, UrlHost,

View file

@ -323,8 +323,9 @@ bool EntryAttributes::operator!=(const EntryAttributes& other) const
QRegularExpressionMatch EntryAttributes::matchReference(const QString& text) QRegularExpressionMatch EntryAttributes::matchReference(const QString& text)
{ {
// Updated regex to handle nested braces in SearchText (e.g., {UUID})
static const QRegularExpression referenceRegExp( static const QRegularExpression referenceRegExp(
R"(\{REF:(?<WantedField>[TUPANI])@(?<SearchIn>[TUPANIO]):(?<SearchText>[^}]+)\})", R"(\{REF:(?<WantedField>[TUPANI])@(?<SearchIn>[TUPANIO]):(?<SearchText>(?:[^{}]|\{[^}]*\})+)\})",
QRegularExpression::CaseInsensitiveOption); QRegularExpression::CaseInsensitiveOption);
return referenceRegExp.match(text); return referenceRegExp.match(text);

View file

@ -445,6 +445,49 @@ void TestEntry::testResolveReferencePlaceholders()
entry3->attributes()->value("AttributeNotes")); entry3->attributes()->value("AttributeNotes"));
} }
void TestEntry::testResolveUuidPlaceholder()
{
Database db;
auto* root = db.rootGroup();
auto* entry = new Entry();
entry->setGroup(root);
entry->setUuid(QUuid::createUuid());
entry->setTitle("Test Entry");
entry->setUsername("TestUser");
entry->setPassword("TestPass");
entry->setNotes("Test with UUID: {UUID}");
// Test that {UUID} placeholder resolves to the entry's UUID
QString expectedUuid = entry->uuidToHex();
QString resolvedNotes = entry->resolveMultiplePlaceholders(entry->notes());
QCOMPARE(resolvedNotes, QString("Test with UUID: %1").arg(expectedUuid));
// Test {UUID} placeholder directly
QCOMPARE(entry->resolveMultiplePlaceholders("{UUID}"), expectedUuid);
// Test case insensitivity
QCOMPARE(entry->resolveMultiplePlaceholders("{uuid}"), expectedUuid);
QCOMPARE(entry->resolveMultiplePlaceholders("{Uuid}"), expectedUuid);
// Test mixed case in text
QCOMPARE(entry->resolveMultiplePlaceholders("UUID is {UUID} here"), QString("UUID is %1 here").arg(expectedUuid));
// Test advanced attribute with {REF:U@I:{UUID}} - should resolve to the entry's own username
entry->attributes()->set("SelfReference", "{REF:U@I:{UUID}}");
QString attributeValue = entry->attributes()->value("SelfReference");
QString resolvedSelfRef = entry->resolveMultiplePlaceholders(attributeValue);
// Test the manual reference to confirm it works as before
QString manualReference = QString("{REF:U@I:%1}").arg(entry->uuidToHex());
entry->attributes()->set("ManualReference", manualReference);
QString resolvedManualRef = entry->resolveMultiplePlaceholders(entry->attributes()->value("ManualReference"));
// Test that both approaches work
QCOMPARE(resolvedManualRef, entry->username());
QCOMPARE(resolvedSelfRef, entry->username());
}
void TestEntry::testResolveNonIdPlaceholdersToUuid() void TestEntry::testResolveNonIdPlaceholdersToUuid()
{ {
Database db; Database db;

View file

@ -35,6 +35,7 @@ private slots:
void testResolveUrlPlaceholders(); void testResolveUrlPlaceholders();
void testResolveRecursivePlaceholders(); void testResolveRecursivePlaceholders();
void testResolveReferencePlaceholders(); void testResolveReferencePlaceholders();
void testResolveUuidPlaceholder();
void testResolveNonIdPlaceholdersToUuid(); void testResolveNonIdPlaceholdersToUuid();
void testResolveConversionPlaceholders(); void testResolveConversionPlaceholders();
void testResolveReplacePlaceholders(); void testResolveReplacePlaceholders();