Implement recursive resolving for placeholders

This commit is contained in:
frostasm 2017-10-16 10:35:40 +03:00
parent e81d8beb19
commit f0fcc19915
4 changed files with 209 additions and 104 deletions

View File

@ -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:<WantedField>@I:<uuid of referenced entry>}
// 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:<Name>}, 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:<WantedField>@I:<uuid of referenced entry>}
// 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

View File

@ -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 <class T> bool set(T& property, const T& value);

View File

@ -20,7 +20,9 @@
#include <QTest>
#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"));
}

View File

@ -33,6 +33,7 @@ private slots:
void testClone();
void testResolveUrl();
void testResolveUrlPlaceholders();
void testResolveRecursivePlaceholders();
};
#endif // KEEPASSX_TESTENTRY_H