Skip to content

Commit 917c4cc

Browse files
authored
Merge pull request #1797 from keepassxreboot/feature/better-search
Implement advanced search
2 parents a5e7da6 + 3400769 commit 917c4cc

32 files changed

+1056
-206
lines changed
897 Bytes
Loading
1.19 KB
Loading
2.07 KB
Loading

src/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ set(keepassx_SOURCES
126126
gui/UnlockDatabaseWidget.cpp
127127
gui/UnlockDatabaseDialog.cpp
128128
gui/WelcomeWidget.cpp
129-
gui/widgets/ElidedLabel.cpp
130129
gui/csvImport/CsvImportWidget.cpp
131130
gui/csvImport/CsvImportWizard.cpp
132131
gui/csvImport/CsvParserModel.cpp
@@ -154,6 +153,8 @@ set(keepassx_SOURCES
154153
gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
155154
gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp
156155
gui/settings/SettingsWidget.cpp
156+
gui/widgets/ElidedLabel.cpp
157+
gui/widgets/PopupHelpWidget.cpp
157158
gui/wizard/NewDatabaseWizard.cpp
158159
gui/wizard/NewDatabaseWizardPage.cpp
159160
gui/wizard/NewDatabaseWizardPageMetaData.cpp

src/browser/BrowserService.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ bool BrowserService::openDatabase(bool triggerUnlock)
8888
}
8989

9090
if (triggerUnlock) {
91-
KEEPASSXC_MAIN_WINDOW->bringToFront();
91+
getMainWindow()->bringToFront();
9292
m_bringToFrontRequested = true;
9393
}
9494

@@ -390,7 +390,7 @@ QList<Entry*> BrowserService::searchEntries(Database* db, const QString& hostnam
390390
return entries;
391391
}
392392

393-
for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup, Qt::CaseInsensitive)) {
393+
for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup)) {
394394
QString entryUrl = entry->url();
395395
QUrl entryQUrl(entryUrl);
396396
QString entryScheme = entryQUrl.scheme();
@@ -901,7 +901,7 @@ void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget)
901901
{
902902
if (dbWidget) {
903903
if (m_bringToFrontRequested) {
904-
KEEPASSXC_MAIN_WINDOW->lower();
904+
getMainWindow()->lower();
905905
m_bringToFrontRequested = false;
906906
}
907907
emit databaseUnlocked();

src/core/EntrySearcher.cpp

Lines changed: 122 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,73 +19,154 @@
1919
#include "EntrySearcher.h"
2020

2121
#include "core/Group.h"
22+
#include "core/Tools.h"
2223

23-
QList<Entry*> EntrySearcher::search(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity)
24+
EntrySearcher::EntrySearcher(bool caseSensitive)
25+
: m_caseSensitive(caseSensitive)
26+
, m_termParser(R"re(([-!*+]+)?(?:(\w*):)?(?:(?=")"((?:[^"\\]|\\.)*)"|([^ ]*))( |$))re")
27+
// Group 1 = modifiers, Group 2 = field, Group 3 = quoted string, Group 4 = unquoted string
2428
{
25-
if (!group->resolveSearchingEnabled()) {
26-
return QList<Entry*>();
27-
}
28-
29-
return searchEntries(searchTerm, group, caseSensitivity);
3029
}
3130

32-
QList<Entry*>
33-
EntrySearcher::searchEntries(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity)
31+
QList<Entry*> EntrySearcher::search(const QString& searchString, const Group* baseGroup, bool forceSearch)
3432
{
35-
QList<Entry*> searchResult;
36-
37-
const QList<Entry*>& entryList = group->entries();
38-
for (Entry* entry : entryList) {
39-
searchResult.append(matchEntry(searchTerm, entry, caseSensitivity));
40-
}
33+
Q_ASSERT(baseGroup);
4134

42-
const QList<Group*>& children = group->children();
43-
for (Group* childGroup : children) {
44-
if (childGroup->searchingEnabled() != Group::Disable) {
45-
if (matchGroup(searchTerm, childGroup, caseSensitivity)) {
46-
searchResult.append(childGroup->entriesRecursive());
47-
} else {
48-
searchResult.append(searchEntries(searchTerm, childGroup, caseSensitivity));
49-
}
35+
QList<Entry*> results;
36+
for (const auto group : baseGroup->groupsRecursive(true)) {
37+
if (forceSearch || group->resolveSearchingEnabled()) {
38+
results.append(searchEntries(searchString, group->entries()));
5039
}
5140
}
5241

53-
return searchResult;
42+
return results;
5443
}
5544

56-
QList<Entry*> EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity)
45+
QList<Entry*> EntrySearcher::searchEntries(const QString& searchString, const QList<Entry*>& entries)
5746
{
58-
const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts);
59-
for (const QString& word : wordList) {
60-
if (!wordMatch(word, entry, caseSensitivity)) {
61-
return QList<Entry*>();
62-
}
47+
QList<Entry*> results;
48+
for (Entry* entry : entries) {
49+
if (searchEntryImpl(searchString, entry)) {
50+
results.append(entry);
51+
}
6352
}
53+
return results;
54+
}
6455

65-
return QList<Entry*>() << entry;
56+
void EntrySearcher::setCaseSensitive(bool state)
57+
{
58+
m_caseSensitive = state;
6659
}
6760

68-
bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity)
61+
bool EntrySearcher::isCaseSensitive()
6962
{
70-
return entry->resolvePlaceholder(entry->title()).contains(word, caseSensitivity)
71-
|| entry->resolvePlaceholder(entry->username()).contains(word, caseSensitivity)
72-
|| entry->resolvePlaceholder(entry->url()).contains(word, caseSensitivity)
73-
|| entry->resolvePlaceholder(entry->notes()).contains(word, caseSensitivity);
63+
return m_caseSensitive;
7464
}
7565

