* Moved search bar to toolbar and consolidated search options into dropdown list * Updated GUI tests to be atomic and rewrote search tests * Searches are saved between databases * Search is cleared when all databases are closed * Implemented global search shortcut (CTRL+F) and a notification bar when in search mode
589 lines
16 KiB
C++
589 lines
16 KiB
C++
/*
|
|
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
|
|
*
|
|
* 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 "AutoType.h"
|
|
|
|
#include <QApplication>
|
|
#include <QPluginLoader>
|
|
|
|
#include "autotype/AutoTypePlatformPlugin.h"
|
|
#include "autotype/AutoTypeSelectDialog.h"
|
|
#include "autotype/WildcardMatcher.h"
|
|
#include "core/Config.h"
|
|
#include "core/Database.h"
|
|
#include "core/Entry.h"
|
|
#include "core/FilePath.h"
|
|
#include "core/Group.h"
|
|
#include "core/ListDeleter.h"
|
|
#include "core/Tools.h"
|
|
#include "gui/MessageBox.h"
|
|
|
|
AutoType* AutoType::m_instance = nullptr;
|
|
|
|
AutoType::AutoType(QObject* parent, bool test)
|
|
: QObject(parent)
|
|
, m_inAutoType(false)
|
|
, m_currentGlobalKey(static_cast<Qt::Key>(0))
|
|
, m_currentGlobalModifiers(0)
|
|
, m_pluginLoader(new QPluginLoader(this))
|
|
, m_plugin(nullptr)
|
|
, m_executor(nullptr)
|
|
, m_windowFromGlobal(0)
|
|
{
|
|
// prevent crash when the plugin has unresolved symbols
|
|
m_pluginLoader->setLoadHints(QLibrary::ResolveAllSymbolsHint);
|
|
|
|
QString pluginName = "keepassx-autotype-";
|
|
if (!test) {
|
|
pluginName += QApplication::platformName();
|
|
}
|
|
else {
|
|
pluginName += "test";
|
|
}
|
|
|
|
QString pluginPath = filePath()->pluginPath(pluginName);
|
|
|
|
if (!pluginPath.isEmpty()) {
|
|
loadPlugin(pluginPath);
|
|
}
|
|
|
|
connect(qApp, SIGNAL(aboutToQuit()), SLOT(unloadPlugin()));
|
|
}
|
|
|
|
AutoType::~AutoType()
|
|
{
|
|
if (m_executor) {
|
|
delete m_executor;
|
|
m_executor = nullptr;
|
|
}
|
|
}
|
|
|
|
void AutoType::loadPlugin(const QString& pluginPath)
|
|
{
|
|
m_pluginLoader->setFileName(pluginPath);
|
|
|
|
QObject* pluginInstance = m_pluginLoader->instance();
|
|
if (pluginInstance) {
|
|
m_plugin = qobject_cast<AutoTypePlatformInterface*>(pluginInstance);
|
|
m_executor = nullptr;
|
|
|
|
if (m_plugin) {
|
|
if (m_plugin->isAvailable()) {
|
|
m_executor = m_plugin->createExecutor();
|
|
connect(pluginInstance, SIGNAL(globalShortcutTriggered()), SIGNAL(globalShortcutTriggered()));
|
|
}
|
|
else {
|
|
unloadPlugin();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!m_plugin) {
|
|
qWarning("Unable to load auto-type plugin:\n%s", qPrintable(m_pluginLoader->errorString()));
|
|
}
|
|
}
|
|
|
|
AutoType* AutoType::instance()
|
|
{
|
|
if (!m_instance) {
|
|
m_instance = new AutoType(qApp);
|
|
}
|
|
|
|
return m_instance;
|
|
}
|
|
|
|
void AutoType::createTestInstance()
|
|
{
|
|
Q_ASSERT(!m_instance);
|
|
|
|
m_instance = new AutoType(qApp, true);
|
|
}
|
|
|
|
QStringList AutoType::windowTitles()
|
|
{
|
|
if (!m_plugin) {
|
|
return QStringList();
|
|
}
|
|
|
|
return m_plugin->windowTitles();
|
|
}
|
|
|
|
void AutoType::performAutoType(const Entry* entry, QWidget* hideWindow, const QString& customSequence, WId window)
|
|
{
|
|
if (m_inAutoType || !m_plugin) {
|
|
return;
|
|
}
|
|
m_inAutoType = true;
|
|
|
|
QString sequence;
|
|
if (customSequence.isEmpty()) {
|
|
sequence = autoTypeSequence(entry);
|
|
}
|
|
else {
|
|
sequence = customSequence;
|
|
}
|
|
|
|
QList<AutoTypeAction*> actions;
|
|
ListDeleter<AutoTypeAction*> actionsDeleter(&actions);
|
|
|
|
if (!parseActions(sequence, entry, actions)) {
|
|
m_inAutoType = false; // TODO: make this automatic
|
|
return;
|
|
}
|
|
|
|
if (hideWindow) {
|
|
hideWindow->showMinimized();
|
|
}
|
|
|
|
Tools::wait(m_plugin->initialTimeout());
|
|
|
|
if (!window) {
|
|
window = m_plugin->activeWindow();
|
|
}
|
|
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
|
|
|
|
for (AutoTypeAction* action : asConst(actions)) {
|
|
if (m_plugin->activeWindow() != window) {
|
|
qWarning("Active window changed, interrupting auto-type.");
|
|
break;
|
|
}
|
|
|
|
action->accept(m_executor);
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
|
|
}
|
|
|
|
m_inAutoType = false;
|
|
}
|
|
|
|
void AutoType::performGlobalAutoType(const QList<Database*>& dbList)
|
|
{
|
|
if (m_inAutoType || !m_plugin) {
|
|
return;
|
|
}
|
|
|
|
QString windowTitle = m_plugin->activeWindowTitle();
|
|
|
|
if (windowTitle.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
m_inAutoType = true;
|
|
|
|
QList<Entry*> entryList;
|
|
QHash<Entry*, QString> sequenceHash;
|
|
|
|
// TODO: Check if there are any active databases here, if not do nothing
|
|
// TODO: Check if all databases are locked, if so ask to unlock them
|
|
|
|
for (Database* db : dbList) {
|
|
const QList<Entry*> dbEntries = db->rootGroup()->entriesRecursive();
|
|
for (Entry* entry : dbEntries) {
|
|
QString sequence = autoTypeSequence(entry, windowTitle);
|
|
if (!sequence.isEmpty()) {
|
|
entryList << entry;
|
|
sequenceHash.insert(entry, sequence);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (entryList.isEmpty()) {
|
|
m_inAutoType = false;
|
|
QString message = tr("Couldn't find an entry that matches the window title:");
|
|
message.append("\n\n");
|
|
message.append(windowTitle);
|
|
MessageBox::information(nullptr, tr("Auto-Type - KeePassX"), message);
|
|
}
|
|
else if ((entryList.size() == 1) && !config()->get("security/autotypeask").toBool()) {
|
|
m_inAutoType = false;
|
|
performAutoType(entryList.first(), nullptr, sequenceHash[entryList.first()]);
|
|
}
|
|
else {
|
|
m_windowFromGlobal = m_plugin->activeWindow();
|
|
AutoTypeSelectDialog* selectDialog = new AutoTypeSelectDialog();
|
|
connect(selectDialog, SIGNAL(entryActivated(Entry*,QString)),
|
|
SLOT(performAutoTypeFromGlobal(Entry*,QString)));
|
|
connect(selectDialog, SIGNAL(rejected()), SLOT(resetInAutoType()));
|
|
selectDialog->setEntries(entryList, sequenceHash);
|
|
selectDialog->show();
|
|
// necessary when the main window is minimized
|
|
selectDialog->activateWindow();
|
|
}
|
|
}
|
|
|
|
void AutoType::performAutoTypeFromGlobal(Entry* entry, const QString& sequence)
|
|
{
|
|
Q_ASSERT(m_inAutoType);
|
|
|
|
m_plugin->raiseWindow(m_windowFromGlobal);
|
|
|
|
m_inAutoType = false;
|
|
performAutoType(entry, nullptr, sequence, m_windowFromGlobal);
|
|
}
|
|
|
|
void AutoType::resetInAutoType()
|
|
{
|
|
Q_ASSERT(m_inAutoType);
|
|
|
|
m_inAutoType = false;
|
|
}
|
|
|
|
void AutoType::unloadPlugin()
|
|
{
|
|
if (m_executor) {
|
|
delete m_executor;
|
|
m_executor = nullptr;
|
|
}
|
|
|
|
if (m_plugin) {
|
|
m_plugin->unload();
|
|
m_plugin = nullptr;
|
|
}
|
|
}
|
|
|
|
bool AutoType::registerGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers)
|
|
{
|
|
Q_ASSERT(key);
|
|
Q_ASSERT(modifiers);
|
|
|
|
if (!m_plugin) {
|
|
return false;
|
|
}
|
|
|
|
if (key != m_currentGlobalKey || modifiers != m_currentGlobalModifiers) {
|
|
if (m_currentGlobalKey && m_currentGlobalModifiers) {
|
|
m_plugin->unregisterGlobalShortcut(m_currentGlobalKey, m_currentGlobalModifiers);
|
|
}
|
|
|
|
if (m_plugin->registerGlobalShortcut(key, modifiers)) {
|
|
m_currentGlobalKey = key;
|
|
m_currentGlobalModifiers = modifiers;
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void AutoType::unregisterGlobalShortcut()
|
|
{
|
|
if (m_plugin && m_currentGlobalKey && m_currentGlobalModifiers) {
|
|
m_plugin->unregisterGlobalShortcut(m_currentGlobalKey, m_currentGlobalModifiers);
|
|
}
|
|
}
|
|
|
|
int AutoType::callEventFilter(void* event)
|
|
{
|
|
if (!m_plugin) {
|
|
return -1;
|
|
}
|
|
|
|
return m_plugin->platformEventFilter(event);
|
|
}
|
|
|
|
bool AutoType::parseActions(const QString& sequence, const Entry* entry, QList<AutoTypeAction*>& actions)
|
|
{
|
|
QString tmpl;
|
|
bool inTmpl = false;
|
|
|
|
for (const QChar& ch : sequence) {
|
|
// TODO: implement support for {{}, {}} and {DELAY=X}
|
|
|
|
if (inTmpl) {
|
|
if (ch == '{') {
|
|
qWarning("Syntax error in auto-type sequence.");
|
|
return false;
|
|
}
|
|
else if (ch == '}') {
|
|
actions.append(createActionFromTemplate(tmpl, entry));
|
|
inTmpl = false;
|
|
tmpl.clear();
|
|
}
|
|
else {
|
|
tmpl += ch;
|
|
}
|
|
}
|
|
else if (ch == '{') {
|
|
inTmpl = true;
|
|
}
|
|
else if (ch == '}') {
|
|
qWarning("Syntax error in auto-type sequence.");
|
|
return false;
|
|
}
|
|
else {
|
|
actions.append(new AutoTypeChar(ch));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QList<AutoTypeAction*> AutoType::createActionFromTemplate(const QString& tmpl, const Entry* entry)
|
|
{
|
|
QString tmplName = tmpl.toLower();
|
|
int num = -1;
|
|
QList<AutoTypeAction*> list;
|
|
|
|
QRegExp repeatRegEx("(.+) (\\d+)", Qt::CaseSensitive, QRegExp::RegExp2);
|
|
if (repeatRegEx.exactMatch(tmplName)) {
|
|
tmplName = repeatRegEx.cap(1);
|
|
num = repeatRegEx.cap(2).toInt();
|
|
|
|
if (num == 0) {
|
|
return list;
|
|
}
|
|
// some safety checks
|
|
else if (tmplName == "delay") {
|
|
if (num > 10000) {
|
|
return list;
|
|
}
|
|
}
|
|
else if (num > 100) {
|
|
return list;
|
|
}
|
|
}
|
|
|
|
if (tmplName == "tab") {
|
|
list.append(new AutoTypeKey(Qt::Key_Tab));
|
|
}
|
|
else if (tmplName == "enter") {
|
|
list.append(new AutoTypeKey(Qt::Key_Enter));
|
|
}
|
|
else if (tmplName == "up") {
|
|
list.append(new AutoTypeKey(Qt::Key_Up));
|
|
}
|
|
else if (tmplName == "down") {
|
|
list.append(new AutoTypeKey(Qt::Key_Down));
|
|
}
|
|
else if (tmplName == "left") {
|
|
list.append(new AutoTypeKey(Qt::Key_Left));
|
|
}
|
|
else if (tmplName == "right") {
|
|
list.append(new AutoTypeKey(Qt::Key_Right));
|
|
}
|
|
else if (tmplName == "insert" || tmplName == "ins") {
|
|
list.append(new AutoTypeKey(Qt::Key_Insert));
|
|
}
|
|
else if (tmplName == "delete" || tmplName == "del") {
|
|
list.append(new AutoTypeKey(Qt::Key_Delete));
|
|
}
|
|
else if (tmplName == "home") {
|
|
list.append(new AutoTypeKey(Qt::Key_Home));
|
|
}
|
|
else if (tmplName == "end") {
|
|
list.append(new AutoTypeKey(Qt::Key_End));
|
|
}
|
|
else if (tmplName == "pgup") {
|
|
list.append(new AutoTypeKey(Qt::Key_PageUp));
|
|
}
|
|
else if (tmplName == "pgdown") {
|
|
list.append(new AutoTypeKey(Qt::Key_PageDown));
|
|
}
|
|
else if (tmplName == "backspace" || tmplName == "bs" || tmplName == "bksp") {
|
|
list.append(new AutoTypeKey(Qt::Key_Backspace));
|
|
}
|
|
else if (tmplName == "break") {
|
|
list.append(new AutoTypeKey(Qt::Key_Pause));
|
|
}
|
|
else if (tmplName == "capslock") {
|
|
list.append(new AutoTypeKey(Qt::Key_CapsLock));
|
|
}
|
|
else if (tmplName == "esc") {
|
|
list.append(new AutoTypeKey(Qt::Key_Escape));
|
|
}
|
|
else if (tmplName == "help") {
|
|
list.append(new AutoTypeKey(Qt::Key_Help));
|
|
}
|
|
else if (tmplName == "numlock") {
|
|
list.append(new AutoTypeKey(Qt::Key_NumLock));
|
|
}
|
|
else if (tmplName == "ptrsc") {
|
|
list.append(new AutoTypeKey(Qt::Key_Print));
|
|
}
|
|
else if (tmplName == "scolllock") {
|
|
list.append(new AutoTypeKey(Qt::Key_ScrollLock));
|
|
}
|
|
// Qt doesn't know about keypad keys so use the normal ones instead
|
|
else if (tmplName == "add" || tmplName == "+") {
|
|
list.append(new AutoTypeChar('+'));
|
|
}
|
|
else if (tmplName == "subtract") {
|
|
list.append(new AutoTypeChar('-'));
|
|
}
|
|
else if (tmplName == "multiply") {
|
|
list.append(new AutoTypeChar('*'));
|
|
}
|
|
else if (tmplName == "divide") {
|
|
list.append(new AutoTypeChar('/'));
|
|
}
|
|
else if (tmplName == "^") {
|
|
list.append(new AutoTypeChar('^'));
|
|
}
|
|
else if (tmplName == "%") {
|
|
list.append(new AutoTypeChar('%'));
|
|
}
|
|
else if (tmplName == "~") {
|
|
list.append(new AutoTypeChar('~'));
|
|
}
|
|
else if (tmplName == "(") {
|
|
list.append(new AutoTypeChar('('));
|
|
}
|
|
else if (tmplName == ")") {
|
|
list.append(new AutoTypeChar(')'));
|
|
}
|
|
else if (tmplName == "{") {
|
|
list.append(new AutoTypeChar('{'));
|
|
}
|
|
else if (tmplName == "}") {
|
|
list.append(new AutoTypeChar('}'));
|
|
}
|
|
else {
|
|
QRegExp fnRegexp("f(\\d+)", Qt::CaseInsensitive, QRegExp::RegExp2);
|
|
if (fnRegexp.exactMatch(tmplName)) {
|
|
int fnNo = fnRegexp.cap(1).toInt();
|
|
if (fnNo >= 1 && fnNo <= 16) {
|
|
list.append(new AutoTypeKey(static_cast<Qt::Key>(Qt::Key_F1 - 1 + fnNo)));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!list.isEmpty()) {
|
|
for (int i = 1; i < num; i++) {
|
|
list.append(list.at(0)->clone());
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
|
|
if (tmplName == "delay" && num > 0) {
|
|
list.append(new AutoTypeDelay(num));
|
|
}
|
|
else if (tmplName == "clearfield") {
|
|
list.append(new AutoTypeClearField());
|
|
}
|
|
|
|
if (!list.isEmpty()) {
|
|
return list;
|
|
}
|
|
|
|
|
|
const QString placeholder = QString("{%1}").arg(tmplName);
|
|
const QString resolved = entry->resolvePlaceholders(placeholder);
|
|
if (placeholder != resolved) {
|
|
for (const QChar& ch : resolved) {
|
|
if (ch == '\n') {
|
|
list.append(new AutoTypeKey(Qt::Key_Enter));
|
|
}
|
|
else if (ch == '\t') {
|
|
list.append(new AutoTypeKey(Qt::Key_Tab));
|
|
}
|
|
else {
|
|
list.append(new AutoTypeChar(ch));
|
|
}
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
QString AutoType::autoTypeSequence(const Entry* entry, const QString& windowTitle)
|
|
{
|
|
if (!entry->autoTypeEnabled()) {
|
|
return QString();
|
|
}
|
|
|
|
bool enableSet = false;
|
|
QString sequence;
|
|
if (!windowTitle.isEmpty()) {
|
|
bool match = false;
|
|
const QList<AutoTypeAssociations::Association> assocList = entry->autoTypeAssociations()->getAll();
|
|
for (const AutoTypeAssociations::Association& assoc : assocList) {
|
|
if (windowMatches(windowTitle, assoc.window)) {
|
|
if (!assoc.sequence.isEmpty()) {
|
|
sequence = assoc.sequence;
|
|
}
|
|
else {
|
|
sequence = entry->defaultAutoTypeSequence();
|
|
}
|
|
match = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!match && config()->get("AutoTypeEntryTitleMatch").toBool() && !entry->title().isEmpty()
|
|
&& windowTitle.contains(entry->title(), Qt::CaseInsensitive)) {
|
|
sequence = entry->defaultAutoTypeSequence();
|
|
match = true;
|
|
}
|
|
|
|
if (!match) {
|
|
return QString();
|
|
}
|
|
}
|
|
else {
|
|
sequence = entry->defaultAutoTypeSequence();
|
|
}
|
|
|
|
const Group* group = entry->group();
|
|
do {
|
|
if (!enableSet) {
|
|
if (group->autoTypeEnabled() == Group::Disable) {
|
|
return QString();
|
|
}
|
|
else if (group->autoTypeEnabled() == Group::Enable) {
|
|
enableSet = true;
|
|
}
|
|
}
|
|
|
|
if (sequence.isEmpty()) {
|
|
sequence = group->defaultAutoTypeSequence();
|
|
}
|
|
|
|
group = group->parentGroup();
|
|
} while (group && (!enableSet || sequence.isEmpty()));
|
|
|
|
if (sequence.isEmpty() && (!entry->username().isEmpty() || !entry->password().isEmpty())) {
|
|
if (entry->username().isEmpty()) {
|
|
sequence = "{PASSWORD}{ENTER}";
|
|
}
|
|
else if (entry->password().isEmpty()) {
|
|
sequence = "{USERNAME}{ENTER}";
|
|
}
|
|
else {
|
|
sequence = "{USERNAME}{TAB}{PASSWORD}{ENTER}";
|
|
}
|
|
}
|
|
|
|
return sequence;
|
|
}
|
|
|
|
bool AutoType::windowMatches(const QString& windowTitle, const QString& windowPattern)
|
|
{
|
|
if (windowPattern.startsWith("//") && windowPattern.endsWith("//") && windowPattern.size() >= 4) {
|
|
QRegExp regExp(windowPattern.mid(2, windowPattern.size() - 4), Qt::CaseInsensitive, QRegExp::RegExp2);
|
|
return (regExp.indexIn(windowTitle) != -1);
|
|
}
|
|
else {
|
|
return WildcardMatcher(windowTitle).match(windowPattern);
|
|
}
|
|
}
|