Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement advanced search #1797

Merged
merged 6 commits into from
Nov 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ set(keepassx_SOURCES
gui/UnlockDatabaseWidget.cpp
gui/UnlockDatabaseDialog.cpp
gui/WelcomeWidget.cpp
gui/widgets/ElidedLabel.cpp
gui/csvImport/CsvImportWidget.cpp
gui/csvImport/CsvImportWizard.cpp
gui/csvImport/CsvParserModel.cpp
Expand Down Expand Up @@ -154,6 +153,8 @@ set(keepassx_SOURCES
gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp
gui/settings/SettingsWidget.cpp
gui/widgets/ElidedLabel.cpp
gui/widgets/PopupHelpWidget.cpp
gui/wizard/NewDatabaseWizard.cpp
gui/wizard/NewDatabaseWizardPage.cpp
gui/wizard/NewDatabaseWizardPageMetaData.cpp
Expand Down
6 changes: 3 additions & 3 deletions src/browser/BrowserService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ bool BrowserService::openDatabase(bool triggerUnlock)
}

if (triggerUnlock) {
KEEPASSXC_MAIN_WINDOW->bringToFront();
getMainWindow()->bringToFront();
m_bringToFrontRequested = true;
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -901,7 +901,7 @@ void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget)
{
if (dbWidget) {
if (m_bringToFrontRequested) {
KEEPASSXC_MAIN_WINDOW->lower();
getMainWindow()->lower();
m_bringToFrontRequested = false;
}
emit databaseUnlocked();
Expand Down
163 changes: 122 additions & 41 deletions src/core/EntrySearcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,73 +19,154 @@
#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
{
if (!group->resolveSearchingEnabled()) {
return QList<Entry*>();
}

return searchEntries(searchTerm, group, caseSensitivity);
}

QList<Entry*>
EntrySearcher::searchEntries(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity)
QList<Entry*> EntrySearcher::search(const QString& searchString, const Group* baseGroup, bool forceSearch)
{
QList<Entry*> searchResult;

const QList<Entry*>& entryList = group->entries();
for (Entry* entry : entryList) {
searchResult.append(matchEntry(searchTerm, entry, caseSensitivity));
}
Q_ASSERT(baseGroup);

const QList<Group*>& children = group->children();
for (Group* childGroup : children) {
if (childGroup->searchingEnabled() != Group::Disable) {
if (matchGroup(searchTerm, childGroup, caseSensitivity)) {
searchResult.append(childGroup->entriesRecursive());
} else {
searchResult.append(searchEntries(searchTerm, childGroup, caseSensitivity));
}
QList<Entry*> results;
for (const auto group : baseGroup->groupsRecursive(true)) {
if (forceSearch || group->resolveSearchingEnabled()) {
results.append(searchEntries(searchString, group->entries()));
}
}

return searchResult;
return results;
}

QList<Entry*> EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity)
QList<Entry*> EntrySearcher::searchEntries(const QString& searchString, const QList<Entry*>& entries)
{
const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts);
for (const QString& word : wordList) {
if (!wordMatch(word, entry, caseSensitivity)) {
return QList<Entry*>();
}
QList<Entry*> results;
for (Entry* entry : entries) {
if (searchEntryImpl(searchString, entry)) {
results.append(entry);
}
}
return results;
}

return QList<Entry*>() << entry;
void EntrySearcher::setCaseSensitive(bool state)
{
m_caseSensitive = state;
}

bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity)
bool EntrySearcher::isCaseSensitive()
{
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);
return m_caseSensitive;
}

bool EntrySearcher::matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity)
bool EntrySearcher::searchEntryImpl(const QString& searchString, Entry* entry)
{
const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts);
for (const QString& word : wordList) {
if (!wordMatch(word, group, caseSensitivity)) {
// 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:
// Terms without a specific field try to match title, username, url, and notes
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) || (found && term->exclude)) {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a continue instead? It is into a for and returning here will skip all next term in the for if the current is excluded

Copy link
Member Author

@droidmonkey droidmonkey Mar 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a short circuit because this function only returns true if all the terms match this particular entry (implicit AND of terms).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok make sense

}
}

return true;
}

bool EntrySearcher::wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity)
QList<QSharedPointer<EntrySearcher::SearchTerm> > EntrySearcher::parseSearchTerms(const QString& searchString)
{
return group->name().contains(word, caseSensitivity) || group->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("-") || 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;
}
41 changes: 35 additions & 6 deletions src/core/EntrySearcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,50 @@
#define KEEPASSX_ENTRYSEARCHER_H

#include <QString>
#include <QRegularExpression>

class Group;
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 Group* group, Qt::CaseSensitivity caseSensitivity);
QList<Entry*> matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity);
bool wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity);
bool matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity);
bool wordMatch(const QString& word, const Group* group, 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
29 changes: 29 additions & 0 deletions src/core/Tools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
#include <QImageReader>
#include <QLocale>
#include <QStringList>
#include <QRegularExpression>

#include <QElapsedTimer>

#include <cctype>
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions src/core/Tools.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <algorithm>

class QIODevice;
class QRegularExpression;

namespace Tools
{
Expand All @@ -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)
Expand Down
11 changes: 0 additions & 11 deletions src/gui/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ namespace

Application::Application(int& argc, char** argv)
: QApplication(argc, argv)
, m_mainWindow(nullptr)
#ifdef Q_OS_UNIX
, m_unixSignalNotifier(nullptr)
#endif
Expand Down Expand Up @@ -143,16 +142,6 @@ Application::~Application()
}
}

QWidget* Application::mainWindow() const
{
return m_mainWindow;
}

void Application::setMainWindow(QWidget* mainWindow)
{
m_mainWindow = mainWindow;
}

bool Application::event(QEvent* event)
{
// Handle Apple QFileOpenEvent from finder (double click on .kdbx file)
Expand Down
4 changes: 0 additions & 4 deletions src/gui/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ class Application : public QApplication

public:
Application(int& argc, char** argv);
QWidget* mainWindow() const;
~Application() override;
void setMainWindow(QWidget* mainWindow);

bool event(QEvent* event) override;
bool isAlreadyRunning() const;
Expand All @@ -60,8 +58,6 @@ private slots:
void socketReadyRead();

private:
QWidget* m_mainWindow;

#if defined(Q_OS_UNIX)
/**
* Register Unix signals such as SIGINT and SIGTERM for clean shutdown.
Expand Down
Loading