76-
bool EntrySearcher::matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity)
66+
bool EntrySearcher::searchEntryImpl(const QString& searchString, Entry* entry)
7767
{
78-
const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts);
79-
for (const QString& word : wordList) {
80-
if (!wordMatch(word, group, caseSensitivity)) {
68+
// Pre-load in case they are needed
69+
auto attributes = QStringList(entry->attributes()->keys());
70+
auto attachments = QStringList(entry->attachments()->keys());
71+
72+
bool found;
73+
auto searchTerms = parseSearchTerms(searchString);
74+
75+
for (const auto& term : searchTerms) {
76+
switch (term->field) {
77+
case Field::Title:
78+
found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch();
79+
break;
80+
case Field::Username:
81+
found = term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch();
82+
break;
83+
case Field::Password:
84+
found = term->regex.match(entry->resolvePlaceholder(entry->password())).hasMatch();
85+
break;
86+
case Field::Url:
87+
found = term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch();
88+
break;
89+
case Field::Notes:
90+
found = term->regex.match(entry->notes()).hasMatch();
91+
break;
92+
case Field::Attribute:
93+
found = !attributes.filter(term->regex).empty();
94+
break;
95+
case Field::Attachment:
96+
found = !attachments.filter(term->regex).empty();
97+
break;
98+
default:
99+
// Terms without a specific field try to match title, username, url, and notes
100+
found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch() ||
101+
term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch() ||
102+
term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch() ||
103+
term->regex.match(entry->notes()).hasMatch();
104+
}
105+
106+
// Short circuit if we failed to match or we matched and are excluding this term
107+
if ((!found && !term->exclude) || (found && term->exclude)) {
81108
return false;
82109
}
83110
}
84111

85112
return true;
86113
}
87114

88-
bool EntrySearcher::wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity)
115+
QList<QSharedPointer<EntrySearcher::SearchTerm> > EntrySearcher::parseSearchTerms(const QString& searchString)
89116
{
90-
return group->name().contains(word, caseSensitivity) || group->notes().contains(word, caseSensitivity);
117+
auto terms = QList<QSharedPointer<SearchTerm> >();
118+
119+
auto results = m_termParser.globalMatch(searchString);
120+
while (results.hasNext()) {
121+
auto result = results.next();
122+
auto term = QSharedPointer<SearchTerm>::create();
123+
124+
// Quoted string group
125+
term->word = result.captured(3);
126+
127+
// If empty, use the unquoted string group
128+
if (term->word.isEmpty()) {
129+
term->word = result.captured(4);
130+
}
131+
132+
// If still empty, ignore this match
133+
if (term->word.isEmpty()) {
134+
continue;
135+
}
136+
137+
auto mods = result.captured(1);
138+
139+
// Convert term to regex
140+
term->regex = Tools::convertToRegex(term->word, !mods.contains("*"), mods.contains("+"), m_caseSensitive);
141+
142+
// Exclude modifier
143+
term->exclude = mods.contains("-") || mods.contains("!");
144+
145+
// Determine the field to search
146+
QString field = result.captured(2);
147+
if (!field.isEmpty()) {
148+
auto cs = Qt::CaseInsensitive;
149+
if (field.compare("title", cs) == 0) {
150+
term->field = Field::Title;
151+
} else if (field.startsWith("user", cs)) {
152+
term->field = Field::Username;
153+
} else if (field.startsWith("pass", cs)) {
154+
term->field = Field::Password;
155+
} else if (field.compare("url", cs) == 0) {
156+
term->field = Field::Url;
157+
} else if (field.compare("notes", cs) == 0) {
158+
term->field = Field::Notes;
159+
} else if (field.startsWith("attr", cs)) {
160+
term->field = Field::Attribute;
161+
} else if (field.startsWith("attach", cs)) {
162+
term->field = Field::Attachment;
163+
} else {
164+
term->field = Field::Undefined;
165+
}
166+
}
167+
168+
terms.append(term);
169+
}
170+
171+
return terms;
91172
}

src/core/EntrySearcher.h

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,50 @@
2020
#define KEEPASSX_ENTRYSEARCHER_H
2121

2222
#include <QString>
23+
#include <QRegularExpression>
2324

2425
class Group;
2526
class Entry;
2627

2728
class EntrySearcher
2829
{
2930
public:
30-
QList<Entry*> search(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity);
31+
explicit EntrySearcher(bool caseSensitive = false);
32+
33+
QList<Entry*> search(const QString& searchString, const Group* baseGroup, bool forceSearch = false);
34+
QList<Entry*> searchEntries(const QString& searchString, const QList<Entry*>& entries);
35+
36+
void setCaseSensitive(bool state);
37+
bool isCaseSensitive();
3138

3239
private:
33-
QList<Entry*> searchEntries(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity);
34-
QList<Entry*> matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity);
35-
bool wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity);
36-
bool matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity);
37-
bool wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity);
40+
bool searchEntryImpl(const QString& searchString, Entry* entry);
41+
42+
enum class Field {
43+
Undefined,
44+
Title,
45+
Username,
46+
Password,
47+
Url,
48+
Notes,
49+
Attribute,
50+
Attachment
51+
};
52+
53+
struct SearchTerm
54+
{
55+
Field field;
56+
QString word;
57+
QRegularExpression regex;
58+
bool exclude;
59+
};
60+
61+
QList<QSharedPointer<SearchTerm> > parseSearchTerms(const QString& searchString);
62+
63+
bool m_caseSensitive;
64+
QRegularExpression m_termParser;
65+
66+
friend class TestEntrySearcher;
3867
};
3968

4069
#endif // KEEPASSX_ENTRYSEARCHER_H

src/core/Tools.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
#include <QImageReader>
2727
#include <QLocale>
2828
#include <QStringList>
29+
#include <QRegularExpression>
30+
2931
#include <QElapsedTimer>
3032

3133
#include <cctype>
@@ -199,4 +201,31 @@ void wait(int ms)
199201
}
200202
}
201203

204+
// Escape common regex symbols except for *, ?, and |
205+
auto regexEscape = QRegularExpression(R"re(([-[\]{}()+.,\\\/^$#]))re");
206+
207+
QRegularExpression convertToRegex(const QString& string, bool useWildcards, bool exactMatch, bool caseSensitive)
208+
{
209+
QString pattern = string;
210+
211+
// Wildcard support (*, ?, |)
212+
if (useWildcards) {
213+
pattern.replace(regexEscape, "\\\\1");
214+
pattern.replace("*", ".*");
215+
pattern.replace("?", ".");
216+
}
217+
218+
// Exact modifier
219+
if (exactMatch) {
220+
pattern = "^" + pattern + "$";
221+
}
222+
223+
auto regex = QRegularExpression(pattern);
224+
if (!caseSensitive) {
225+
regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
226+
}
227+
228+
return regex;
229+
}
230+
202231
} // namespace Tools

src/core/Tools.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <algorithm>
2828

2929
class QIODevice;
30+
class QRegularExpression;
3031

3132
namespace Tools
3233
{
@@ -38,6 +39,8 @@ bool isHex(const QByteArray& ba);
3839
bool isBase64(const QByteArray& ba);
3940
void sleep(int ms);
4041
void wait(int ms);
42+
QRegularExpression convertToRegex(const QString& string, bool useWildcards = false, bool exactMatch = false,
43+
bool caseSensitive = false);
4144

4245
template <typename RandomAccessIterator, typename T>
4346
RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value)

src/gui/Application.cpp

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ namespace
4949

5050
Application::Application(int& argc, char** argv)
5151
: QApplication(argc, argv)
52-
, m_mainWindow(nullptr)
5352
#ifdef Q_OS_UNIX
5453
, m_unixSignalNotifier(nullptr)
5554
#endif
@@ -143,16 +142,6 @@ Application::~Application()
143142
}
144143
}
145144

146-
QWidget* Application::mainWindow() const
147-
{
148-
return m_mainWindow;
149-
}
150-
151-
void Application::setMainWindow(QWidget* mainWindow)
152-
{
153-
m_mainWindow = mainWindow;
154-
}
155-
156145
bool Application::event(QEvent* event)
157146
{
158147
// Handle Apple QFileOpenEvent from finder (double click on .kdbx file)

0 commit comments

Comments
 (0)