mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-23 22:39:43 -05:00
Add advanced search term parser
* Support quoted strings & per-field searching * Support regex and exact matching * Simplify search sequence * Make search widget larger * Add regex converter to Tools namespace
This commit is contained in:
parent
4b57fcb563
commit
4b983251cb
@ -390,7 +390,7 @@ QList<Entry*> BrowserService::searchEntries(Database* db, const QString& hostnam
|
||||
return entries;
|
||||
}
|
||||
|
||||
for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup, Qt::CaseInsensitive)) {
|
||||
for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup)) {
|
||||
QString entryUrl = entry->url();
|
||||
QUrl entryQUrl(entryUrl);
|
||||
QString entryScheme = entryQUrl.scheme();
|
||||
|
@ -19,42 +19,91 @@
|
||||
#include "EntrySearcher.h"
|
||||
|
||||
#include "core/Group.h"
|
||||
#include "core/Tools.h"
|
||||
|
||||
QList<Entry*> EntrySearcher::search(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity)
|
||||
EntrySearcher::EntrySearcher(bool caseSensitive)
|
||||
: m_caseSensitive(caseSensitive)
|
||||
, m_termParser(R"re(([-*+]+)?(?:(\w*):)?(?:(?=")"((?:[^"\\]|\\.)*)"|([^ ]*))( |$))re")
|
||||
// Group 1 = modifiers, Group 2 = field, Group 3 = quoted string, Group 4 = unquoted string
|
||||
{
|
||||
}
|
||||
|
||||
QList<Entry*> EntrySearcher::search(const QString& searchString, const Group* baseGroup, bool forceSearch)
|
||||
{
|
||||
Q_ASSERT(baseGroup);
|
||||
|
||||
QList<Entry*> results;
|
||||
|
||||
if (group->resolveSearchingEnabled()) {
|
||||
results.append(searchEntries(searchTerm, group->entries(), caseSensitivity));
|
||||
}
|
||||
|
||||
for (Group* childGroup : group->children()) {
|
||||
if (childGroup->resolveSearchingEnabled()) {
|
||||
results.append(searchEntries(searchTerm, childGroup->entries(), caseSensitivity));
|
||||
for (const auto group : baseGroup->groupsRecursive(true)) {
|
||||
if (forceSearch || group->resolveSearchingEnabled()) {
|
||||
results.append(searchEntries(searchString, group->entries()));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
QList<Entry*> EntrySearcher::searchEntries(const QString& searchTerm, const QList<Entry*>& entries,
|
||||
Qt::CaseSensitivity caseSensitivity)
|
||||
QList<Entry*> EntrySearcher::searchEntries(const QString& searchString, const QList<Entry*>& entries)
|
||||
{
|
||||
QList<Entry*> results;
|
||||
for (Entry* entry : entries) {
|
||||
if (matchEntry(searchTerm, entry, caseSensitivity)) {
|
||||
if (searchEntryImpl(searchString, entry)) {
|
||||
results.append(entry);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
bool EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry,
|
||||
Qt::CaseSensitivity caseSensitivity)
|
||||
void EntrySearcher::setCaseSensitive(bool state)
|
||||
{
|
||||
const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts);
|
||||
for (const QString& word : wordList) {
|
||||
if (!wordMatch(word, entry, caseSensitivity)) {
|
||||
m_caseSensitive = state;
|
||||
}
|
||||
|
||||
bool EntrySearcher::isCaseSensitive()
|
||||
{
|
||||
return m_caseSensitive;
|
||||
}
|
||||
|
||||
bool EntrySearcher::searchEntryImpl(const QString& searchString, Entry* entry)
|
||||
{
|
||||
// Pre-load in case they are needed
|
||||
auto attributes = QStringList(entry->attributes()->keys());
|
||||
auto attachments = QStringList(entry->attachments()->keys());
|
||||
|
||||
bool found;
|
||||
auto searchTerms = parseSearchTerms(searchString);
|
||||
|
||||
for (const auto& term : searchTerms) {
|
||||
switch (term->field) {
|
||||
case Field::Title:
|
||||
found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch();
|
||||
break;
|
||||
case Field::Username:
|
||||
found = term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch();
|
||||
break;
|
||||
case Field::Password:
|
||||
found = term->regex.match(entry->resolvePlaceholder(entry->password())).hasMatch();
|
||||
break;
|
||||
case Field::Url:
|
||||
found = term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch();
|
||||
break;
|
||||
case Field::Notes:
|
||||
found = term->regex.match(entry->notes()).hasMatch();
|
||||
break;
|
||||
case Field::Attribute:
|
||||
found = !attributes.filter(term->regex).empty();
|
||||
break;
|
||||
case Field::Attachment:
|
||||
found = !attachments.filter(term->regex).empty();
|
||||
break;
|
||||
default:
|
||||
found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch() ||
|
||||
term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch() ||
|
||||
term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch() ||
|
||||
term->regex.match(entry->notes()).hasMatch();
|
||||
}
|
||||
|
||||
// Short circuit if we failed to match or we matched and are excluding this term
|
||||
if (!found || term->exclude) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -62,10 +111,61 @@ bool EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity)
|
||||
QList<QSharedPointer<EntrySearcher::SearchTerm> > EntrySearcher::parseSearchTerms(const QString& searchString)
|
||||
{
|
||||
return entry->resolvePlaceholder(entry->title()).contains(word, caseSensitivity)
|
||||
|| entry->resolvePlaceholder(entry->username()).contains(word, caseSensitivity)
|
||||
|| entry->resolvePlaceholder(entry->url()).contains(word, caseSensitivity)
|
||||
|| entry->resolvePlaceholder(entry->notes()).contains(word, caseSensitivity);
|
||||
auto terms = QList<QSharedPointer<SearchTerm> >();
|
||||
|
||||
auto results = m_termParser.globalMatch(searchString);
|
||||
while (results.hasNext()) {
|
||||
auto result = results.next();
|
||||
auto term = QSharedPointer<SearchTerm>::create();
|
||||
|
||||
// Quoted string group
|
||||
term->word = result.captured(3);
|
||||
|
||||
// If empty, use the unquoted string group
|
||||
if (term->word.isEmpty()) {
|
||||
term->word = result.captured(4);
|
||||
}
|
||||
|
||||
// If still empty, ignore this match
|
||||
if (term->word.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto mods = result.captured(1);
|
||||
|
||||
// Convert term to regex
|
||||
term->regex = Tools::convertToRegex(term->word, !mods.contains("*"), mods.contains("+"), m_caseSensitive);
|
||||
|
||||
// Exclude modifier
|
||||
term->exclude = mods.contains("-");
|
||||
|
||||
// Determine the field to search
|
||||
QString field = result.captured(2);
|
||||
if (!field.isEmpty()) {
|
||||
auto cs = Qt::CaseInsensitive;
|
||||
if (field.compare("title", cs) == 0) {
|
||||
term->field = Field::Title;
|
||||
} else if (field.startsWith("user", cs)) {
|
||||
term->field = Field::Username;
|
||||
} else if (field.startsWith("pass", cs)) {
|
||||
term->field = Field::Password;
|
||||
} else if (field.compare("url", cs) == 0) {
|
||||
term->field = Field::Url;
|
||||
} else if (field.compare("notes", cs) == 0) {
|
||||
term->field = Field::Notes;
|
||||
} else if (field.startsWith("attr", cs)) {
|
||||
term->field = Field::Attribute;
|
||||
} else if (field.startsWith("attach", cs)) {
|
||||
term->field = Field::Attachment;
|
||||
} else {
|
||||
term->field = Field::Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
terms.append(term);
|
||||
}
|
||||
|
||||
return terms;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@
|
||||
#define KEEPASSX_ENTRYSEARCHER_H
|
||||
|
||||
#include <QString>
|
||||
#include <QRegularExpression>
|
||||
|
||||
class Group;
|
||||
class Entry;
|
||||
@ -27,12 +28,42 @@ class Entry;
|
||||
class EntrySearcher
|
||||
{
|
||||
public:
|
||||
QList<Entry*> search(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity);
|
||||
explicit EntrySearcher(bool caseSensitive = false);
|
||||
|
||||
QList<Entry*> search(const QString& searchString, const Group* baseGroup, bool forceSearch = false);
|
||||
QList<Entry*> searchEntries(const QString& searchString, const QList<Entry*>& entries);
|
||||
|
||||
void setCaseSensitive(bool state);
|
||||
bool isCaseSensitive();
|
||||
|
||||
private:
|
||||
QList<Entry*> searchEntries(const QString& searchTerm, const QList<Entry*>& entries, Qt::CaseSensitivity caseSensitivity);
|
||||
bool matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity);
|
||||
bool wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity);
|
||||
bool searchEntryImpl(const QString& searchString, Entry* entry);
|
||||
|
||||
enum class Field {
|
||||
Undefined,
|
||||
Title,
|
||||
Username,
|
||||
Password,
|
||||
Url,
|
||||
Notes,
|
||||
Attribute,
|
||||
Attachment
|
||||
};
|
||||
|
||||
struct SearchTerm
|
||||
{
|
||||
Field field;
|
||||
QString word;
|
||||
QRegularExpression regex;
|
||||
bool exclude;
|
||||
};
|
||||
|
||||
QList<QSharedPointer<SearchTerm> > parseSearchTerms(const QString& searchString);
|
||||
|
||||
bool m_caseSensitive;
|
||||
QRegularExpression m_termParser;
|
||||
|
||||
friend class TestEntrySearcher;
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_ENTRYSEARCHER_H
|
||||
|
@ -26,6 +26,8 @@
|
||||
#include <QImageReader>
|
||||
#include <QLocale>
|
||||
#include <QStringList>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <QElapsedTimer>
|
||||
|
||||
#include <cctype>
|
||||
@ -199,4 +201,31 @@ void wait(int ms)
|
||||
}
|
||||
}
|
||||
|
||||
// Escape common regex symbols except for *, ?, and |
|
||||
auto regexEscape = QRegularExpression(R"re(([-[\]{}()+.,\\\/^$#]))re");
|
||||
|
||||
QRegularExpression convertToRegex(const QString& string, bool useWildcards, bool exactMatch, bool caseSensitive)
|
||||
{
|
||||
QString pattern = string;
|
||||
|
||||
// Wildcard support (*, ?, |)
|
||||
if (useWildcards) {
|
||||
pattern.replace(regexEscape, "\\\\1");
|
||||
pattern.replace("*", ".*");
|
||||
pattern.replace("?", ".");
|
||||
}
|
||||
|
||||
// Exact modifier
|
||||
if (exactMatch) {
|
||||
pattern = "^" + pattern + "$";
|
||||
}
|
||||
|
||||
auto regex = QRegularExpression(pattern);
|
||||
if (!caseSensitive) {
|
||||
regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
|
||||
return regex;
|
||||
}
|
||||
|
||||
} // namespace Tools
|
||||
|
@ -27,6 +27,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
class QIODevice;
|
||||
class QRegularExpression;
|
||||
|
||||
namespace Tools
|
||||
{
|
||||
@ -38,6 +39,8 @@ bool isHex(const QByteArray& ba);
|
||||
bool isBase64(const QByteArray& ba);
|
||||
void sleep(int ms);
|
||||
void wait(int ms);
|
||||
QRegularExpression convertToRegex(const QString& string, bool useWildcards = false, bool exactMatch = false,
|
||||
bool caseSensitive = false);
|
||||
|
||||
template <typename RandomAccessIterator, typename T>
|
||||
RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value)
|
||||
|
@ -209,7 +209,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
|
||||
m_fileWatchUnblockTimer.setSingleShot(true);
|
||||
m_ignoreAutoReload = false;
|
||||
|
||||
m_searchCaseSensitive = false;
|
||||
m_EntrySearcher = new EntrySearcher(false);
|
||||
m_searchLimitGroup = config()->get("SearchLimitGroup", false).toBool();
|
||||
|
||||
#ifdef WITH_XC_SSHAGENT
|
||||
@ -227,6 +227,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
|
||||
|
||||
DatabaseWidget::~DatabaseWidget()
|
||||
{
|
||||
delete m_EntrySearcher;
|
||||
}
|
||||
|
||||
DatabaseWidget::Mode DatabaseWidget::currentMode() const
|
||||
@ -1012,17 +1013,15 @@ void DatabaseWidget::search(const QString& searchtext)
|
||||
|
||||
emit searchModeAboutToActivate();
|
||||
|
||||
Qt::CaseSensitivity caseSensitive = m_searchCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive;
|
||||
|
||||
Group* searchGroup = m_searchLimitGroup ? currentGroup() : m_db->rootGroup();
|
||||
|
||||
QList<Entry*> searchResult = EntrySearcher().search(searchtext, searchGroup, caseSensitive);
|
||||
QList<Entry*> searchResult = m_EntrySearcher->search(searchtext, searchGroup);
|
||||
|
||||
m_entryView->displaySearch(searchResult);
|
||||
m_lastSearchText = searchtext;
|
||||
|
||||
// Display a label detailing our search results
|
||||
if (searchResult.size() > 0) {
|
||||
if (!searchResult.isEmpty()) {
|
||||
m_searchingLabel->setText(tr("Search Results (%1)").arg(searchResult.size()));
|
||||
} else {
|
||||
m_searchingLabel->setText(tr("No Results"));
|
||||
@ -1035,7 +1034,7 @@ void DatabaseWidget::search(const QString& searchtext)
|
||||
|
||||
void DatabaseWidget::setSearchCaseSensitive(bool state)
|
||||
{
|
||||
m_searchCaseSensitive = state;
|
||||
m_EntrySearcher->setCaseSensitive(state);
|
||||
refreshSearch();
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,7 @@ class EditEntryWidget;
|
||||
class EditGroupWidget;
|
||||
class Entry;
|
||||
class EntryView;
|
||||
class EntrySearcher;
|
||||
class Group;
|
||||
class GroupView;
|
||||
class KeePass1OpenWidget;
|
||||
@ -246,8 +247,8 @@ private:
|
||||
QString m_databaseFileName;
|
||||
|
||||
// Search state
|
||||
EntrySearcher* m_EntrySearcher;
|
||||
QString m_lastSearchText;
|
||||
bool m_searchCaseSensitive;
|
||||
bool m_searchLimitGroup;
|
||||
|
||||
// CSV import state
|
||||
|
@ -137,7 +137,7 @@ void SearchWidget::startSearchTimer()
|
||||
if (!m_searchTimer->isActive()) {
|
||||
m_searchTimer->stop();
|
||||
}
|
||||
m_searchTimer->start(100);
|
||||
m_searchTimer->start(300);
|
||||
}
|
||||
|
||||
void SearchWidget::startSearch()
|
||||
|
@ -34,6 +34,9 @@
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>30</width>
|
||||
@ -44,6 +47,18 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="searchEdit">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">padding:3px</string>
|
||||
</property>
|
||||
|
@ -58,9 +58,9 @@ EntryView::EntryView(QWidget* parent)
|
||||
m_headerMenu->setTitle(tr("Customize View"));
|
||||
m_headerMenu->addSection(tr("Customize View"));
|
||||
|
||||
m_hideUsernamesAction = m_headerMenu->addAction(tr("Hide Usernames"), m_model, SLOT(setUsernamesHidden(bool)));
|
||||
m_hideUsernamesAction = m_headerMenu->addAction(tr("Hide Usernames"), this, SLOT(setUsernamesHidden(bool)));
|
||||
m_hideUsernamesAction->setCheckable(true);
|
||||
m_hidePasswordsAction = m_headerMenu->addAction(tr("Hide Passwords"), m_model, SLOT(setPasswordsHidden(bool)));
|
||||
m_hidePasswordsAction = m_headerMenu->addAction(tr("Hide Passwords"), this, SLOT(setPasswordsHidden(bool)));
|
||||
m_hidePasswordsAction->setCheckable(true);
|
||||
m_headerMenu->addSeparator();
|
||||
|
||||
|
@ -43,9 +43,7 @@ public:
|
||||
int numberOfSelectedEntries();
|
||||
void setFirstEntryActive();
|
||||
bool isUsernamesHidden() const;
|
||||
void setUsernamesHidden(bool hide);
|
||||
bool isPasswordsHidden() const;
|
||||
void setPasswordsHidden(bool hide);
|
||||
QByteArray viewState() const;
|
||||
bool setViewState(const QByteArray& state);
|
||||
|
||||
@ -57,6 +55,10 @@ signals:
|
||||
void entrySelectionChanged();
|
||||
void viewStateChanged();
|
||||
|
||||
public slots:
|
||||
void setUsernamesHidden(bool hide);
|
||||
void setPasswordsHidden(bool hide);
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void focusInEvent(QFocusEvent* event) override;
|
||||
|
@ -22,23 +22,32 @@ QTEST_GUILESS_MAIN(TestEntrySearcher)
|
||||
|
||||
void TestEntrySearcher::initTestCase()
|
||||
{
|
||||
m_groupRoot = new Group();
|
||||
m_rootGroup = new Group();
|
||||
}
|
||||
|
||||
void TestEntrySearcher::cleanupTestCase()
|
||||
{
|
||||
delete m_groupRoot;
|
||||
delete m_rootGroup;
|
||||
}
|
||||
|
||||
void TestEntrySearcher::testSearch()
|
||||
{
|
||||
/**
|
||||
* Root
|
||||
* - group1 (search disabled)
|
||||
* - group11
|
||||
* - group2
|
||||
* - group21
|
||||
* - group211
|
||||
* - group2111
|
||||
*/
|
||||
Group* group1 = new Group();
|
||||
Group* group2 = new Group();
|
||||
Group* group3 = new Group();
|
||||
|
||||
group1->setParent(m_groupRoot);
|
||||
group2->setParent(m_groupRoot);
|
||||
group3->setParent(m_groupRoot);
|
||||
group1->setParent(m_rootGroup);
|
||||
group2->setParent(m_rootGroup);
|
||||
group3->setParent(m_rootGroup);
|
||||
|
||||
Group* group11 = new Group();
|
||||
|
||||
@ -55,15 +64,15 @@ void TestEntrySearcher::testSearch()
|
||||
group1->setSearchingEnabled(Group::Disable);
|
||||
|
||||
Entry* eRoot = new Entry();
|
||||
eRoot->setNotes("test search term test");
|
||||
eRoot->setGroup(m_groupRoot);
|
||||
eRoot->setTitle("test search term test");
|
||||
eRoot->setGroup(m_rootGroup);
|
||||
|
||||
Entry* eRoot2 = new Entry();
|
||||
eRoot2->setNotes("test term test");
|
||||
eRoot2->setGroup(m_groupRoot);
|
||||
eRoot2->setGroup(m_rootGroup);
|
||||
|
||||
Entry* e1 = new Entry();
|
||||
e1->setNotes("test search term test");
|
||||
e1->setUsername("test search term test");
|
||||
e1->setGroup(group1);
|
||||
|
||||
Entry* e11 = new Entry();
|
||||
@ -71,29 +80,37 @@ void TestEntrySearcher::testSearch()
|
||||
e11->setGroup(group11);
|
||||
|
||||
Entry* e2111 = new Entry();
|
||||
e2111->setNotes("test search term test");
|
||||
e2111->setTitle("test search term test");
|
||||
e2111->setGroup(group2111);
|
||||
|
||||
Entry* e2111b = new Entry();
|
||||
e2111b->setNotes("test search test");
|
||||
e2111b->setPassword("testpass");
|
||||
e2111b->setGroup(group2111);
|
||||
|
||||
Entry* e3 = new Entry();
|
||||
e3->setNotes("test search term test");
|
||||
e3->setUrl("test search term test");
|
||||
e3->setGroup(group3);
|
||||
|
||||
Entry* e3b = new Entry();
|
||||
e3b->setNotes("test search test");
|
||||
e3b->setTitle("test search test");
|
||||
e3b->setPassword("realpass");
|
||||
e3b->setGroup(group3);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("search term", m_groupRoot, Qt::CaseInsensitive);
|
||||
QCOMPARE(m_searchResult.count(), 2);
|
||||
m_searchResult = m_entrySearcher.search("search", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 5);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("search term", group211, Qt::CaseInsensitive);
|
||||
m_searchResult = m_entrySearcher.search("search term", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 3);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("search term", group211);
|
||||
QCOMPARE(m_searchResult.count(), 1);
|
||||
|
||||
// Parent group disabled search
|
||||
m_searchResult = m_entrySearcher.search("search term", group11, Qt::CaseInsensitive);
|
||||
m_searchResult = m_entrySearcher.search("password:testpass", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 1);
|
||||
|
||||
// Parent group has search disabled
|
||||
m_searchResult = m_entrySearcher.search("search term", group11);
|
||||
QCOMPARE(m_searchResult.count(), 0);
|
||||
}
|
||||
|
||||
@ -102,38 +119,74 @@ void TestEntrySearcher::testAndConcatenationInSearch()
|
||||
Entry* entry = new Entry();
|
||||
entry->setNotes("abc def ghi");
|
||||
entry->setTitle("jkl");
|
||||
entry->setGroup(m_groupRoot);
|
||||
entry->setGroup(m_rootGroup);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("", m_groupRoot, Qt::CaseInsensitive);
|
||||
m_searchResult = m_entrySearcher.search("", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 1);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("def", m_groupRoot, Qt::CaseInsensitive);
|
||||
m_searchResult = m_entrySearcher.search("def", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 1);
|
||||
|
||||
m_searchResult = m_entrySearcher.search(" abc ghi ", m_groupRoot, Qt::CaseInsensitive);
|
||||
m_searchResult = m_entrySearcher.search(" abc ghi ", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 1);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("ghi ef", m_groupRoot, Qt::CaseInsensitive);
|
||||
m_searchResult = m_entrySearcher.search("ghi ef", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 1);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("abc ef xyz", m_groupRoot, Qt::CaseInsensitive);
|
||||
m_searchResult = m_entrySearcher.search("abc ef xyz", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 0);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("abc kl", m_groupRoot, Qt::CaseInsensitive);
|
||||
m_searchResult = m_entrySearcher.search("abc kl", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 1);
|
||||
}
|
||||
|
||||
void TestEntrySearcher::testAllAttributesAreSearched()
|
||||
{
|
||||
Entry* entry = new Entry();
|
||||
entry->setGroup(m_groupRoot);
|
||||
entry->setGroup(m_rootGroup);
|
||||
|
||||
entry->setTitle("testTitle");
|
||||
entry->setUsername("testUsername");
|
||||
entry->setUrl("testUrl");
|
||||
entry->setNotes("testNote");
|
||||
|
||||
m_searchResult =
|
||||
m_entrySearcher.search("testTitle testUsername testUrl testNote", m_groupRoot, Qt::CaseInsensitive);
|
||||
// Default is to AND all terms together
|
||||
m_searchResult = m_entrySearcher.search("testTitle testUsername testUrl testNote", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 1);
|
||||
}
|
||||
|
||||
void TestEntrySearcher::testSearchTermParser()
|
||||
{
|
||||
// Test standard search terms
|
||||
auto terms = m_entrySearcher.parseSearchTerms("-test \"quoted \\\"string\\\"\" user:user pass:\"test me\" noquote ");
|
||||
|
||||
QCOMPARE(terms.length(), 5);
|
||||
|
||||
QCOMPARE(terms[0]->field, EntrySearcher::Field::Undefined);
|
||||
QCOMPARE(terms[0]->word, QString("test"));
|
||||
QCOMPARE(terms[0]->exclude, true);
|
||||
|
||||
QCOMPARE(terms[1]->field, EntrySearcher::Field::Undefined);
|
||||
QCOMPARE(terms[1]->word, QString("quoted \\\"string\\\""));
|
||||
QCOMPARE(terms[1]->exclude, false);
|
||||
|
||||
QCOMPARE(terms[2]->field, EntrySearcher::Field::Username);
|
||||
QCOMPARE(terms[2]->word, QString("user"));
|
||||
|
||||
QCOMPARE(terms[3]->field, EntrySearcher::Field::Password);
|
||||
QCOMPARE(terms[3]->word, QString("test me"));
|
||||
|
||||
QCOMPARE(terms[4]->field, EntrySearcher::Field::Undefined);
|
||||
QCOMPARE(terms[4]->word, QString("noquote"));
|
||||
|
||||
// Test wildcard and regex search terms
|
||||
terms = m_entrySearcher.parseSearchTerms("+url:*.google.com *user:\\d+\\w{2}");
|
||||
|
||||
QCOMPARE(terms.length(), 2);
|
||||
|
||||
QCOMPARE(terms[0]->field, EntrySearcher::Field::Url);
|
||||
QCOMPARE(terms[0]->regex.pattern(), QString("^.*\\.google\\.com$"));
|
||||
|
||||
QCOMPARE(terms[1]->field, EntrySearcher::Field::Username);
|
||||
QCOMPARE(terms[1]->regex.pattern(), QString("\\d+\\w{2}"));
|
||||
}
|
||||
|
@ -34,9 +34,10 @@ private slots:
|
||||
void testAndConcatenationInSearch();
|
||||
void testSearch();
|
||||
void testAllAttributesAreSearched();
|
||||
void testSearchTermParser();
|
||||
|
||||
private:
|
||||
Group* m_groupRoot;
|
||||
Group* m_rootGroup;
|
||||
EntrySearcher m_entrySearcher;
|
||||
QList<Entry*> m_searchResult;
|
||||
};
|
||||
|
@ -841,9 +841,10 @@ void TestGui::testSearch()
|
||||
// Ensure Down focuses on entry view when search text is selected
|
||||
QTest::keyClick(searchTextEdit, Qt::Key_Down);
|
||||
QTRY_VERIFY(entryView->hasFocus());
|
||||
QCOMPARE(entryView->selectionModel()->currentIndex().row(), 0);
|
||||
// Test that password copies (entry has focus)
|
||||
QClipboard* clipboard = QApplication::clipboard();
|
||||
QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
|
||||
QTest::keyClick(entryView, Qt::Key_C, Qt::ControlModifier);
|
||||
QModelIndex searchedItem = entryView->model()->index(0, 1);
|
||||
Entry* searchedEntry = entryView->entryFromIndex(searchedItem);
|
||||
QTRY_COMPARE(searchedEntry->password(), clipboard->text());
|
||||
|
Loading…
Reference in New Issue
Block a user