From f0fcc199156b842e7fc972d4896d451dc1a99fc7 Mon Sep 17 00:00:00 2001 From: frostasm Date: Mon, 16 Oct 2017 10:35:40 +0300 Subject: [PATCH] Implement recursive resolving for placeholders --- src/core/Entry.cpp | 200 +++++++++++++++++++++++++------------------- src/core/Entry.h | 6 +- tests/TestEntry.cpp | 106 +++++++++++++++++++---- tests/TestEntry.h | 1 + 4 files changed, 209 insertions(+), 104 deletions(-) diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 1a1dff02a..d5afec046 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -26,6 +26,8 @@ #include "totp/totp.h" const int Entry::DefaultIconNumber = 0; +const int Entry::ResolveMaximumDepth = 10; + Entry::Entry() : m_attributes(new EntryAttributes(this)) @@ -670,6 +672,108 @@ void Entry::updateModifiedSinceBegin() m_modifiedSinceBegin = true; } +QString Entry::resolveMultiplePlaceholdersRecursive(const QString &str, int maxDepth) const +{ + if (maxDepth <= 0) { + qWarning("Maximum depth of replacement has been reached. Entry uuid: %s", qPrintable(uuid().toHex())); + return str; + } + + QString result = str; + QRegExp placeholderRegEx("(\\{[^\\}]+\\})", Qt::CaseInsensitive, QRegExp::RegExp2); + placeholderRegEx.setMinimal(true); + int pos = 0; + while ((pos = placeholderRegEx.indexIn(str, pos)) != -1) { + const QString found = placeholderRegEx.cap(1); + result.replace(found, resolvePlaceholderRecursive(found, maxDepth - 1)); + pos += placeholderRegEx.matchedLength(); + } + + if (result != str) { + result = resolveMultiplePlaceholdersRecursive(result, maxDepth - 1); + } + + return result; +} + +QString Entry::resolvePlaceholderRecursive(const QString &placeholder, int maxDepth) const +{ + const PlaceholderType typeOfPlaceholder = placeholderType(placeholder); + switch (typeOfPlaceholder) { + case PlaceholderType::NotPlaceholder: + return placeholder; + case PlaceholderType::Unknown: + qWarning("Can't resolve placeholder %s for entry with uuid %s", qPrintable(placeholder), + qPrintable(uuid().toHex())); + return placeholder; + case PlaceholderType::Title: + return title(); + case PlaceholderType::UserName: + return username(); + case PlaceholderType::Password: + return password(); + case PlaceholderType::Notes: + return notes(); + case PlaceholderType::Totp: + return totp(); + case PlaceholderType::Url: + return url(); + case PlaceholderType::UrlWithoutScheme: + case PlaceholderType::UrlScheme: + case PlaceholderType::UrlHost: + case PlaceholderType::UrlPort: + case PlaceholderType::UrlPath: + case PlaceholderType::UrlQuery: + case PlaceholderType::UrlFragment: + case PlaceholderType::UrlUserInfo: + case PlaceholderType::UrlUserName: + case PlaceholderType::UrlPassword: { + const QString strUrl = resolveMultiplePlaceholdersRecursive(url(), maxDepth - 1); + return resolveUrlPlaceholder(strUrl, typeOfPlaceholder); + } + case PlaceholderType::CustomAttribute: { + const QString key = placeholder.mid(3, placeholder.length() - 4); // {S:attr} => mid(3, len - 4) + return attributes()->hasKey(key) ? attributes()->value(key) : QString(); + } + case PlaceholderType::Reference: { + // resolving references in format: {REF:@I:} + // using format from http://keepass.info/help/base/fieldrefs.html at the time of writing, + // but supporting lookups of standard fields and references by UUID only + + QString result; + QRegExp* referenceRegExp = m_attributes->referenceRegExp(); + if (referenceRegExp->indexIn(placeholder) != -1) { + constexpr int wantedFieldIndex = 1; + constexpr int referencedUuidIndex = 2; + const Uuid referencedUuid(QByteArray::fromHex(referenceRegExp->cap(referencedUuidIndex).toLatin1())); + const Entry* refEntry = m_group->database()->resolveEntry(referencedUuid); + if (refEntry) { + const QString wantedField = referenceRegExp->cap(wantedFieldIndex).toLower(); + if (wantedField == "t") { + result = refEntry->title(); + } else if (wantedField == "u") { + result = refEntry->username(); + } else if (wantedField == "p") { + result = refEntry->password(); + } else if (wantedField == "a") { + result = refEntry->url(); + } else if (wantedField == "n") { + result = refEntry->notes(); + } + + // Referencing fields of other entries only works with standard fields, not with custom user strings. + // If you want to reference a custom user string, you need to place a redirection in a standard field + // of the entry with the custom string, using {S:}, and reference the standard field. + result = refEntry->resolveMultiplePlaceholdersRecursive(result, maxDepth - 1); + } + } + return result; + } + } + + return placeholder; +} + Group* Entry::group() { return m_group; @@ -736,97 +840,21 @@ QString Entry::maskPasswordPlaceholders(const QString &str) const QString Entry::resolveMultiplePlaceholders(const QString& str) const { - QString result = str; - QRegExp tmplRegEx("(\\{.*\\})", Qt::CaseInsensitive, QRegExp::RegExp2); - tmplRegEx.setMinimal(true); - QStringList tmplList; - int pos = 0; - - while ((pos = tmplRegEx.indexIn(str, pos)) != -1) { - QString found = tmplRegEx.cap(1); - result.replace(found,resolvePlaceholder(found)); - pos += tmplRegEx.matchedLength(); - } - - return result; + return resolveMultiplePlaceholdersRecursive(str, ResolveMaximumDepth); } QString Entry::resolvePlaceholder(const QString& placeholder) const { - const PlaceholderType placeholderType = this->placeholderType(placeholder); - switch (placeholderType) { - case PlaceholderType::NotPlaceholder: - return placeholder; - case PlaceholderType::Unknown: - qWarning("Can't resolve placeholder '%s' for entry with uuid %s", qPrintable(placeholder), - qPrintable(uuid().toHex())); - return placeholder; - case PlaceholderType::Title: - return title(); - case PlaceholderType::UserName: - return username(); - case PlaceholderType::Password: - return password(); - case PlaceholderType::Notes: - return notes(); - case PlaceholderType::Totp: - return totp(); - case PlaceholderType::Url: - case PlaceholderType::UrlWithoutScheme: - case PlaceholderType::UrlScheme: - case PlaceholderType::UrlHost: - case PlaceholderType::UrlPort: - case PlaceholderType::UrlPath: - case PlaceholderType::UrlQuery: - case PlaceholderType::UrlFragment: - case PlaceholderType::UrlUserInfo: - case PlaceholderType::UrlUserName: - case PlaceholderType::UrlPassword: - return resolveUrlPlaceholder(placeholderType); - case PlaceholderType::CustomAttribute: { - const QString key = placeholder.mid(3, placeholder.length() - 4); // {S:attribute} => mid(3, len - 4) - return attributes()->hasKey(key) ? attributes()->value(key) : placeholder; - } - case PlaceholderType::Reference: { - // resolving references in format: {REF:@I:} - // using format from http://keepass.info/help/base/fieldrefs.html at the time of writing, - // but supporting lookups of standard fields and references by UUID only - - QRegExp* tmpRegExp = m_attributes->referenceRegExp(); - if (tmpRegExp->indexIn(placeholder) != -1) { - constexpr int wantedFieldIndex = 1; - constexpr int referencedUuidIndex = 2; - const Uuid referencedUuid(QByteArray::fromHex(tmpRegExp->cap(referencedUuidIndex).toLatin1())); - const Entry* refEntry = m_group->database()->resolveEntry(referencedUuid); - if (refEntry) { - QString result; - const QString wantedField = tmpRegExp->cap(wantedFieldIndex).toLower(); - if (wantedField == "t") result = refEntry->title(); - else if (wantedField == "u") result = refEntry->username(); - else if (wantedField == "p") result = refEntry->password(); - else if (wantedField == "a") result = refEntry->url(); - else if (wantedField == "n") result = refEntry->notes(); - - result = refEntry->resolveMultiplePlaceholders(result); - return result; - } - } - } - } - - return placeholder; + return resolvePlaceholderRecursive(placeholder, ResolveMaximumDepth); } -QString Entry::resolveUrlPlaceholder(Entry::PlaceholderType placeholderType) const +QString Entry::resolveUrlPlaceholder(const QString &str, Entry::PlaceholderType placeholderType) const { - const QString urlStr = url(); - if (urlStr.isEmpty()) + if (str.isEmpty()) return QString(); - const QUrl qurl(urlStr); + const QUrl qurl(str); switch (placeholderType) { - case PlaceholderType::Url: - return urlStr; case PlaceholderType::UrlWithoutScheme: return qurl.toString(QUrl::RemoveScheme | QUrl::FullyDecoded); case PlaceholderType::UrlScheme: @@ -847,12 +875,13 @@ QString Entry::resolveUrlPlaceholder(Entry::PlaceholderType placeholderType) con return qurl.userName(); case PlaceholderType::UrlPassword: return qurl.password(); - default: - Q_ASSERT(false); + default: { + Q_ASSERT_X(false, "Entry::resolveUrlPlaceholder", "Bad url placeholder type"); break; } + } - return urlStr; + return QString(); } Entry::PlaceholderType Entry::placeholderType(const QString &placeholder) const @@ -886,8 +915,7 @@ Entry::PlaceholderType Entry::placeholderType(const QString &placeholder) const { QStringLiteral("{URL:PASSWORD}"), PlaceholderType::UrlPassword } }; - PlaceholderType result = placeholders.value(placeholder.toUpper(), PlaceholderType::Unknown); - return result; + return placeholders.value(placeholder.toUpper(), PlaceholderType::Unknown); } QString Entry::resolveUrl(const QString& url) const diff --git a/src/core/Entry.h b/src/core/Entry.h index 7b7a25088..212c8668a 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -96,6 +96,7 @@ public: const EntryAttachments* attachments() const; static const int DefaultIconNumber; + static const int ResolveMaximumDepth; void setUuid(const Uuid& uuid); void setIcon(int iconNumber); @@ -168,7 +169,7 @@ public: QString maskPasswordPlaceholders(const QString& str) const; QString resolveMultiplePlaceholders(const QString& str) const; QString resolvePlaceholder(const QString& str) const; - QString resolveUrlPlaceholder(PlaceholderType placeholderType) const; + QString resolveUrlPlaceholder(const QString &str, PlaceholderType placeholderType) const; PlaceholderType placeholderType(const QString& placeholder) const; QString resolveUrl(const QString& url) const; @@ -199,6 +200,9 @@ private slots: void updateModifiedSinceBegin(); private: + QString resolveMultiplePlaceholdersRecursive(const QString& str, int maxDepth) const; + QString resolvePlaceholderRecursive(const QString& placeholder, int maxDepth) const; + const Database* database() const; template bool set(T& property, const T& value); diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index 056dbc931..1e863dbeb 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -20,7 +20,9 @@ #include +#include "core/Database.h" #include "core/Entry.h" +#include "core/Group.h" #include "crypto/Crypto.h" QTEST_GUILESS_MAIN(TestEntry) @@ -161,8 +163,8 @@ void TestEntry::testResolveUrl() void TestEntry::testResolveUrlPlaceholders() { - Entry* entry = new Entry(); - entry->setUrl("https://user:pw@keepassxc.org:80/path/example.php?q=e&s=t+2#fragment"); + Entry entry; + entry.setUrl("https://user:pw@keepassxc.org:80/path/example.php?q=e&s=t+2#fragment"); QString rmvscm("//user:pw@keepassxc.org:80/path/example.php?q=e&s=t+2#fragment"); // Entry URL without scheme name. QString scm("https"); // Scheme name of the entry URL. @@ -173,20 +175,90 @@ void TestEntry::testResolveUrlPlaceholders() QString userinfo("user:pw"); // User information of the entry URL. QString username("user"); // User name of the entry URL. QString password("pw"); // Password of the entry URL. - QString fragment("fragment"); // Password of the entry URL. + QString fragment("fragment"); // Fragment of the entry URL. - QCOMPARE(entry->resolvePlaceholder("{URL:RMVSCM}"), rmvscm); - QCOMPARE(entry->resolvePlaceholder("{URL:WITHOUTSCHEME}"), rmvscm); - QCOMPARE(entry->resolvePlaceholder("{URL:SCM}"), scm); - QCOMPARE(entry->resolvePlaceholder("{URL:SCHEME}"), scm); - QCOMPARE(entry->resolvePlaceholder("{URL:HOST}"), host); - QCOMPARE(entry->resolvePlaceholder("{URL:PORT}"), port); - QCOMPARE(entry->resolvePlaceholder("{URL:PATH}"), path); - QCOMPARE(entry->resolvePlaceholder("{URL:QUERY}"), query); - QCOMPARE(entry->resolvePlaceholder("{URL:USERINFO}"), userinfo); - QCOMPARE(entry->resolvePlaceholder("{URL:USERNAME}"), username); - QCOMPARE(entry->resolvePlaceholder("{URL:PASSWORD}"), password); - QCOMPARE(entry->resolvePlaceholder("{URL:FRAGMENT}"), fragment); - - delete entry; + QCOMPARE(entry.resolvePlaceholder("{URL:RMVSCM}"), rmvscm); + QCOMPARE(entry.resolvePlaceholder("{URL:WITHOUTSCHEME}"), rmvscm); + QCOMPARE(entry.resolvePlaceholder("{URL:SCM}"), scm); + QCOMPARE(entry.resolvePlaceholder("{URL:SCHEME}"), scm); + QCOMPARE(entry.resolvePlaceholder("{URL:HOST}"), host); + QCOMPARE(entry.resolvePlaceholder("{URL:PORT}"), port); + QCOMPARE(entry.resolvePlaceholder("{URL:PATH}"), path); + QCOMPARE(entry.resolvePlaceholder("{URL:QUERY}"), query); + QCOMPARE(entry.resolvePlaceholder("{URL:USERINFO}"), userinfo); + QCOMPARE(entry.resolvePlaceholder("{URL:USERNAME}"), username); + QCOMPARE(entry.resolvePlaceholder("{URL:PASSWORD}"), password); + QCOMPARE(entry.resolvePlaceholder("{URL:FRAGMENT}"), fragment); +} + +void TestEntry::testResolveRecursivePlaceholders() +{ + Database db; + Group* root = db.rootGroup(); + + Entry* entry1 = new Entry(); + entry1->setGroup(root); + entry1->setUuid(Uuid::random()); + entry1->setTitle("{USERNAME}"); + entry1->setUsername("{PASSWORD}"); + entry1->setPassword("{URL}"); + entry1->setUrl("{S:CustomTitle}"); + entry1->attributes()->set("CustomTitle", "RecursiveValue"); + QCOMPARE(entry1->resolveMultiplePlaceholders(entry1->title()), QString("RecursiveValue")); + + Entry* entry2 = new Entry(); + entry2->setGroup(root); + entry2->setUuid(Uuid::random()); + entry2->setTitle("Entry2Title"); + entry2->setUsername("{S:CustomUserNameAttribute}"); + entry2->setPassword(QString("{REF:P@I:%1}").arg(entry1->uuid().toHex())); + entry2->setUrl("http://{S:IpAddress}:{S:Port}/{S:Uri}"); + entry2->attributes()->set("CustomUserNameAttribute", "CustomUserNameValue"); + entry2->attributes()->set("IpAddress", "127.0.0.1"); + entry2->attributes()->set("Port", "1234"); + entry2->attributes()->set("Uri", "uri/path"); + + Entry* entry3 = new Entry(); + entry3->setGroup(root); + entry3->setUuid(Uuid::random()); + entry3->setTitle(QString("{REF:T@I:%1}").arg(entry2->uuid().toHex())); + entry3->setUsername(QString("{REF:U@I:%1}").arg(entry2->uuid().toHex())); + entry3->setPassword(QString("{REF:P@I:%1}").arg(entry2->uuid().toHex())); + entry3->setUrl(QString("{REF:A@I:%1}").arg(entry2->uuid().toHex())); + + QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->title()), QString("Entry2Title")); + QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->username()), QString("CustomUserNameValue")); + QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->password()), QString("RecursiveValue")); + QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->url()), QString("http://127.0.0.1:1234/uri/path")); + + Entry* entry4 = new Entry(); + entry4->setGroup(root); + entry4->setUuid(Uuid::random()); + entry4->setTitle(QString("{REF:T@I:%1}").arg(entry3->uuid().toHex())); + entry4->setUsername(QString("{REF:U@I:%1}").arg(entry3->uuid().toHex())); + entry4->setPassword(QString("{REF:P@I:%1}").arg(entry3->uuid().toHex())); + entry4->setUrl(QString("{REF:A@I:%1}").arg(entry3->uuid().toHex())); + + QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->title()), QString("Entry2Title")); + QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->username()), QString("CustomUserNameValue")); + QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->password()), QString("RecursiveValue")); + QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->url()), QString("http://127.0.0.1:1234/uri/path")); + + Entry* entry5 = new Entry(); + entry5->setGroup(root); + entry5->setUuid(Uuid::random()); + entry5->attributes()->set("Scheme", "http"); + entry5->attributes()->set("Host", "host.org"); + entry5->attributes()->set("Port", "2017"); + entry5->attributes()->set("Path", "/some/path"); + entry5->attributes()->set("UserName", "username"); + entry5->attributes()->set("Password", "password"); + entry5->attributes()->set("Query", "q=e&t=s"); + entry5->attributes()->set("Fragment", "fragment"); + entry5->setUrl("{S:Scheme}://{S:UserName}:{S:Password}@{S:Host}:{S:Port}{S:Path}?{S:Query}#{S:Fragment}"); + entry5->setTitle("title+{URL:Path}+{URL:Fragment}+title"); + + const QString url("http://username:password@host.org:2017/some/path?q=e&t=s#fragment"); + QCOMPARE(entry5->resolveMultiplePlaceholders(entry5->url()), url); + QCOMPARE(entry5->resolveMultiplePlaceholders(entry5->title()), QString("title+/some/path+fragment+title")); } diff --git a/tests/TestEntry.h b/tests/TestEntry.h index febdf0855..50fec57a5 100644 --- a/tests/TestEntry.h +++ b/tests/TestEntry.h @@ -33,6 +33,7 @@ private slots: void testClone(); void testResolveUrl(); void testResolveUrlPlaceholders(); + void testResolveRecursivePlaceholders(); }; #endif // KEEPASSX_TESTENTRY_H