diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 464628dc..d5afec04 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,67 +840,82 @@ 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& str) const +QString Entry::resolvePlaceholder(const QString& placeholder) const { - QString result = str; + return resolvePlaceholderRecursive(placeholder, ResolveMaximumDepth); +} - const QList keyList = attributes()->keys(); - for (const QString& key : keyList) { - Qt::CaseSensitivity cs = Qt::CaseInsensitive; - QString k = key; +QString Entry::resolveUrlPlaceholder(const QString &str, Entry::PlaceholderType placeholderType) const +{ + if (str.isEmpty()) + return QString(); - if (!EntryAttributes::isDefaultAttribute(key)) { - cs = Qt::CaseSensitive; - k.prepend("{S:"); - } else { - k.prepend("{"); - } - - - k.append("}"); - if (result.compare(k,cs)==0) { - result.replace(result,attributes()->value(key)); - break; - } + const QUrl qurl(str); + switch (placeholderType) { + case PlaceholderType::UrlWithoutScheme: + return qurl.toString(QUrl::RemoveScheme | QUrl::FullyDecoded); + case PlaceholderType::UrlScheme: + return qurl.scheme(); + case PlaceholderType::UrlHost: + return qurl.host(); + case PlaceholderType::UrlPort: + return QString::number(qurl.port()); + case PlaceholderType::UrlPath: + return qurl.path(); + case PlaceholderType::UrlQuery: + return qurl.query(); + case PlaceholderType::UrlFragment: + return qurl.fragment(); + case PlaceholderType::UrlUserInfo: + return qurl.userInfo(); + case PlaceholderType::UrlUserName: + return qurl.userName(); + case PlaceholderType::UrlPassword: + return qurl.password(); + default: { + Q_ASSERT_X(false, "Entry::resolveUrlPlaceholder", "Bad url placeholder type"); + break; + } } - // 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 + return QString(); +} - QRegExp* tmpRegExp = m_attributes->referenceRegExp(); - if (tmpRegExp->indexIn(result) != -1) { - // cap(0) contains the whole reference - // cap(1) contains which field is wanted - // cap(2) contains the uuid of the referenced entry - Entry* tmpRefEntry = m_group->database()->resolveEntry(Uuid(QByteArray::fromHex(tmpRegExp->cap(2).toLatin1()))); - if (tmpRefEntry) { - // entry found, get the relevant field - QString tmpRefField = tmpRegExp->cap(1).toLower(); - if (tmpRefField == "t") result.replace(tmpRegExp->cap(0), tmpRefEntry->title(), Qt::CaseInsensitive); - else if (tmpRefField == "u") result.replace(tmpRegExp->cap(0), tmpRefEntry->username(), Qt::CaseInsensitive); - else if (tmpRefField == "p") result.replace(tmpRegExp->cap(0), tmpRefEntry->password(), Qt::CaseInsensitive); - else if (tmpRefField == "a") result.replace(tmpRegExp->cap(0), tmpRefEntry->url(), Qt::CaseInsensitive); - else if (tmpRefField == "n") result.replace(tmpRegExp->cap(0), tmpRefEntry->notes(), Qt::CaseInsensitive); - } +Entry::PlaceholderType Entry::placeholderType(const QString &placeholder) const +{ + if (!placeholder.startsWith(QLatin1Char('{')) || !placeholder.endsWith(QLatin1Char('}'))) { + return PlaceholderType::NotPlaceholder; + } else if (placeholder.startsWith(QLatin1Literal("{S:"))) { + return PlaceholderType::CustomAttribute; + } else if (placeholder.startsWith(QLatin1Literal("{REF:"))) { + return PlaceholderType::Reference; } - return result; + static const QMap placeholders { + { QStringLiteral("{TITLE}"), PlaceholderType::Title }, + { QStringLiteral("{USERNAME}"), PlaceholderType::UserName }, + { QStringLiteral("{PASSWORD}"), PlaceholderType::Password }, + { QStringLiteral("{NOTES}"), PlaceholderType::Notes }, + { QStringLiteral("{TOTP}"), PlaceholderType::Totp }, + { QStringLiteral("{URL}"), PlaceholderType::Url }, + { QStringLiteral("{URL:RMVSCM}"), PlaceholderType::UrlWithoutScheme }, + { QStringLiteral("{URL:WITHOUTSCHEME}"), PlaceholderType::UrlWithoutScheme }, + { QStringLiteral("{URL:SCM}"), PlaceholderType::UrlScheme }, + { QStringLiteral("{URL:SCHEME}"), PlaceholderType::UrlScheme }, + { QStringLiteral("{URL:HOST}"), PlaceholderType::UrlHost }, + { QStringLiteral("{URL:PORT}"), PlaceholderType::UrlPort }, + { QStringLiteral("{URL:PATH}"), PlaceholderType::UrlPath }, + { QStringLiteral("{URL:QUERY}"), PlaceholderType::UrlQuery }, + { QStringLiteral("{URL:FRAGMENT}"), PlaceholderType::UrlFragment }, + { QStringLiteral("{URL:USERINFO}"), PlaceholderType::UrlUserInfo }, + { QStringLiteral("{URL:USERNAME}"), PlaceholderType::UrlUserName }, + { QStringLiteral("{URL:PASSWORD}"), PlaceholderType::UrlPassword } + }; + + 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 a37637c2..212c8668 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); @@ -134,6 +135,29 @@ public: }; Q_DECLARE_FLAGS(CloneFlags, CloneFlag) + enum class PlaceholderType { + NotPlaceholder, + Unknown, + Title, + UserName, + Password, + Notes, + Totp, + Url, + UrlWithoutScheme, + UrlScheme, + UrlHost, + UrlPort, + UrlPath, + UrlQuery, + UrlFragment, + UrlUserInfo, + UrlUserName, + UrlPassword, + Reference, + CustomAttribute + }; + /** * Creates a duplicate of this entry except that the returned entry isn't * part of any group. @@ -145,6 +169,8 @@ public: QString maskPasswordPlaceholders(const QString& str) const; QString resolveMultiplePlaceholders(const QString& str) const; QString resolvePlaceholder(const QString& str) const; + QString resolveUrlPlaceholder(const QString &str, PlaceholderType placeholderType) const; + PlaceholderType placeholderType(const QString& placeholder) const; QString resolveUrl(const QString& url) const; /** @@ -174,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 84ad0318..1e863dbe 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) @@ -158,3 +160,105 @@ void TestEntry::testResolveUrl() delete entry; } + +void TestEntry::testResolveUrlPlaceholders() +{ + 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. + QString host("keepassxc.org"); // Host component of the entry URL. + QString port("80"); // Port number of the entry URL. + QString path("/path/example.php"); // Path component of the entry URL. + QString query("q=e&s=t+2"); // Query information of the entry URL. + 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"); // 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); +} + +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 3f6d20ee..50fec57a 100644 --- a/tests/TestEntry.h +++ b/tests/TestEntry.h @@ -32,6 +32,8 @@ private slots: void testCopyDataFrom(); void testClone(); void testResolveUrl(); + void testResolveUrlPlaceholders(); + void testResolveRecursivePlaceholders(); }; #endif // KEEPASSX_TESTENTRY_H