mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Implement search for reference placeholder based on fields other than ID
This commit is contained in:
parent
806dd5d783
commit
4c4d8a5e84
@ -98,6 +98,11 @@ Entry* Database::resolveEntry(const Uuid& uuid)
|
||||
return recFindEntry(uuid, m_rootGroup);
|
||||
}
|
||||
|
||||
Entry *Database::resolveEntry(const QString &text, EntryReferenceType referenceType)
|
||||
{
|
||||
return recFindEntry(text, referenceType, m_rootGroup);
|
||||
}
|
||||
|
||||
Entry* Database::recFindEntry(const Uuid& uuid, Group* group)
|
||||
{
|
||||
const QList<Entry*> entryList = group->entries();
|
||||
@ -118,6 +123,56 @@ Entry* Database::recFindEntry(const Uuid& uuid, Group* group)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Entry *Database::recFindEntry(const QString &text, EntryReferenceType referenceType, Group *group)
|
||||
{
|
||||
Q_ASSERT_X(referenceType != EntryReferenceType::Unknown, "Database::recFindEntry",
|
||||
"Can't search entry with \"referenceType\" parameter equal to \"Unknown\"");
|
||||
|
||||
bool found = false;
|
||||
const QList<Entry*> entryList = group->entries();
|
||||
for (Entry* entry : entryList) {
|
||||
switch (referenceType) {
|
||||
case EntryReferenceType::Unknown:
|
||||
return nullptr;
|
||||
case EntryReferenceType::Title:
|
||||
found = entry->title() == text;
|
||||
break;
|
||||
case EntryReferenceType::UserName:
|
||||
found = entry->username() == text;
|
||||
break;
|
||||
case EntryReferenceType::Password:
|
||||
found = entry->password() == text;
|
||||
break;
|
||||
case EntryReferenceType::Url:
|
||||
found = entry->url() == text;
|
||||
break;
|
||||
case EntryReferenceType::Notes:
|
||||
found = entry->notes() == text;
|
||||
break;
|
||||
case EntryReferenceType::Uuid:
|
||||
found = entry->uuid().toHex() == text;
|
||||
break;
|
||||
case EntryReferenceType::CustomAttributes:
|
||||
found = entry->attributes()->containsValue(text);
|
||||
break;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
const QList<Group*> children = group->children();
|
||||
for (Group* child : children) {
|
||||
Entry* result = recFindEntry(text, referenceType, child);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Group* Database::resolveGroup(const Uuid& uuid)
|
||||
{
|
||||
return recFindGroup(uuid, m_rootGroup);
|
||||
|
@ -27,6 +27,7 @@
|
||||
#include "keys/CompositeKey.h"
|
||||
|
||||
class Entry;
|
||||
enum class EntryReferenceType;
|
||||
class Group;
|
||||
class Metadata;
|
||||
class QTimer;
|
||||
@ -81,6 +82,7 @@ public:
|
||||
Metadata* metadata();
|
||||
const Metadata* metadata() const;
|
||||
Entry* resolveEntry(const Uuid& uuid);
|
||||
Entry* resolveEntry(const QString& text, EntryReferenceType referenceType);
|
||||
Group* resolveGroup(const Uuid& uuid);
|
||||
QList<DeletedObject> deletedObjects();
|
||||
void addDeletedObject(const DeletedObject& delObj);
|
||||
@ -142,6 +144,7 @@ private slots:
|
||||
|
||||
private:
|
||||
Entry* recFindEntry(const Uuid& uuid, Group* group);
|
||||
Entry* recFindEntry(const QString& text, EntryReferenceType referenceType, Group* group);
|
||||
Group* recFindGroup(const Uuid& uuid, Group* group);
|
||||
|
||||
void createRecycleBin();
|
||||
|
@ -92,6 +92,29 @@ void Entry::setUpdateTimeinfo(bool value)
|
||||
m_updateTimeinfo = value;
|
||||
}
|
||||
|
||||
EntryReferenceType Entry::referenceType(const QString& referenceStr)
|
||||
{
|
||||
const QString referenceLowerStr = referenceStr.toLower();
|
||||
EntryReferenceType result = EntryReferenceType::Unknown;
|
||||
if (referenceLowerStr == QLatin1String("t")) {
|
||||
result = EntryReferenceType::Title;
|
||||
} else if (referenceLowerStr == QLatin1String("u")) {
|
||||
result = EntryReferenceType::UserName;
|
||||
} else if (referenceLowerStr == QLatin1String("p")) {
|
||||
result = EntryReferenceType::Password;
|
||||
} else if (referenceLowerStr == QLatin1String("a")) {
|
||||
result = EntryReferenceType::Url;
|
||||
} else if (referenceLowerStr == QLatin1String("n")) {
|
||||
result = EntryReferenceType::Notes;
|
||||
} else if (referenceLowerStr == QLatin1String("i")) {
|
||||
result = EntryReferenceType::Uuid;
|
||||
} else if (referenceLowerStr == QLatin1String("o")) {
|
||||
result = EntryReferenceType::CustomAttributes;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Uuid Entry::uuid() const
|
||||
{
|
||||
return m_uuid;
|
||||
@ -764,45 +787,63 @@ QString Entry::resolvePlaceholderRecursive(const QString &placeholder, int maxDe
|
||||
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;
|
||||
}
|
||||
case PlaceholderType::Reference:
|
||||
return resolveReferencePlaceholderRecursive(placeholder, maxDepth);
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
QString Entry::resolveReferencePlaceholderRecursive(const QString &placeholder, int maxDepth) const
|
||||
{
|
||||
// resolving references in format: {REF:<WantedField>@<SearchIn>:<SearchText>}
|
||||
// using format from http://keepass.info/help/base/fieldrefs.html at the time of writing
|
||||
|
||||
QRegularExpression* referenceRegExp = m_attributes->referenceRegExp();
|
||||
QRegularExpressionMatch match = referenceRegExp->match(placeholder);
|
||||
if (!match.hasMatch()) {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
QString result;
|
||||
const QString searchIn = match.captured(EntryAttributes::SearchInGroupName);
|
||||
const QString searchText = match.captured(EntryAttributes::SearchTextGroupName);
|
||||
|
||||
const EntryReferenceType searchInType = Entry::referenceType(searchIn);
|
||||
const Entry* refEntry = m_group->database()->resolveEntry(searchText, searchInType);
|
||||
|
||||
if (refEntry) {
|
||||
const QString wantedField = match.captured(EntryAttributes::WantedFieldGroupName);
|
||||
result = refEntry->referenceFieldValue(Entry::referenceType(wantedField));
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
QString Entry::referenceFieldValue(EntryReferenceType referenceType) const
|
||||
{
|
||||
switch (referenceType) {
|
||||
case EntryReferenceType::Title:
|
||||
return title();
|
||||
case EntryReferenceType::UserName:
|
||||
return username();
|
||||
case EntryReferenceType::Password:
|
||||
return password();
|
||||
case EntryReferenceType::Url:
|
||||
return url();
|
||||
case EntryReferenceType::Notes:
|
||||
return notes();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
Group* Entry::group()
|
||||
{
|
||||
return m_group;
|
||||
|
@ -36,6 +36,17 @@
|
||||
class Database;
|
||||
class Group;
|
||||
|
||||
enum class EntryReferenceType {
|
||||
Unknown,
|
||||
Title,
|
||||
UserName,
|
||||
Password,
|
||||
Url,
|
||||
Notes,
|
||||
Uuid,
|
||||
CustomAttributes
|
||||
};
|
||||
|
||||
struct EntryData
|
||||
{
|
||||
int iconNumber;
|
||||
@ -203,6 +214,10 @@ private slots:
|
||||
private:
|
||||
QString resolveMultiplePlaceholdersRecursive(const QString& str, int maxDepth) const;
|
||||
QString resolvePlaceholderRecursive(const QString& placeholder, int maxDepth) const;
|
||||
QString resolveReferencePlaceholderRecursive(const QString& placeholder, int maxDepth) const;
|
||||
QString referenceFieldValue(EntryReferenceType referenceType) const;
|
||||
|
||||
static EntryReferenceType referenceType(const QString& referenceStr);
|
||||
|
||||
const Database* database() const;
|
||||
template <class T> bool set(T& property, const T& value);
|
||||
|
@ -25,11 +25,17 @@ const QString EntryAttributes::URLKey = "URL";
|
||||
const QString EntryAttributes::NotesKey = "Notes";
|
||||
const QStringList EntryAttributes::DefaultAttributes(QStringList() << TitleKey << UserNameKey
|
||||
<< PasswordKey << URLKey << NotesKey);
|
||||
|
||||
const QString EntryAttributes::WantedFieldGroupName = "WantedField";
|
||||
const QString EntryAttributes::SearchInGroupName = "SearchIn";
|
||||
const QString EntryAttributes::SearchTextGroupName = "SearchText";
|
||||
|
||||
const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD";
|
||||
|
||||
EntryAttributes::EntryAttributes(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_referenceRegExp("\\{REF:([TUPAN])@I:([^}]+)\\}", Qt::CaseInsensitive, QRegExp::RegExp2)
|
||||
, m_referenceRegExp("\\{REF:(?<WantedField>[TUPAN])@(?<SearchIn>[TUPANIO]):(?<SearchText>[^}]+)\\}",
|
||||
QRegularExpression::CaseInsensitiveOption)
|
||||
{
|
||||
clear();
|
||||
}
|
||||
@ -66,6 +72,11 @@ bool EntryAttributes::contains(const QString &key) const
|
||||
return m_attributes.contains(key);
|
||||
}
|
||||
|
||||
bool EntryAttributes::containsValue(const QString &value) const
|
||||
{
|
||||
return m_attributes.values().contains(value);
|
||||
}
|
||||
|
||||
bool EntryAttributes::isProtected(const QString& key) const
|
||||
{
|
||||
return m_protectedAttributes.contains(key);
|
||||
@ -78,14 +89,11 @@ bool EntryAttributes::isReference(const QString& key) const
|
||||
return false;
|
||||
}
|
||||
|
||||
QString data = value(key);
|
||||
if (m_referenceRegExp.indexIn(data) != -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
const QString data = value(key);
|
||||
return m_referenceRegExp.match(data).hasMatch();
|
||||
}
|
||||
|
||||
QRegExp* EntryAttributes::referenceRegExp()
|
||||
QRegularExpression* EntryAttributes::referenceRegExp()
|
||||
{
|
||||
return &m_referenceRegExp;
|
||||
}
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
@ -35,9 +36,10 @@ public:
|
||||
QList<QString> customKeys();
|
||||
QString value(const QString& key) const;
|
||||
bool contains(const QString& key) const;
|
||||
bool containsValue(const QString& value) const;
|
||||
bool isProtected(const QString& key) const;
|
||||
bool isReference(const QString& key) const;
|
||||
QRegExp* referenceRegExp();
|
||||
QRegularExpression *referenceRegExp();
|
||||
void set(const QString& key, const QString& value, bool protect = false);
|
||||
void remove(const QString& key);
|
||||
void rename(const QString& oldKey, const QString& newKey);
|
||||
@ -58,6 +60,10 @@ public:
|
||||
static const QString RememberCmdExecAttr;
|
||||
static bool isDefaultAttribute(const QString& key);
|
||||
|
||||
static const QString WantedFieldGroupName;
|
||||
static const QString SearchInGroupName;
|
||||
static const QString SearchTextGroupName;
|
||||
|
||||
signals:
|
||||
void modified();
|
||||
void defaultKeyModified();
|
||||
@ -74,7 +80,7 @@ signals:
|
||||
private:
|
||||
QMap<QString, QString> m_attributes;
|
||||
QSet<QString> m_protectedAttributes;
|
||||
QRegExp m_referenceRegExp;
|
||||
QRegularExpression m_referenceRegExp;
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_ENTRYATTRIBUTES_H
|
||||
|
@ -285,29 +285,62 @@ void TestEntry::testResolveReferencePlaceholders()
|
||||
entry2->setUuid(Uuid::random());
|
||||
entry2->setTitle("Title2");
|
||||
entry2->setUsername("Username2");
|
||||
entry2->setPassword("password2");
|
||||
entry2->setPassword("Password2");
|
||||
entry2->setUrl("Url2");
|
||||
entry2->setNotes("Notes2");
|
||||
entry2->attributes()->set("CustomAttribute2", "CustomAttributeValue2");
|
||||
entry2->attributes()->set("CustomAttribute21", "CustomAttributeValue21");
|
||||
|
||||
Entry* entry3 = new Entry();
|
||||
entry3->setGroup(root);
|
||||
entry3->setUuid(Uuid::random());
|
||||
entry3->setTitle("{S:AttributeTitle}");
|
||||
entry3->setUsername("{S:AttributeUsername}");
|
||||
entry3->setPassword("{S:AttributePassword}");
|
||||
entry3->setUrl("{S:AttributeUrl}");
|
||||
entry3->setNotes("{S:AttributeNotes}");
|
||||
entry3->attributes()->set("AttributeTitle", "TitleValue");
|
||||
entry3->attributes()->set("AttributeUsername", "UsernameValue");
|
||||
entry3->attributes()->set("AttributePassword", "PasswordValue");
|
||||
entry3->attributes()->set("AttributeUrl", "UrlValue");
|
||||
entry3->attributes()->set("AttributeNotes", "NotesValue");
|
||||
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuid().toHex())), QString("Title1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), QString("Title1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry1->username())), QString("Title1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:T@P:%1}").arg(entry1->password())), QString("Title1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:T@A:%1}").arg(entry1->url())), QString("Title1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:T@N:%1}").arg(entry1->notes())), QString("Title1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:T@O:%1}").arg(entry1->attributes()->value("CustomAttribute1"))), QString("Title1"));
|
||||
Entry* tstEntry = new Entry();
|
||||
tstEntry->setGroup(root);
|
||||
tstEntry->setUuid(Uuid::random());
|
||||
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuid().toHex())), QString("Title1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), QString("Title1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:U@U:%1}").arg(entry1->username())), QString("Username1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:P@P:%1}").arg(entry1->password())), QString("Password1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry1->url())), QString("Url1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry1->notes())), QString("Notes1"));
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(QString("{REF:O@O:%1}").arg(entry1->attributes()->value("CustomAttribute1"))), QString("CustomAttributeValue1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuid().toHex())), QString("Title1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), QString("Title1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry1->username())), QString("Title1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@P:%1}").arg(entry1->password())), QString("Title1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@A:%1}").arg(entry1->url())), QString("Title1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@N:%1}").arg(entry1->notes())), QString("Title1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@O:%1}").arg(entry1->attributes()->value("CustomAttribute1"))), QString("Title1"));
|
||||
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuid().toHex())), QString("Title1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), QString("Title1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@U:%1}").arg(entry1->username())), QString("Username1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@P:%1}").arg(entry1->password())), QString("Password1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry1->url())), QString("Url1"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry1->notes())), QString("Notes1"));
|
||||
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry2->uuid().toHex())), QString("Title2"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry2->title())), QString("Title2"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry2->username())), QString("Title2"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@P:%1}").arg(entry2->password())), QString("Title2"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@A:%1}").arg(entry2->url())), QString("Title2"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@N:%1}").arg(entry2->notes())), QString("Title2"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@O:%1}").arg(entry2->attributes()->value("CustomAttribute2"))), QString("Title2"));
|
||||
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry2->title())), QString("Title2"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@U:%1}").arg(entry2->username())), QString("Username2"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@P:%1}").arg(entry2->password())), QString("Password2"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry2->url())), QString("Url2"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry2->notes())), QString("Notes2"));
|
||||
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry3->uuid().toHex())), QString("TitleValue"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry3->uuid().toHex())), QString("TitleValue"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(entry3->uuid().toHex())), QString("UsernameValue"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(entry3->uuid().toHex())), QString("PasswordValue"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(entry3->uuid().toHex())), QString("UrlValue"));
|
||||
QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(entry3->uuid().toHex())), QString("NotesValue"));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user