mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-10-12 21:40:54 -04:00
Expose EntrySearcher's SearchTerm for internal code usage
This commit is contained in:
parent
329701a34e
commit
b96c1e92a3
3 changed files with 97 additions and 67 deletions
|
@ -28,6 +28,20 @@ EntrySearcher::EntrySearcher(bool caseSensitive)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search group, and its children, directly by provided search terms
|
||||||
|
* @param searchTerms search terms
|
||||||
|
* @param baseGroup group to start search from, cannot be null
|
||||||
|
* @param forceSearch ignore group search settings
|
||||||
|
* @return list of entries that match the search terms
|
||||||
|
*/
|
||||||
|
QList<Entry*> EntrySearcher::search(const QList<SearchTerm>& searchTerms, const Group* baseGroup, bool forceSearch)
|
||||||
|
{
|
||||||
|
Q_ASSERT(baseGroup);
|
||||||
|
m_searchTerms = searchTerms;
|
||||||
|
return repeat(baseGroup, forceSearch);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search group, and its children, by parsing the provided search
|
* Search group, and its children, by parsing the provided search
|
||||||
* string for search terms.
|
* string for search terms.
|
||||||
|
@ -69,6 +83,19 @@ QList<Entry*> EntrySearcher::repeat(const Group* baseGroup, bool forceSearch)
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search provided entries by the provided search terms
|
||||||
|
*
|
||||||
|
* @param searchTerms search terms
|
||||||
|
* @param entries list of entries to include in the search
|
||||||
|
* @return list of entries that match the search terms
|
||||||
|
*/
|
||||||
|
QList<Entry*> EntrySearcher::searchEntries(const QList<SearchTerm>& searchTerms, const QList<Entry*>& entries)
|
||||||
|
{
|
||||||
|
m_searchTerms = searchTerms;
|
||||||
|
return repeatEntries(entries);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search provided entries by parsing the search string
|
* Search provided entries by parsing the search string
|
||||||
* for search terms.
|
* for search terms.
|
||||||
|
@ -124,46 +151,46 @@ bool EntrySearcher::searchEntryImpl(Entry* entry)
|
||||||
|
|
||||||
bool found;
|
bool found;
|
||||||
for (const auto& term : m_searchTerms) {
|
for (const auto& term : m_searchTerms) {
|
||||||
switch (term->field) {
|
switch (term.field) {
|
||||||
case Field::Title:
|
case Field::Title:
|
||||||
found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch();
|
found = term.regex.match(entry->resolvePlaceholder(entry->title())).hasMatch();
|
||||||
break;
|
break;
|
||||||
case Field::Username:
|
case Field::Username:
|
||||||
found = term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch();
|
found = term.regex.match(entry->resolvePlaceholder(entry->username())).hasMatch();
|
||||||
break;
|
break;
|
||||||
case Field::Password:
|
case Field::Password:
|
||||||
found = term->regex.match(entry->resolvePlaceholder(entry->password())).hasMatch();
|
found = term.regex.match(entry->resolvePlaceholder(entry->password())).hasMatch();
|
||||||
break;
|
break;
|
||||||
case Field::Url:
|
case Field::Url:
|
||||||
found = term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch();
|
found = term.regex.match(entry->resolvePlaceholder(entry->url())).hasMatch();
|
||||||
break;
|
break;
|
||||||
case Field::Notes:
|
case Field::Notes:
|
||||||
found = term->regex.match(entry->notes()).hasMatch();
|
found = term.regex.match(entry->notes()).hasMatch();
|
||||||
break;
|
break;
|
||||||
case Field::AttributeKey:
|
case Field::AttributeKV:
|
||||||
found = !attributes.filter(term->regex).empty();
|
found = !attributes.filter(term.regex).empty();
|
||||||
break;
|
break;
|
||||||
case Field::Attachment:
|
case Field::Attachment:
|
||||||
found = !attachments.filter(term->regex).empty();
|
found = !attachments.filter(term.regex).empty();
|
||||||
break;
|
break;
|
||||||
case Field::AttributeValue:
|
case Field::AttributeValue:
|
||||||
// skip protected attributes
|
// skip protected attributes
|
||||||
if (entry->attributes()->isProtected(term->word)) {
|
if (entry->attributes()->isProtected(term.word)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
found = entry->attributes()->contains(term->word)
|
found = entry->attributes()->contains(term.word)
|
||||||
&& term->regex.match(entry->attributes()->value(term->word)).hasMatch();
|
&& term.regex.match(entry->attributes()->value(term.word)).hasMatch();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Terms without a specific field try to match title, username, url, and notes
|
// Terms without a specific field try to match title, username, url, and notes
|
||||||
found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch()
|
found = term.regex.match(entry->resolvePlaceholder(entry->title())).hasMatch()
|
||||||
|| term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch()
|
|| term.regex.match(entry->resolvePlaceholder(entry->username())).hasMatch()
|
||||||
|| term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch()
|
|| term.regex.match(entry->resolvePlaceholder(entry->url())).hasMatch()
|
||||||
|| term->regex.match(entry->notes()).hasMatch();
|
|| term.regex.match(entry->notes()).hasMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short circuit if we failed to match or we matched and are excluding this term
|
// Short circuit if we failed to match or we matched and are excluding this term
|
||||||
if ((!found && !term->exclude) || (found && term->exclude)) {
|
if ((!found && !term.exclude) || (found && term.exclude)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,7 +202,7 @@ void EntrySearcher::parseSearchTerms(const QString& searchString)
|
||||||
{
|
{
|
||||||
static const QList<QPair<QString, Field>> fieldnames{
|
static const QList<QPair<QString, Field>> fieldnames{
|
||||||
{QStringLiteral("attachment"), Field::Attachment},
|
{QStringLiteral("attachment"), Field::Attachment},
|
||||||
{QStringLiteral("attribute"), Field::AttributeKey},
|
{QStringLiteral("attribute"), Field::AttributeKV},
|
||||||
{QStringLiteral("notes"), Field::Notes},
|
{QStringLiteral("notes"), Field::Notes},
|
||||||
{QStringLiteral("pw"), Field::Password},
|
{QStringLiteral("pw"), Field::Password},
|
||||||
{QStringLiteral("password"), Field::Password},
|
{QStringLiteral("password"), Field::Password},
|
||||||
|
@ -188,44 +215,44 @@ void EntrySearcher::parseSearchTerms(const QString& searchString)
|
||||||
auto results = m_termParser.globalMatch(searchString);
|
auto results = m_termParser.globalMatch(searchString);
|
||||||
while (results.hasNext()) {
|
while (results.hasNext()) {
|
||||||
auto result = results.next();
|
auto result = results.next();
|
||||||
auto term = QSharedPointer<SearchTerm>::create();
|
SearchTerm term{};
|
||||||
|
|
||||||
// Quoted string group
|
// Quoted string group
|
||||||
term->word = result.captured(3);
|
term.word = result.captured(3);
|
||||||
|
|
||||||
// If empty, use the unquoted string group
|
// If empty, use the unquoted string group
|
||||||
if (term->word.isEmpty()) {
|
if (term.word.isEmpty()) {
|
||||||
term->word = result.captured(4);
|
term.word = result.captured(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If still empty, ignore this match
|
// If still empty, ignore this match
|
||||||
if (term->word.isEmpty()) {
|
if (term.word.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto mods = result.captured(1);
|
auto mods = result.captured(1);
|
||||||
|
|
||||||
// Convert term to regex
|
// Convert term to regex
|
||||||
term->regex = Tools::convertToRegex(term->word, !mods.contains("*"), mods.contains("+"), m_caseSensitive);
|
term.regex = Tools::convertToRegex(term.word, !mods.contains("*"), mods.contains("+"), m_caseSensitive);
|
||||||
|
|
||||||
// Exclude modifier
|
// Exclude modifier
|
||||||
term->exclude = mods.contains("-") || mods.contains("!");
|
term.exclude = mods.contains("-") || mods.contains("!");
|
||||||
|
|
||||||
// Determine the field to search
|
// Determine the field to search
|
||||||
term->field = Field::Undefined;
|
term.field = Field::Undefined;
|
||||||
|
|
||||||
QString field = result.captured(2);
|
QString field = result.captured(2);
|
||||||
if (!field.isEmpty()) {
|
if (!field.isEmpty()) {
|
||||||
if (field.startsWith("_", Qt::CaseInsensitive)) {
|
if (field.startsWith("_", Qt::CaseInsensitive)) {
|
||||||
term->field = Field::AttributeValue;
|
term.field = Field::AttributeValue;
|
||||||
// searching a custom attribute
|
// searching a custom attribute
|
||||||
// in this case term->word is the attribute key (removing the leading "_")
|
// in this case term.word is the attribute key (removing the leading "_")
|
||||||
// and term->regex is used to match attribute value
|
// and term.regex is used to match attribute value
|
||||||
term->word = field.mid(1);
|
term.word = field.mid(1);
|
||||||
} else {
|
} else {
|
||||||
for (const auto& pair : fieldnames) {
|
for (const auto& pair : fieldnames) {
|
||||||
if (pair.first.startsWith(field, Qt::CaseInsensitive)) {
|
if (pair.first.startsWith(field, Qt::CaseInsensitive)) {
|
||||||
term->field = pair.second;
|
term.field = pair.second;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,18 +28,6 @@ class Entry;
|
||||||
class EntrySearcher
|
class EntrySearcher
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit EntrySearcher(bool caseSensitive = false);
|
|
||||||
|
|
||||||
QList<Entry*> search(const QString& searchString, const Group* baseGroup, bool forceSearch = false);
|
|
||||||
QList<Entry*> repeat(const Group* baseGroup, bool forceSearch = false);
|
|
||||||
|
|
||||||
QList<Entry*> searchEntries(const QString& searchString, const QList<Entry*>& entries);
|
|
||||||
QList<Entry*> repeatEntries(const QList<Entry*>& entries);
|
|
||||||
|
|
||||||
void setCaseSensitive(bool state);
|
|
||||||
bool isCaseSensitive();
|
|
||||||
|
|
||||||
private:
|
|
||||||
enum class Field
|
enum class Field
|
||||||
{
|
{
|
||||||
Undefined,
|
Undefined,
|
||||||
|
@ -48,7 +36,7 @@ private:
|
||||||
Password,
|
Password,
|
||||||
Url,
|
Url,
|
||||||
Notes,
|
Notes,
|
||||||
AttributeKey,
|
AttributeKV,
|
||||||
Attachment,
|
Attachment,
|
||||||
AttributeValue
|
AttributeValue
|
||||||
};
|
};
|
||||||
|
@ -56,17 +44,32 @@ private:
|
||||||
struct SearchTerm
|
struct SearchTerm
|
||||||
{
|
{
|
||||||
Field field;
|
Field field;
|
||||||
|
// only used when field == Field::AttributeValue
|
||||||
QString word;
|
QString word;
|
||||||
QRegularExpression regex;
|
QRegularExpression regex;
|
||||||
bool exclude;
|
bool exclude;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
explicit EntrySearcher(bool caseSensitive = false);
|
||||||
|
|
||||||
|
QList<Entry*> search(const QList<SearchTerm>& searchTerms, const Group* baseGroup, bool forceSearch = false);
|
||||||
|
QList<Entry*> search(const QString& searchString, const Group* baseGroup, bool forceSearch = false);
|
||||||
|
QList<Entry*> repeat(const Group* baseGroup, bool forceSearch = false);
|
||||||
|
|
||||||
|
QList<Entry*> searchEntries(const QList<SearchTerm>& searchTerms, const QList<Entry*>& entries);
|
||||||
|
QList<Entry*> searchEntries(const QString& searchString, const QList<Entry*>& entries);
|
||||||
|
QList<Entry*> repeatEntries(const QList<Entry*>& entries);
|
||||||
|
|
||||||
|
void setCaseSensitive(bool state);
|
||||||
|
bool isCaseSensitive();
|
||||||
|
|
||||||
|
private:
|
||||||
bool searchEntryImpl(Entry* entry);
|
bool searchEntryImpl(Entry* entry);
|
||||||
void parseSearchTerms(const QString& searchString);
|
void parseSearchTerms(const QString& searchString);
|
||||||
|
|
||||||
bool m_caseSensitive;
|
bool m_caseSensitive;
|
||||||
QRegularExpression m_termParser;
|
QRegularExpression m_termParser;
|
||||||
QList<QSharedPointer<SearchTerm>> m_searchTerms;
|
QList<SearchTerm> m_searchTerms;
|
||||||
|
|
||||||
friend class TestEntrySearcher;
|
friend class TestEntrySearcher;
|
||||||
};
|
};
|
||||||
|
|
|
@ -197,22 +197,22 @@ void TestEntrySearcher::testSearchTermParser()
|
||||||
|
|
||||||
QCOMPARE(terms.length(), 5);
|
QCOMPARE(terms.length(), 5);
|
||||||
|
|
||||||
QCOMPARE(terms[0]->field, EntrySearcher::Field::Undefined);
|
QCOMPARE(terms[0].field, EntrySearcher::Field::Undefined);
|
||||||
QCOMPARE(terms[0]->word, QString("test"));
|
QCOMPARE(terms[0].word, QString("test"));
|
||||||
QCOMPARE(terms[0]->exclude, true);
|
QCOMPARE(terms[0].exclude, true);
|
||||||
|
|
||||||
QCOMPARE(terms[1]->field, EntrySearcher::Field::Undefined);
|
QCOMPARE(terms[1].field, EntrySearcher::Field::Undefined);
|
||||||
QCOMPARE(terms[1]->word, QString("quoted \\\"string\\\""));
|
QCOMPARE(terms[1].word, QString("quoted \\\"string\\\""));
|
||||||
QCOMPARE(terms[1]->exclude, false);
|
QCOMPARE(terms[1].exclude, false);
|
||||||
|
|
||||||
QCOMPARE(terms[2]->field, EntrySearcher::Field::Username);
|
QCOMPARE(terms[2].field, EntrySearcher::Field::Username);
|
||||||
QCOMPARE(terms[2]->word, QString("user"));
|
QCOMPARE(terms[2].word, QString("user"));
|
||||||
|
|
||||||
QCOMPARE(terms[3]->field, EntrySearcher::Field::Password);
|
QCOMPARE(terms[3].field, EntrySearcher::Field::Password);
|
||||||
QCOMPARE(terms[3]->word, QString("test me"));
|
QCOMPARE(terms[3].word, QString("test me"));
|
||||||
|
|
||||||
QCOMPARE(terms[4]->field, EntrySearcher::Field::Undefined);
|
QCOMPARE(terms[4].field, EntrySearcher::Field::Undefined);
|
||||||
QCOMPARE(terms[4]->word, QString("noquote"));
|
QCOMPARE(terms[4].word, QString("noquote"));
|
||||||
|
|
||||||
// Test wildcard and regex search terms
|
// Test wildcard and regex search terms
|
||||||
m_entrySearcher.parseSearchTerms("+url:*.google.com *user:\\d+\\w{2}");
|
m_entrySearcher.parseSearchTerms("+url:*.google.com *user:\\d+\\w{2}");
|
||||||
|
@ -220,11 +220,11 @@ void TestEntrySearcher::testSearchTermParser()
|
||||||
|
|
||||||
QCOMPARE(terms.length(), 2);
|
QCOMPARE(terms.length(), 2);
|
||||||
|
|
||||||
QCOMPARE(terms[0]->field, EntrySearcher::Field::Url);
|
QCOMPARE(terms[0].field, EntrySearcher::Field::Url);
|
||||||
QCOMPARE(terms[0]->regex.pattern(), QString("^.*\\.google\\.com$"));
|
QCOMPARE(terms[0].regex.pattern(), QString("^.*\\.google\\.com$"));
|
||||||
|
|
||||||
QCOMPARE(terms[1]->field, EntrySearcher::Field::Username);
|
QCOMPARE(terms[1].field, EntrySearcher::Field::Username);
|
||||||
QCOMPARE(terms[1]->regex.pattern(), QString("\\d+\\w{2}"));
|
QCOMPARE(terms[1].regex.pattern(), QString("\\d+\\w{2}"));
|
||||||
|
|
||||||
// Test custom attribute search terms
|
// Test custom attribute search terms
|
||||||
m_entrySearcher.parseSearchTerms("+_abc:efg _def:\"ddd\"");
|
m_entrySearcher.parseSearchTerms("+_abc:efg _def:\"ddd\"");
|
||||||
|
@ -232,13 +232,13 @@ void TestEntrySearcher::testSearchTermParser()
|
||||||
|
|
||||||
QCOMPARE(terms.length(), 2);
|
QCOMPARE(terms.length(), 2);
|
||||||
|
|
||||||
QCOMPARE(terms[0]->field, EntrySearcher::Field::AttributeValue);
|
QCOMPARE(terms[0].field, EntrySearcher::Field::AttributeValue);
|
||||||
QCOMPARE(terms[0]->word, QString("abc"));
|
QCOMPARE(terms[0].word, QString("abc"));
|
||||||
QCOMPARE(terms[0]->regex.pattern(), QString("^efg$"));
|
QCOMPARE(terms[0].regex.pattern(), QString("^efg$"));
|
||||||
|
|
||||||
QCOMPARE(terms[1]->field, EntrySearcher::Field::AttributeValue);
|
QCOMPARE(terms[1].field, EntrySearcher::Field::AttributeValue);
|
||||||
QCOMPARE(terms[1]->word, QString("def"));
|
QCOMPARE(terms[1].word, QString("def"));
|
||||||
QCOMPARE(terms[1]->regex.pattern(), QString("ddd"));
|
QCOMPARE(terms[1].regex.pattern(), QString("ddd"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestEntrySearcher::testCustomAttributesAreSearched()
|
void TestEntrySearcher::testCustomAttributesAreSearched()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue