2014-05-15 17:48:54 -04:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2014 Florian Geyer <blueice@fobos.de>
|
2017-06-09 17:40:36 -04:00
|
|
|
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
2014-05-15 17:48:54 -04:00
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 2 or (at your option)
|
|
|
|
* version 3 of the License.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "EntrySearcher.h"
|
|
|
|
|
2022-01-23 10:00:48 -05:00
|
|
|
#include "PasswordHealth.h"
|
2014-05-15 17:48:54 -04:00
|
|
|
#include "core/Group.h"
|
2018-03-25 16:24:30 -04:00
|
|
|
#include "core/Tools.h"
|
2014-05-15 17:48:54 -04:00
|
|
|
|
2020-01-24 12:42:00 -05:00
|
|
|
EntrySearcher::EntrySearcher(bool caseSensitive, bool skipProtected)
|
2018-03-25 16:24:30 -04:00
|
|
|
: m_caseSensitive(caseSensitive)
|
2020-01-24 12:42:00 -05:00
|
|
|
, m_skipProtected(skipProtected)
|
2014-05-15 17:48:54 -04:00
|
|
|
{
|
2018-03-25 16:24:30 -04:00
|
|
|
}
|
2014-05-15 17:48:54 -04:00
|
|
|
|
2019-11-01 16:23:26 -04:00
|
|
|
/**
|
|
|
|
* Search group, and its children, directly by provided search terms
|
|
|
|
* @param searchTerms search terms
|
|
|
|
* @param baseGroup group to start search from, cannot be null
|
|
|
|
* @param forceSearch ignore group search settings
|
|
|
|
* @return list of entries that match the search terms
|
|
|
|
*/
|
|
|
|
QList<Entry*> EntrySearcher::search(const QList<SearchTerm>& searchTerms, const Group* baseGroup, bool forceSearch)
|
|
|
|
{
|
|
|
|
Q_ASSERT(baseGroup);
|
|
|
|
m_searchTerms = searchTerms;
|
|
|
|
return repeat(baseGroup, forceSearch);
|
|
|
|
}
|
|
|
|
|
2019-02-24 18:52:25 -05:00
|
|
|
/**
|
|
|
|
* Search group, and its children, by parsing the provided search
|
|
|
|
* string for search terms.
|
|
|
|
*
|
|
|
|
* @param searchString search terms
|
|
|
|
* @param baseGroup group to start search from, cannot be null
|
|
|
|
* @param forceSearch ignore group search settings
|
|
|
|
* @return list of entries that match the search terms
|
|
|
|
*/
|
2018-03-25 16:24:30 -04:00
|
|
|
QList<Entry*> EntrySearcher::search(const QString& searchString, const Group* baseGroup, bool forceSearch)
|
|
|
|
{
|
|
|
|
Q_ASSERT(baseGroup);
|
2019-02-22 17:17:51 -05:00
|
|
|
parseSearchTerms(searchString);
|
|
|
|
return repeat(baseGroup, forceSearch);
|
|
|
|
}
|
|
|
|
|
2019-02-24 18:52:25 -05:00
|
|
|
/**
|
|
|
|
* Repeat the last search starting from the given group
|
|
|
|
*
|
|
|
|
* @param baseGroup group to start search from, cannot be null
|
|
|
|
* @param forceSearch ignore group search settings
|
|
|
|
* @return list of entries that match the search terms
|
|
|
|
*/
|
2019-02-22 17:17:51 -05:00
|
|
|
QList<Entry*> EntrySearcher::repeat(const Group* baseGroup, bool forceSearch)
|
|
|
|
{
|
|
|
|
Q_ASSERT(baseGroup);
|
|
|
|
|
2018-03-25 16:24:30 -04:00
|
|
|
QList<Entry*> results;
|
|
|
|
for (const auto group : baseGroup->groupsRecursive(true)) {
|
|
|
|
if (forceSearch || group->resolveSearchingEnabled()) {
|
2020-05-08 11:13:15 -04:00
|
|
|
for (const auto entry : group->entries()) {
|
2019-02-22 17:17:51 -05:00
|
|
|
if (searchEntryImpl(entry)) {
|
|
|
|
results.append(entry);
|
|
|
|
}
|
|
|
|
}
|
2014-05-15 17:48:54 -04:00
|
|
|
}
|
|
|
|
}
|
2018-03-19 23:16:22 -04:00
|
|
|
return results;
|
2014-05-15 17:48:54 -04:00
|
|
|
}
|
2014-05-15 18:19:58 -04:00
|
|
|
|
2019-11-01 16:23:26 -04:00
|
|
|
/**
|
|
|
|
* Search provided entries by the provided search terms
|
|
|
|
*
|
|
|
|
* @param searchTerms search terms
|
|
|
|
* @param entries list of entries to include in the search
|
|
|
|
* @return list of entries that match the search terms
|
|
|
|
*/
|
|
|
|
QList<Entry*> EntrySearcher::searchEntries(const QList<SearchTerm>& searchTerms, const QList<Entry*>& entries)
|
|
|
|
{
|
|
|
|
m_searchTerms = searchTerms;
|
|
|
|
return repeatEntries(entries);
|
|
|
|
}
|
|
|
|
|
2019-02-24 18:52:25 -05:00
|
|
|
/**
|
|
|
|
* Search provided entries by parsing the search string
|
|
|
|
* for search terms.
|
|
|
|
*
|
|
|
|
* @param searchString search terms
|
|
|
|
* @param entries list of entries to include in the search
|
|
|
|
* @return list of entries that match the search terms
|
|
|
|
*/
|
2018-03-25 16:24:30 -04:00
|
|
|
QList<Entry*> EntrySearcher::searchEntries(const QString& searchString, const QList<Entry*>& entries)
|
2019-02-22 17:17:51 -05:00
|
|
|
{
|
|
|
|
parseSearchTerms(searchString);
|
|
|
|
return repeatEntries(entries);
|
|
|
|
}
|
|
|
|
|
2019-02-24 18:52:25 -05:00
|
|
|
/**
|
|
|
|
* Repeat the last search on the given entries
|
|
|
|
*
|
|
|
|
* @param entries list of entries to include in the search
|
|
|
|
* @return list of entries that match the search terms
|
|
|
|
*/
|
2019-02-22 17:17:51 -05:00
|
|
|
QList<Entry*> EntrySearcher::repeatEntries(const QList<Entry*>& entries)
|
2014-05-15 18:19:58 -04:00
|
|
|
{
|
2018-03-19 23:16:22 -04:00
|
|
|
QList<Entry*> results;
|
2019-02-22 17:17:51 -05:00
|
|
|
for (auto* entry : entries) {
|
|
|
|
if (searchEntryImpl(entry)) {
|
2019-01-20 09:50:20 -05:00
|
|
|
results.append(entry);
|
|
|
|
}
|
2014-05-15 18:19:58 -04:00
|
|
|
}
|
2018-03-19 23:16:22 -04:00
|
|
|
return results;
|
2014-05-15 18:19:58 -04:00
|
|
|
}
|
2017-01-28 11:27:20 -05:00
|
|
|
|
2019-02-24 18:52:25 -05:00
|
|
|
/**
|
|
|
|
* Set the next search to be case sensitive or not
|
|
|
|
*
|
2019-03-19 14:48:33 -04:00
|
|
|
* @param state
|
2019-02-24 18:52:25 -05:00
|
|
|
*/
|
2018-03-25 16:24:30 -04:00
|
|
|
void EntrySearcher::setCaseSensitive(bool state)
|
|
|
|
{
|
|
|
|
m_caseSensitive = state;
|
|
|
|
}
|
|
|
|
|
2020-01-24 12:42:00 -05:00
|
|
|
bool EntrySearcher::isCaseSensitive() const
|
2017-01-28 11:27:20 -05:00
|
|
|
{
|
2018-03-25 16:24:30 -04:00
|
|
|
return m_caseSensitive;
|
|
|
|
}
|
|
|
|
|
2020-05-08 11:13:15 -04:00
|
|
|
bool EntrySearcher::searchEntryImpl(const Entry* entry)
|
2018-03-25 16:24:30 -04:00
|
|
|
{
|
|
|
|
// Pre-load in case they are needed
|
2019-02-22 17:17:51 -05:00
|
|
|
auto attributes_keys = entry->attributes()->customKeys();
|
|
|
|
auto attributes = QStringList(attributes_keys + entry->attributes()->values(attributes_keys));
|
2018-03-25 16:24:30 -04:00
|
|
|
auto attachments = QStringList(entry->attachments()->keys());
|
2020-05-08 11:13:15 -04:00
|
|
|
// Build a group hierarchy to allow searching for e.g. /group1/subgroup*
|
2023-07-19 16:11:59 -04:00
|
|
|
QString hierarchy;
|
|
|
|
if (entry->group()) {
|
|
|
|
hierarchy = entry->group()->hierarchy().join('/').prepend("/");
|
|
|
|
}
|
2018-03-25 16:24:30 -04:00
|
|
|
|
2020-01-24 12:42:00 -05:00
|
|
|
// By default, empty term matches every entry.
|
2022-01-23 10:00:48 -05:00
|
|
|
// However when skipping protected fields, we will reject everything instead
|
2020-01-24 12:42:00 -05:00
|
|
|
bool found = !m_skipProtected;
|
2019-02-22 17:17:51 -05:00
|
|
|
for (const auto& term : m_searchTerms) {
|
2019-11-01 16:23:26 -04:00
|
|
|
switch (term.field) {
|
2018-03-25 16:24:30 -04:00
|
|
|
case Field::Title:
|
2019-11-01 16:23:26 -04:00
|
|
|
found = term.regex.match(entry->resolvePlaceholder(entry->title())).hasMatch();
|
2018-03-25 16:24:30 -04:00
|
|
|
break;
|
|
|
|
case Field::Username:
|
2019-11-01 16:23:26 -04:00
|
|
|
found = term.regex.match(entry->resolvePlaceholder(entry->username())).hasMatch();
|
2018-03-25 16:24:30 -04:00
|
|
|
break;
|
|
|
|
case Field::Password:
|
2020-01-24 12:42:00 -05:00
|
|
|
if (m_skipProtected) {
|
|
|
|
continue;
|
|
|
|
}
|
2019-11-01 16:23:26 -04:00
|
|
|
found = term.regex.match(entry->resolvePlaceholder(entry->password())).hasMatch();
|
2018-03-25 16:24:30 -04:00
|
|
|
break;
|
|
|
|
case Field::Url:
|
2019-11-01 16:23:26 -04:00
|
|
|
found = term.regex.match(entry->resolvePlaceholder(entry->url())).hasMatch();
|
2018-03-25 16:24:30 -04:00
|
|
|
break;
|
|
|
|
case Field::Notes:
|
2019-11-01 16:23:26 -04:00
|
|
|
found = term.regex.match(entry->notes()).hasMatch();
|
2018-03-25 16:24:30 -04:00
|
|
|
break;
|
2019-11-01 16:23:26 -04:00
|
|
|
case Field::AttributeKV:
|
|
|
|
found = !attributes.filter(term.regex).empty();
|
2018-03-25 16:24:30 -04:00
|
|
|
break;
|
|
|
|
case Field::Attachment:
|
2019-11-01 16:23:26 -04:00
|
|
|
found = !attachments.filter(term.regex).empty();
|
2018-03-25 16:24:30 -04:00
|
|
|
break;
|
2019-02-21 00:51:23 -05:00
|
|
|
case Field::AttributeValue:
|
2020-01-24 12:42:00 -05:00
|
|
|
if (m_skipProtected && entry->attributes()->isProtected(term.word)) {
|
2019-02-21 00:51:23 -05:00
|
|
|
continue;
|
|
|
|
}
|
2019-11-01 16:23:26 -04:00
|
|
|
found = entry->attributes()->contains(term.word)
|
|
|
|
&& term.regex.match(entry->attributes()->value(term.word)).hasMatch();
|
2019-02-21 00:51:23 -05:00
|
|
|
break;
|
2020-05-08 11:13:15 -04:00
|
|
|
case Field::Group:
|
|
|
|
// Match against the full hierarchy if the word contains a '/' otherwise just the group name
|
|
|
|
if (term.word.contains('/')) {
|
|
|
|
found = term.regex.match(hierarchy).hasMatch();
|
2023-07-19 16:11:59 -04:00
|
|
|
} else if (entry->group()) {
|
2020-05-08 11:13:15 -04:00
|
|
|
found = term.regex.match(entry->group()->name()).hasMatch();
|
|
|
|
}
|
|
|
|
break;
|
2022-01-23 10:00:48 -05:00
|
|
|
case Field::Tag:
|
2022-09-07 19:25:23 -04:00
|
|
|
found = entry->tagList().indexOf(term.regex) != -1;
|
2022-01-23 10:00:48 -05:00
|
|
|
break;
|
|
|
|
case Field::Is:
|
2022-09-07 19:25:23 -04:00
|
|
|
if (term.word.startsWith("expired", Qt::CaseInsensitive)) {
|
|
|
|
auto days = 0;
|
|
|
|
auto parts = term.word.split("-", QString::SkipEmptyParts);
|
|
|
|
if (parts.length() >= 2) {
|
|
|
|
days = parts[1].toInt();
|
|
|
|
}
|
|
|
|
found = entry->willExpireInDays(days) && !entry->isRecycled();
|
2022-01-23 10:00:48 -05:00
|
|
|
break;
|
|
|
|
} else if (term.word.compare("weak", Qt::CaseInsensitive) == 0) {
|
|
|
|
if (!entry->excludeFromReports() && !entry->password().isEmpty() && !entry->isExpired()) {
|
|
|
|
const auto quality = entry->passwordHealth()->quality();
|
|
|
|
if (quality == PasswordHealth::Quality::Bad || quality == PasswordHealth::Quality::Poor
|
|
|
|
|| quality == PasswordHealth::Quality::Weak) {
|
|
|
|
found = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
found = false;
|
|
|
|
break;
|
2023-07-04 07:24:10 -04:00
|
|
|
case Field::Uuid:
|
|
|
|
found = term.regex.match(entry->uuidToHex()).hasMatch();
|
|
|
|
break;
|
2018-03-25 16:24:30 -04:00
|
|
|
default:
|
2018-11-01 21:33:27 -04:00
|
|
|
// Terms without a specific field try to match title, username, url, and notes
|
2019-11-01 16:23:26 -04:00
|
|
|
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()
|
2022-09-07 19:25:23 -04:00
|
|
|
|| entry->tagList().indexOf(term.regex) != -1 || term.regex.match(entry->notes()).hasMatch();
|
2018-03-25 16:24:30 -04:00
|
|
|
}
|
|
|
|
|
2020-01-24 12:42:00 -05:00
|
|
|
// negate the result if exclude:
|
|
|
|
// * if found and not excluding, the entry matches
|
|
|
|
// * if didn't found but excluding, the entry also matches
|
|
|
|
found = (found && !term.exclude) || (!found && term.exclude);
|
|
|
|
|
|
|
|
// short circuit if we failed the match
|
|
|
|
if (!found) {
|
2017-01-28 11:27:20 -05:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-24 12:42:00 -05:00
|
|
|
return found;
|
2017-01-28 11:27:20 -05:00
|
|
|
}
|
|
|
|
|
2019-02-22 17:17:51 -05:00
|
|
|
void EntrySearcher::parseSearchTerms(const QString& searchString)
|
2017-01-28 11:27:20 -05:00
|
|
|
{
|
2019-08-12 10:33:31 -04:00
|
|
|
static const QList<QPair<QString, Field>> fieldnames{
|
|
|
|
{QStringLiteral("attachment"), Field::Attachment},
|
2019-11-01 16:23:26 -04:00
|
|
|
{QStringLiteral("attribute"), Field::AttributeKV},
|
2019-08-12 10:33:31 -04:00
|
|
|
{QStringLiteral("notes"), Field::Notes},
|
|
|
|
{QStringLiteral("pw"), Field::Password},
|
|
|
|
{QStringLiteral("password"), Field::Password},
|
2022-09-07 19:25:23 -04:00
|
|
|
{QStringLiteral("title"), Field::Title}, // title before tag to capture t:<word>
|
|
|
|
{QStringLiteral("username"), Field::Username}, // username before url to capture u:<word>
|
2019-08-12 10:33:31 -04:00
|
|
|
{QStringLiteral("url"), Field::Url},
|
2022-01-23 10:00:48 -05:00
|
|
|
{QStringLiteral("group"), Field::Group},
|
|
|
|
{QStringLiteral("tag"), Field::Tag},
|
2023-07-04 07:24:10 -04:00
|
|
|
{QStringLiteral("is"), Field::Is},
|
|
|
|
{QStringLiteral("uuid"), Field::Uuid}};
|
2019-08-12 10:33:31 -04:00
|
|
|
|
2022-09-07 19:25:23 -04:00
|
|
|
// Group 1 = modifiers, Group 2 = field, Group 3 = quoted string, Group 4 = unquoted string
|
|
|
|
static QRegularExpression termParser(R"re(([-!*+]+)?(?:(\w*):)?(?:(?=")"((?:[^"\\]|\\.)*)"|([^ ]*))( |$))re");
|
|
|
|
|
2019-02-22 17:17:51 -05:00
|
|
|
m_searchTerms.clear();
|
2022-09-07 19:25:23 -04:00
|
|
|
auto results = termParser.globalMatch(searchString);
|
2018-03-25 16:24:30 -04:00
|
|
|
while (results.hasNext()) {
|
|
|
|
auto result = results.next();
|
2019-11-01 16:23:26 -04:00
|
|
|
SearchTerm term{};
|
2018-03-25 16:24:30 -04:00
|
|
|
|
|
|
|
// Quoted string group
|
2019-11-01 16:23:26 -04:00
|
|
|
term.word = result.captured(3);
|
2022-09-07 19:25:23 -04:00
|
|
|
// Unescape quotes
|
|
|
|
term.word.replace("\\\"", "\"");
|
2018-03-25 16:24:30 -04:00
|
|
|
|
|
|
|
// If empty, use the unquoted string group
|
2019-11-01 16:23:26 -04:00
|
|
|
if (term.word.isEmpty()) {
|
|
|
|
term.word = result.captured(4);
|
2018-03-25 16:24:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// If still empty, ignore this match
|
2019-11-01 16:23:26 -04:00
|
|
|
if (term.word.isEmpty()) {
|
2018-03-25 16:24:30 -04:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto mods = result.captured(1);
|
|
|
|
|
|
|
|
// Convert term to regex
|
2021-12-30 09:19:07 -05:00
|
|
|
int opts = m_caseSensitive ? Tools::RegexConvertOpts::CASE_SENSITIVE : Tools::RegexConvertOpts::DEFAULT;
|
|
|
|
if (!mods.contains("*")) {
|
|
|
|
opts |= Tools::RegexConvertOpts::WILDCARD_ALL;
|
|
|
|
}
|
|
|
|
if (mods.contains("+")) {
|
|
|
|
opts |= Tools::RegexConvertOpts::EXACT_MATCH;
|
|
|
|
}
|
|
|
|
term.regex = Tools::convertToRegex(term.word, opts);
|
2018-03-25 16:24:30 -04:00
|
|
|
|
|
|
|
// Exclude modifier
|
2019-11-01 16:23:26 -04:00
|
|
|
term.exclude = mods.contains("-") || mods.contains("!");
|
2018-03-25 16:24:30 -04:00
|
|
|
|
|
|
|
// Determine the field to search
|
2019-11-01 16:23:26 -04:00
|
|
|
term.field = Field::Undefined;
|
2019-08-12 10:33:31 -04:00
|
|
|
|
2018-03-25 16:24:30 -04:00
|
|
|
QString field = result.captured(2);
|
|
|
|
if (!field.isEmpty()) {
|
2019-08-12 10:33:31 -04:00
|
|
|
if (field.startsWith("_", Qt::CaseInsensitive)) {
|
2019-11-01 16:23:26 -04:00
|
|
|
term.field = Field::AttributeValue;
|
2019-02-21 00:51:23 -05:00
|
|
|
// searching a custom attribute
|
2019-11-01 16:23:26 -04:00
|
|
|
// in this case term.word is the attribute key (removing the leading "_")
|
|
|
|
// and term.regex is used to match attribute value
|
|
|
|
term.word = field.mid(1);
|
2019-08-12 10:33:31 -04:00
|
|
|
} else {
|
|
|
|
for (const auto& pair : fieldnames) {
|
|
|
|
if (pair.first.startsWith(field, Qt::CaseInsensitive)) {
|
2019-11-01 16:23:26 -04:00
|
|
|
term.field = pair.second;
|
2019-08-12 10:33:31 -04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2018-03-25 16:24:30 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-22 17:17:51 -05:00
|
|
|
m_searchTerms.append(term);
|
2018-03-25 16:24:30 -04:00
|
|
|
}
|
2017-01-28 11:27:20 -05:00
|
|
|
}
|