mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-24 06:49:46 -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;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup, Qt::CaseInsensitive)) {
|
for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup)) {
|
||||||
QString entryUrl = entry->url();
|
QString entryUrl = entry->url();
|
||||||
QUrl entryQUrl(entryUrl);
|
QUrl entryQUrl(entryUrl);
|
||||||
QString entryScheme = entryQUrl.scheme();
|
QString entryScheme = entryQUrl.scheme();
|
||||||
|
@ -19,42 +19,91 @@
|
|||||||
#include "EntrySearcher.h"
|
#include "EntrySearcher.h"
|
||||||
|
|
||||||
#include "core/Group.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;
|
QList<Entry*> results;
|
||||||
|
for (const auto group : baseGroup->groupsRecursive(true)) {
|
||||||
if (group->resolveSearchingEnabled()) {
|
if (forceSearch || group->resolveSearchingEnabled()) {
|
||||||
results.append(searchEntries(searchTerm, group->entries(), caseSensitivity));
|
results.append(searchEntries(searchString, group->entries()));
|
||||||
}
|
|
||||||
|
|
||||||
for (Group* childGroup : group->children()) {
|
|
||||||
if (childGroup->resolveSearchingEnabled()) {
|
|
||||||
results.append(searchEntries(searchTerm, childGroup->entries(), caseSensitivity));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<Entry*> EntrySearcher::searchEntries(const QString& searchTerm, const QList<Entry*>& entries,
|
QList<Entry*> EntrySearcher::searchEntries(const QString& searchString, const QList<Entry*>& entries)
|
||||||
Qt::CaseSensitivity caseSensitivity)
|
|
||||||
{
|
{
|
||||||
QList<Entry*> results;
|
QList<Entry*> results;
|
||||||
for (Entry* entry : entries) {
|
for (Entry* entry : entries) {
|
||||||
if (matchEntry(searchTerm, entry, caseSensitivity)) {
|
if (searchEntryImpl(searchString, entry)) {
|
||||||
results.append(entry);
|
results.append(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry,
|
void EntrySearcher::setCaseSensitive(bool state)
|
||||||
Qt::CaseSensitivity caseSensitivity)
|
|
||||||
{
|
{
|
||||||
const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts);
|
m_caseSensitive = state;
|
||||||
for (const QString& word : wordList) {
|
}
|
||||||
if (!wordMatch(word, entry, caseSensitivity)) {
|
|
||||||
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,10 +111,61 @@ bool EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry,
|
|||||||
return true;
|
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)
|
auto terms = QList<QSharedPointer<SearchTerm> >();
|
||||||
|| entry->resolvePlaceholder(entry->username()).contains(word, caseSensitivity)
|
|
||||||
|| entry->resolvePlaceholder(entry->url()).contains(word, caseSensitivity)
|
auto results = m_termParser.globalMatch(searchString);
|
||||||
|| entry->resolvePlaceholder(entry->notes()).contains(word, caseSensitivity);
|
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
|
#define KEEPASSX_ENTRYSEARCHER_H
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
class Group;
|
class Group;
|
||||||
class Entry;
|
class Entry;
|
||||||
@ -27,12 +28,42 @@ class Entry;
|
|||||||
class EntrySearcher
|
class EntrySearcher
|
||||||
{
|
{
|
||||||
public:
|
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:
|
private:
|
||||||
QList<Entry*> searchEntries(const QString& searchTerm, const QList<Entry*>& entries, Qt::CaseSensitivity caseSensitivity);
|
bool searchEntryImpl(const QString& searchString, Entry* entry);
|
||||||
bool matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity);
|
|
||||||
bool wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity);
|
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
|
#endif // KEEPASSX_ENTRYSEARCHER_H
|
||||||
|
@ -26,6 +26,8 @@
|
|||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
|
|
||||||
#include <cctype>
|
#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
|
} // namespace Tools
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
class QIODevice;
|
class QIODevice;
|
||||||
|
class QRegularExpression;
|
||||||
|
|
||||||
namespace Tools
|
namespace Tools
|
||||||
{
|
{
|
||||||
@ -38,6 +39,8 @@ bool isHex(const QByteArray& ba);
|
|||||||
bool isBase64(const QByteArray& ba);
|
bool isBase64(const QByteArray& ba);
|
||||||
void sleep(int ms);
|
void sleep(int ms);
|
||||||
void wait(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>
|
template <typename RandomAccessIterator, typename T>
|
||||||
RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value)
|
RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value)
|
||||||
|
@ -209,7 +209,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
|
|||||||
m_fileWatchUnblockTimer.setSingleShot(true);
|
m_fileWatchUnblockTimer.setSingleShot(true);
|
||||||
m_ignoreAutoReload = false;
|
m_ignoreAutoReload = false;
|
||||||
|
|
||||||
m_searchCaseSensitive = false;
|
m_EntrySearcher = new EntrySearcher(false);
|
||||||
m_searchLimitGroup = config()->get("SearchLimitGroup", false).toBool();
|
m_searchLimitGroup = config()->get("SearchLimitGroup", false).toBool();
|
||||||
|
|
||||||
#ifdef WITH_XC_SSHAGENT
|
#ifdef WITH_XC_SSHAGENT
|
||||||
@ -227,6 +227,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
|
|||||||
|
|
||||||
DatabaseWidget::~DatabaseWidget()
|
DatabaseWidget::~DatabaseWidget()
|
||||||
{
|
{
|
||||||
|
delete m_EntrySearcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
DatabaseWidget::Mode DatabaseWidget::currentMode() const
|
DatabaseWidget::Mode DatabaseWidget::currentMode() const
|
||||||
@ -1012,17 +1013,15 @@ void DatabaseWidget::search(const QString& searchtext)
|
|||||||
|
|
||||||
emit searchModeAboutToActivate();
|
emit searchModeAboutToActivate();
|
||||||
|
|
||||||
Qt::CaseSensitivity caseSensitive = m_searchCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive;
|
|
||||||
|
|
||||||
Group* searchGroup = m_searchLimitGroup ? currentGroup() : m_db->rootGroup();
|
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_entryView->displaySearch(searchResult);
|
||||||
m_lastSearchText = searchtext;
|
m_lastSearchText = searchtext;
|
||||||
|
|
||||||
// Display a label detailing our search results
|
// Display a label detailing our search results
|
||||||
if (searchResult.size() > 0) {
|
if (!searchResult.isEmpty()) {
|
||||||
m_searchingLabel->setText(tr("Search Results (%1)").arg(searchResult.size()));
|
m_searchingLabel->setText(tr("Search Results (%1)").arg(searchResult.size()));
|
||||||
} else {
|
} else {
|
||||||
m_searchingLabel->setText(tr("No Results"));
|
m_searchingLabel->setText(tr("No Results"));
|
||||||
@ -1035,7 +1034,7 @@ void DatabaseWidget::search(const QString& searchtext)
|
|||||||
|
|
||||||
void DatabaseWidget::setSearchCaseSensitive(bool state)
|
void DatabaseWidget::setSearchCaseSensitive(bool state)
|
||||||
{
|
{
|
||||||
m_searchCaseSensitive = state;
|
m_EntrySearcher->setCaseSensitive(state);
|
||||||
refreshSearch();
|
refreshSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ class EditEntryWidget;
|
|||||||
class EditGroupWidget;
|
class EditGroupWidget;
|
||||||
class Entry;
|
class Entry;
|
||||||
class EntryView;
|
class EntryView;
|
||||||
|
class EntrySearcher;
|
||||||
class Group;
|
class Group;
|
||||||
class GroupView;
|
class GroupView;
|
||||||
class KeePass1OpenWidget;
|
class KeePass1OpenWidget;
|
||||||
@ -246,8 +247,8 @@ private:
|
|||||||
QString m_databaseFileName;
|
QString m_databaseFileName;
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
|
EntrySearcher* m_EntrySearcher;
|
||||||
QString m_lastSearchText;
|
QString m_lastSearchText;
|
||||||
bool m_searchCaseSensitive;
|
|
||||||
bool m_searchLimitGroup;
|
bool m_searchLimitGroup;
|
||||||
|
|
||||||
// CSV import state
|
// CSV import state
|
||||||
|
@ -137,7 +137,7 @@ void SearchWidget::startSearchTimer()
|
|||||||
if (!m_searchTimer->isActive()) {
|
if (!m_searchTimer->isActive()) {
|
||||||
m_searchTimer->stop();
|
m_searchTimer->stop();
|
||||||
}
|
}
|
||||||
m_searchTimer->start(100);
|
m_searchTimer->start(300);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SearchWidget::startSearch()
|
void SearchWidget::startSearch()
|
||||||
|
@ -34,6 +34,9 @@
|
|||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="sizeType">
|
||||||
|
<enum>QSizePolicy::Minimum</enum>
|
||||||
|
</property>
|
||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>30</width>
|
<width>30</width>
|
||||||
@ -44,6 +47,18 @@
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLineEdit" name="searchEdit">
|
<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">
|
<property name="styleSheet">
|
||||||
<string notr="true">padding:3px</string>
|
<string notr="true">padding:3px</string>
|
||||||
</property>
|
</property>
|
||||||
|
@ -58,9 +58,9 @@ EntryView::EntryView(QWidget* parent)
|
|||||||
m_headerMenu->setTitle(tr("Customize View"));
|
m_headerMenu->setTitle(tr("Customize View"));
|
||||||
m_headerMenu->addSection(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_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_hidePasswordsAction->setCheckable(true);
|
||||||
m_headerMenu->addSeparator();
|
m_headerMenu->addSeparator();
|
||||||
|
|
||||||
|
@ -43,9 +43,7 @@ public:
|
|||||||
int numberOfSelectedEntries();
|
int numberOfSelectedEntries();
|
||||||
void setFirstEntryActive();
|
void setFirstEntryActive();
|
||||||
bool isUsernamesHidden() const;
|
bool isUsernamesHidden() const;
|
||||||
void setUsernamesHidden(bool hide);
|
|
||||||
bool isPasswordsHidden() const;
|
bool isPasswordsHidden() const;
|
||||||
void setPasswordsHidden(bool hide);
|
|
||||||
QByteArray viewState() const;
|
QByteArray viewState() const;
|
||||||
bool setViewState(const QByteArray& state);
|
bool setViewState(const QByteArray& state);
|
||||||
|
|
||||||
@ -57,6 +55,10 @@ signals:
|
|||||||
void entrySelectionChanged();
|
void entrySelectionChanged();
|
||||||
void viewStateChanged();
|
void viewStateChanged();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void setUsernamesHidden(bool hide);
|
||||||
|
void setPasswordsHidden(bool hide);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void keyPressEvent(QKeyEvent* event) override;
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
void focusInEvent(QFocusEvent* event) override;
|
void focusInEvent(QFocusEvent* event) override;
|
||||||
|
@ -22,23 +22,32 @@ QTEST_GUILESS_MAIN(TestEntrySearcher)
|
|||||||
|
|
||||||
void TestEntrySearcher::initTestCase()
|
void TestEntrySearcher::initTestCase()
|
||||||
{
|
{
|
||||||
m_groupRoot = new Group();
|
m_rootGroup = new Group();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestEntrySearcher::cleanupTestCase()
|
void TestEntrySearcher::cleanupTestCase()
|
||||||
{
|
{
|
||||||
delete m_groupRoot;
|
delete m_rootGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestEntrySearcher::testSearch()
|
void TestEntrySearcher::testSearch()
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Root
|
||||||
|
* - group1 (search disabled)
|
||||||
|
* - group11
|
||||||
|
* - group2
|
||||||
|
* - group21
|
||||||
|
* - group211
|
||||||
|
* - group2111
|
||||||
|
*/
|
||||||
Group* group1 = new Group();
|
Group* group1 = new Group();
|
||||||
Group* group2 = new Group();
|
Group* group2 = new Group();
|
||||||
Group* group3 = new Group();
|
Group* group3 = new Group();
|
||||||
|
|
||||||
group1->setParent(m_groupRoot);
|
group1->setParent(m_rootGroup);
|
||||||
group2->setParent(m_groupRoot);
|
group2->setParent(m_rootGroup);
|
||||||
group3->setParent(m_groupRoot);
|
group3->setParent(m_rootGroup);
|
||||||
|
|
||||||
Group* group11 = new Group();
|
Group* group11 = new Group();
|
||||||
|
|
||||||
@ -55,15 +64,15 @@ void TestEntrySearcher::testSearch()
|
|||||||
group1->setSearchingEnabled(Group::Disable);
|
group1->setSearchingEnabled(Group::Disable);
|
||||||
|
|
||||||
Entry* eRoot = new Entry();
|
Entry* eRoot = new Entry();
|
||||||
eRoot->setNotes("test search term test");
|
eRoot->setTitle("test search term test");
|
||||||
eRoot->setGroup(m_groupRoot);
|
eRoot->setGroup(m_rootGroup);
|
||||||
|
|
||||||
Entry* eRoot2 = new Entry();
|
Entry* eRoot2 = new Entry();
|
||||||
eRoot2->setNotes("test term test");
|
eRoot2->setNotes("test term test");
|
||||||
eRoot2->setGroup(m_groupRoot);
|
eRoot2->setGroup(m_rootGroup);
|
||||||
|
|
||||||
Entry* e1 = new Entry();
|
Entry* e1 = new Entry();
|
||||||
e1->setNotes("test search term test");
|
e1->setUsername("test search term test");
|
||||||
e1->setGroup(group1);
|
e1->setGroup(group1);
|
||||||
|
|
||||||
Entry* e11 = new Entry();
|
Entry* e11 = new Entry();
|
||||||
@ -71,29 +80,37 @@ void TestEntrySearcher::testSearch()
|
|||||||
e11->setGroup(group11);
|
e11->setGroup(group11);
|
||||||
|
|
||||||
Entry* e2111 = new Entry();
|
Entry* e2111 = new Entry();
|
||||||
e2111->setNotes("test search term test");
|
e2111->setTitle("test search term test");
|
||||||
e2111->setGroup(group2111);
|
e2111->setGroup(group2111);
|
||||||
|
|
||||||
Entry* e2111b = new Entry();
|
Entry* e2111b = new Entry();
|
||||||
e2111b->setNotes("test search test");
|
e2111b->setNotes("test search test");
|
||||||
|
e2111b->setPassword("testpass");
|
||||||
e2111b->setGroup(group2111);
|
e2111b->setGroup(group2111);
|
||||||
|
|
||||||
Entry* e3 = new Entry();
|
Entry* e3 = new Entry();
|
||||||
e3->setNotes("test search term test");
|
e3->setUrl("test search term test");
|
||||||
e3->setGroup(group3);
|
e3->setGroup(group3);
|
||||||
|
|
||||||
Entry* e3b = new Entry();
|
Entry* e3b = new Entry();
|
||||||
e3b->setNotes("test search test");
|
e3b->setTitle("test search test");
|
||||||
|
e3b->setPassword("realpass");
|
||||||
e3b->setGroup(group3);
|
e3b->setGroup(group3);
|
||||||
|
|
||||||
m_searchResult = m_entrySearcher.search("search term", m_groupRoot, Qt::CaseInsensitive);
|
m_searchResult = m_entrySearcher.search("search", m_rootGroup);
|
||||||
QCOMPARE(m_searchResult.count(), 2);
|
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);
|
QCOMPARE(m_searchResult.count(), 1);
|
||||||
|
|
||||||
// Parent group disabled search
|
m_searchResult = m_entrySearcher.search("password:testpass", m_rootGroup);
|
||||||
m_searchResult = m_entrySearcher.search("search term", group11, Qt::CaseInsensitive);
|
QCOMPARE(m_searchResult.count(), 1);
|
||||||
|
|
||||||
|
// Parent group has search disabled
|
||||||
|
m_searchResult = m_entrySearcher.search("search term", group11);
|
||||||
QCOMPARE(m_searchResult.count(), 0);
|
QCOMPARE(m_searchResult.count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,38 +119,74 @@ void TestEntrySearcher::testAndConcatenationInSearch()
|
|||||||
Entry* entry = new Entry();
|
Entry* entry = new Entry();
|
||||||
entry->setNotes("abc def ghi");
|
entry->setNotes("abc def ghi");
|
||||||
entry->setTitle("jkl");
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
QCOMPARE(m_searchResult.count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestEntrySearcher::testAllAttributesAreSearched()
|
void TestEntrySearcher::testAllAttributesAreSearched()
|
||||||
{
|
{
|
||||||
Entry* entry = new Entry();
|
Entry* entry = new Entry();
|
||||||
entry->setGroup(m_groupRoot);
|
entry->setGroup(m_rootGroup);
|
||||||
|
|
||||||
entry->setTitle("testTitle");
|
entry->setTitle("testTitle");
|
||||||
entry->setUsername("testUsername");
|
entry->setUsername("testUsername");
|
||||||
entry->setUrl("testUrl");
|
entry->setUrl("testUrl");
|
||||||
entry->setNotes("testNote");
|
entry->setNotes("testNote");
|
||||||
|
|
||||||
m_searchResult =
|
// Default is to AND all terms together
|
||||||
m_entrySearcher.search("testTitle testUsername testUrl testNote", m_groupRoot, Qt::CaseInsensitive);
|
m_searchResult = m_entrySearcher.search("testTitle testUsername testUrl testNote", m_rootGroup);
|
||||||
QCOMPARE(m_searchResult.count(), 1);
|
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 testAndConcatenationInSearch();
|
||||||
void testSearch();
|
void testSearch();
|
||||||
void testAllAttributesAreSearched();
|
void testAllAttributesAreSearched();
|
||||||
|
void testSearchTermParser();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Group* m_groupRoot;
|
Group* m_rootGroup;
|
||||||
EntrySearcher m_entrySearcher;
|
EntrySearcher m_entrySearcher;
|
||||||
QList<Entry*> m_searchResult;
|
QList<Entry*> m_searchResult;
|
||||||
};
|
};
|
||||||
|
@ -841,9 +841,10 @@ void TestGui::testSearch()
|
|||||||
// Ensure Down focuses on entry view when search text is selected
|
// Ensure Down focuses on entry view when search text is selected
|
||||||
QTest::keyClick(searchTextEdit, Qt::Key_Down);
|
QTest::keyClick(searchTextEdit, Qt::Key_Down);
|
||||||
QTRY_VERIFY(entryView->hasFocus());
|
QTRY_VERIFY(entryView->hasFocus());
|
||||||
|
QCOMPARE(entryView->selectionModel()->currentIndex().row(), 0);
|
||||||
// Test that password copies (entry has focus)
|
// Test that password copies (entry has focus)
|
||||||
QClipboard* clipboard = QApplication::clipboard();
|
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);
|
QModelIndex searchedItem = entryView->model()->index(0, 1);
|
||||||
Entry* searchedEntry = entryView->entryFromIndex(searchedItem);
|
Entry* searchedEntry = entryView->entryFromIndex(searchedItem);
|
||||||
QTRY_COMPARE(searchedEntry->password(), clipboard->text());
|
QTRY_COMPARE(searchedEntry->password(), clipboard->text());
|
||||||
|
Loading…
Reference in New Issue
Block a user