mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-01-13 16:30:29 -05:00
Merge pull request #1078 from frostasm/implement-recursive-resolving-for-placeholders
Implement recursive resolving for placeholders
This commit is contained in:
commit
190d3a1da9
@ -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,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<QString> 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:<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
|
||||
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<QString, PlaceholderType> 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
|
||||
|
@ -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 <class T> bool set(T& property, const T& value);
|
||||
|
||||
|
@ -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)
|
||||
@ -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"));
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ private slots:
|
||||
void testCopyDataFrom();
|
||||
void testClone();
|
||||
void testResolveUrl();
|
||||
void testResolveUrlPlaceholders();
|
||||
void testResolveRecursivePlaceholders();
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TESTENTRY_H
|
||||
|
Loading…
Reference in New Issue
Block a user