From 5e0df62d7adc951aa05bdb064eddd23f50ab87be Mon Sep 17 00:00:00 2001 From: frostasm Date: Sat, 14 Oct 2017 15:41:45 +0300 Subject: [PATCH 1/3] Add processing of the url placeholders --- src/core/Entry.cpp | 92 ++++++++++++++++++++++++++++++++++++++------- src/core/Entry.h | 17 +++++++++ tests/TestEntry.cpp | 32 ++++++++++++++++ tests/TestEntry.h | 1 + 4 files changed, 128 insertions(+), 14 deletions(-) diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 464628dc2..867e68eab 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -755,23 +755,28 @@ QString Entry::resolvePlaceholder(const QString& str) const { QString result = str; - const QList keyList = attributes()->keys(); - for (const QString& key : keyList) { - Qt::CaseSensitivity cs = Qt::CaseInsensitive; - QString k = key; + const UrlPlaceholderType placeholderType = urlPlaceholderType(str); + if (placeholderType != UrlPlaceholderType::NotUrl) { + return resolveUrlPlaceholder(url(), placeholderType); + } else { + const QList keyList = attributes()->keys(); + for (const QString& key : keyList) { + Qt::CaseSensitivity cs = Qt::CaseInsensitive; + QString k = key; - if (!EntryAttributes::isDefaultAttribute(key)) { - cs = Qt::CaseSensitive; - k.prepend("{S:"); - } else { - k.prepend("{"); - } + 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; + k.append("}"); + if (result.compare(k,cs)==0) { + result.replace(result,attributes()->value(key)); + break; + } } } @@ -829,3 +834,62 @@ QString Entry::resolveUrl(const QString& url) const // No valid http URL's found return QString(""); } + +QString Entry::resolveUrlPlaceholder(const QString &strUrl, Entry::UrlPlaceholderType placeholderType) const +{ + QUrl qurl(strUrl); + if (!qurl.isValid()) + return QString(); + + switch (placeholderType) { + case UrlPlaceholderType::FullUrl: + return strUrl; + case UrlPlaceholderType::WithoutScheme: + return qurl.toString(QUrl::RemoveScheme | QUrl::FullyDecoded); + case UrlPlaceholderType::Scheme: + return qurl.scheme(); + case UrlPlaceholderType::Host: + return qurl.host(); + case UrlPlaceholderType::Port: + return QString::number(qurl.port()); + case UrlPlaceholderType::Path: + return qurl.path(); + case UrlPlaceholderType::Query: + return qurl.query(); + case UrlPlaceholderType::Fragment: + return qurl.fragment(); + case UrlPlaceholderType::UserInfo: + return qurl.userInfo(); + case UrlPlaceholderType::UserName: + return qurl.userName(); + case UrlPlaceholderType::Password: + return qurl.password(); + default: + Q_ASSERT(false); + break; + } + + return QString(); +} + +Entry::UrlPlaceholderType Entry::urlPlaceholderType(const QString &placeholder) const +{ + static const QMap urlPlaceholders { + { QStringLiteral("{URL}"), UrlPlaceholderType::FullUrl }, + { QStringLiteral("{URL:RMVSCM}"), UrlPlaceholderType::WithoutScheme }, + { QStringLiteral("{URL:WITHOUTSCHEME}"), UrlPlaceholderType::WithoutScheme }, + { QStringLiteral("{URL:SCM}"), UrlPlaceholderType::Scheme }, + { QStringLiteral("{URL:SCHEME}"), UrlPlaceholderType::Scheme }, + { QStringLiteral("{URL:HOST}"), UrlPlaceholderType::Host }, + { QStringLiteral("{URL:PORT}"), UrlPlaceholderType::Port }, + { QStringLiteral("{URL:PATH}"), UrlPlaceholderType::Path }, + { QStringLiteral("{URL:QUERY}"), UrlPlaceholderType::Query }, + { QStringLiteral("{URL:FRAGMENT}"), UrlPlaceholderType::Fragment }, + { QStringLiteral("{URL:USERINFO}"), UrlPlaceholderType::UserInfo }, + { QStringLiteral("{URL:USERNAME}"), UrlPlaceholderType::UserName }, + { QStringLiteral("{URL:PASSWORD}"), UrlPlaceholderType::Password } + }; + + UrlPlaceholderType result = urlPlaceholders.value(placeholder.toUpper(), UrlPlaceholderType::NotUrl); + return result; +} diff --git a/src/core/Entry.h b/src/core/Entry.h index a37637c27..51208c9e3 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -134,6 +134,21 @@ public: }; Q_DECLARE_FLAGS(CloneFlags, CloneFlag) + enum class UrlPlaceholderType { + NotUrl, + FullUrl, + WithoutScheme, + Scheme, + Host, + Port, + Path, + Query, + Fragment, + UserInfo, + UserName, + Password, + }; + /** * Creates a duplicate of this entry except that the returned entry isn't * part of any group. @@ -145,6 +160,8 @@ public: QString maskPasswordPlaceholders(const QString& str) const; QString resolveMultiplePlaceholders(const QString& str) const; QString resolvePlaceholder(const QString& str) const; + QString resolveUrlPlaceholder(const QString& url, UrlPlaceholderType placeholderType) const; + UrlPlaceholderType urlPlaceholderType(const QString& placeholder) const; QString resolveUrl(const QString& url) const; /** diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index 84ad03181..056dbc931 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -158,3 +158,35 @@ void TestEntry::testResolveUrl() delete entry; } + +void TestEntry::testResolveUrlPlaceholders() +{ + Entry* entry = new 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"); // Password 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; +} diff --git a/tests/TestEntry.h b/tests/TestEntry.h index 3f6d20ee3..febdf0855 100644 --- a/tests/TestEntry.h +++ b/tests/TestEntry.h @@ -32,6 +32,7 @@ private slots: void testCopyDataFrom(); void testClone(); void testResolveUrl(); + void testResolveUrlPlaceholders(); }; #endif // KEEPASSX_TESTENTRY_H From e81d8beb191eab920f165fa4086779915490e77a Mon Sep 17 00:00:00 2001 From: frostasm Date: Sat, 14 Oct 2017 19:23:22 +0300 Subject: [PATCH 2/3] Refactor Entry::resolvePlaceholder function --- src/core/Entry.cpp | 229 +++++++++++++++++++++++++-------------------- src/core/Entry.h | 34 ++++--- 2 files changed, 149 insertions(+), 114 deletions(-) diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 867e68eab..1a1dff02a 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -751,56 +751,142 @@ QString Entry::resolveMultiplePlaceholders(const QString& str) const return result; } -QString Entry::resolvePlaceholder(const QString& str) const +QString Entry::resolvePlaceholder(const QString& placeholder) const { - QString result = str; + 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 - const UrlPlaceholderType placeholderType = urlPlaceholderType(str); - if (placeholderType != UrlPlaceholderType::NotUrl) { - return resolveUrlPlaceholder(url(), placeholderType); - } else { - const QList keyList = attributes()->keys(); - for (const QString& key : keyList) { - Qt::CaseSensitivity cs = Qt::CaseInsensitive; - QString k = key; + 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(); - 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; + result = refEntry->resolveMultiplePlaceholders(result); + return result; } } } - - // 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(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); - } } + return placeholder; +} + +QString Entry::resolveUrlPlaceholder(Entry::PlaceholderType placeholderType) const +{ + const QString urlStr = url(); + if (urlStr.isEmpty()) + return QString(); + + const QUrl qurl(urlStr); + switch (placeholderType) { + case PlaceholderType::Url: + return urlStr; + 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(false); + break; + } + + return urlStr; +} + +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; + } + + 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 } + }; + + PlaceholderType result = placeholders.value(placeholder.toUpper(), PlaceholderType::Unknown); return result; } @@ -834,62 +920,3 @@ QString Entry::resolveUrl(const QString& url) const // No valid http URL's found return QString(""); } - -QString Entry::resolveUrlPlaceholder(const QString &strUrl, Entry::UrlPlaceholderType placeholderType) const -{ - QUrl qurl(strUrl); - if (!qurl.isValid()) - return QString(); - - switch (placeholderType) { - case UrlPlaceholderType::FullUrl: - return strUrl; - case UrlPlaceholderType::WithoutScheme: - return qurl.toString(QUrl::RemoveScheme | QUrl::FullyDecoded); - case UrlPlaceholderType::Scheme: - return qurl.scheme(); - case UrlPlaceholderType::Host: - return qurl.host(); - case UrlPlaceholderType::Port: - return QString::number(qurl.port()); - case UrlPlaceholderType::Path: - return qurl.path(); - case UrlPlaceholderType::Query: - return qurl.query(); - case UrlPlaceholderType::Fragment: - return qurl.fragment(); - case UrlPlaceholderType::UserInfo: - return qurl.userInfo(); - case UrlPlaceholderType::UserName: - return qurl.userName(); - case UrlPlaceholderType::Password: - return qurl.password(); - default: - Q_ASSERT(false); - break; - } - - return QString(); -} - -Entry::UrlPlaceholderType Entry::urlPlaceholderType(const QString &placeholder) const -{ - static const QMap urlPlaceholders { - { QStringLiteral("{URL}"), UrlPlaceholderType::FullUrl }, - { QStringLiteral("{URL:RMVSCM}"), UrlPlaceholderType::WithoutScheme }, - { QStringLiteral("{URL:WITHOUTSCHEME}"), UrlPlaceholderType::WithoutScheme }, - { QStringLiteral("{URL:SCM}"), UrlPlaceholderType::Scheme }, - { QStringLiteral("{URL:SCHEME}"), UrlPlaceholderType::Scheme }, - { QStringLiteral("{URL:HOST}"), UrlPlaceholderType::Host }, - { QStringLiteral("{URL:PORT}"), UrlPlaceholderType::Port }, - { QStringLiteral("{URL:PATH}"), UrlPlaceholderType::Path }, - { QStringLiteral("{URL:QUERY}"), UrlPlaceholderType::Query }, - { QStringLiteral("{URL:FRAGMENT}"), UrlPlaceholderType::Fragment }, - { QStringLiteral("{URL:USERINFO}"), UrlPlaceholderType::UserInfo }, - { QStringLiteral("{URL:USERNAME}"), UrlPlaceholderType::UserName }, - { QStringLiteral("{URL:PASSWORD}"), UrlPlaceholderType::Password } - }; - - UrlPlaceholderType result = urlPlaceholders.value(placeholder.toUpper(), UrlPlaceholderType::NotUrl); - return result; -} diff --git a/src/core/Entry.h b/src/core/Entry.h index 51208c9e3..7b7a25088 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -134,19 +134,27 @@ public: }; Q_DECLARE_FLAGS(CloneFlags, CloneFlag) - enum class UrlPlaceholderType { - NotUrl, - FullUrl, - WithoutScheme, - Scheme, - Host, - Port, - Path, - Query, - Fragment, - UserInfo, + enum class PlaceholderType { + NotPlaceholder, + Unknown, + Title, UserName, Password, + Notes, + Totp, + Url, + UrlWithoutScheme, + UrlScheme, + UrlHost, + UrlPort, + UrlPath, + UrlQuery, + UrlFragment, + UrlUserInfo, + UrlUserName, + UrlPassword, + Reference, + CustomAttribute }; /** @@ -160,8 +168,8 @@ public: QString maskPasswordPlaceholders(const QString& str) const; QString resolveMultiplePlaceholders(const QString& str) const; QString resolvePlaceholder(const QString& str) const; - QString resolveUrlPlaceholder(const QString& url, UrlPlaceholderType placeholderType) const; - UrlPlaceholderType urlPlaceholderType(const QString& placeholder) const; + QString resolveUrlPlaceholder(PlaceholderType placeholderType) const; + PlaceholderType placeholderType(const QString& placeholder) const; QString resolveUrl(const QString& url) const; /** From f0fcc199156b842e7fc972d4896d451dc1a99fc7 Mon Sep 17 00:00:00 2001 From: frostasm Date: Mon, 16 Oct 2017 10:35:40 +0300 Subject: [PATCH 3/3] 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