|
19 | 19 | #include "EntrySearcher.h" |
20 | 20 |
|
21 | 21 | #include "core/Group.h" |
| 22 | +#include "core/Tools.h" |
22 | 23 |
|
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 |
24 | 28 | { |
25 | | - if (!group->resolveSearchingEnabled()) { |
26 | | - return QList<Entry*>(); |
27 | | - } |
28 | | - |
29 | | - return searchEntries(searchTerm, group, caseSensitivity); |
30 | 29 | } |
31 | 30 |
|
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) |
34 | 32 | { |
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); |
41 | 34 |
|
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())); |
50 | 39 | } |
51 | 40 | } |
52 | 41 |
|
53 | | - return searchResult; |
| 42 | + return results; |
54 | 43 | } |
55 | 44 |
|
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) |
57 | 46 | { |
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 | + } |
63 | 52 | } |
| 53 | + return results; |
| 54 | +} |
64 | 55 |
|
65 | | - return QList<Entry*>() << entry; |
| 56 | +void EntrySearcher::setCaseSensitive(bool state) |
| 57 | +{ |
| 58 | + m_caseSensitive = state; |
66 | 59 | } |
67 | 60 |
|
68 | | -bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity) |
| 61 | +bool EntrySearcher::isCaseSensitive() |
69 | 62 | { |
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; |
74 | 64 | } |
75 | 65 |
|
76 | | -bool EntrySearcher::matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity) |
| 66 | +bool EntrySearcher::searchEntryImpl(const QString& searchString, Entry* entry) |
77 | 67 | { |
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)) { |
81 | 108 | return false; |
82 | 109 | } |
83 | 110 | } |
84 | 111 |
|
85 | 112 | return true; |
86 | 113 | } |
87 | 114 |
|
88 | | -bool EntrySearcher::wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity) |
| 115 | +QList<QSharedPointer<EntrySearcher::SearchTerm> > EntrySearcher::parseSearchTerms(const QString& searchString) |
89 | 116 | { |
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; |
91 | 172 | } |
0 commit comments