Files
keepassxc/src/autotype/AutoType.cpp
2018-01-03 12:56:18 +01:00

682 lines
22 KiB
C++

/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* 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 <QtWidgets/QErrorMessage>
#include <iostream>
#include "config-keepassx.h"
#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_autoTypeDelay(0)
, 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()) {
#ifdef WITH_XC_AUTOTYPE
loadPlugin(pluginPath);
#endif
}
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::executeAutoType(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;
}
if (!checkSyntax(sequence)) {
return;
}
sequence.replace("{{}", "{LEFTBRACE}");
sequence.replace("{}}", "{RIGHTBRACE}");
QList<AutoTypeAction*> actions;
ListDeleter<AutoTypeAction*> actionsDeleter(&actions);
if (!parseActions(sequence, entry, actions)) {
m_inAutoType = false; // TODO: make this automatic
return;
}
if (hideWindow) {
#if defined(Q_OS_MAC)
m_plugin->raiseLastActiveWindow();
#else
hideWindow->showMinimized();
#endif
}
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;
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 - KeePassXC"), 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);
#if defined(Q_OS_MAC)
m_plugin->raiseOwnWindow();
Tools::wait(500);
#endif
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::raiseWindow()
{
#if defined(Q_OS_MAC)
m_plugin->raiseOwnWindow();
#endif
}
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;
m_autoTypeDelay = config()->get("AutoTypeDelay").toInt();
for (const QChar& ch : sequence) {
if (inTmpl) {
if (ch == '{') {
qWarning("Syntax error in auto-type sequence.");
return false;
} else if (ch == '}') {
QList<AutoTypeAction*> autoType = createActionFromTemplate(tmpl, entry);
if (autoType.isEmpty()) {
return false;
}
actions.append(autoType);
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));
}
}
if (m_autoTypeDelay > 0) {
QList<AutoTypeAction*>::iterator i;
i = actions.begin();
while (i != actions.end()) {
++i;
if (i != actions.end()) {
i = actions.insert(i, new AutoTypeDelay(m_autoTypeDelay));
++i;
}
}
}
return true;
}
QList<AutoTypeAction*> AutoType::createActionFromTemplate(const QString& tmpl, const Entry* entry)
{
QString tmplName = tmpl;
int num = -1;
QList<AutoTypeAction*> list;
QRegExp delayRegEx("delay=(\\d+)", Qt::CaseInsensitive, QRegExp::RegExp2);
if (delayRegEx.exactMatch(tmplName)) {
num = delayRegEx.cap(1).toInt();
m_autoTypeDelay = std::max(0, std::min(num, 10000));
return list;
}
QRegExp repeatRegEx("(.+) (\\d+)", Qt::CaseInsensitive, QRegExp::RegExp2);
if (repeatRegEx.exactMatch(tmplName)) {
tmplName = repeatRegEx.cap(1);
num = repeatRegEx.cap(2).toInt();
if (num == 0) {
return list;
}
}
if (tmplName.compare("tab", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Tab));
} else if (tmplName.compare("enter", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Enter));
} else if (tmplName.compare("space", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Space));
} else if (tmplName.compare("up", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Up));
} else if (tmplName.compare("down", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Down));
} else if (tmplName.compare("left", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Left));
} else if (tmplName.compare("right", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Right));
} else if (tmplName.compare("insert", Qt::CaseInsensitive) == 0 ||
tmplName.compare("ins", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Insert));
} else if (tmplName.compare("delete", Qt::CaseInsensitive) == 0 ||
tmplName.compare("del", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Delete));
} else if (tmplName.compare("home", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Home));
} else if (tmplName.compare("end", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_End));
} else if (tmplName.compare("pgup", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_PageUp));
} else if (tmplName.compare("pgdown", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_PageDown));
} else if (tmplName.compare("backspace", Qt::CaseInsensitive) == 0 ||
tmplName.compare("bs", Qt::CaseInsensitive) == 0 || tmplName.compare("bksp", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Backspace));
} else if (tmplName.compare("break", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Pause));
} else if (tmplName.compare("capslock", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_CapsLock));
} else if (tmplName.compare("esc", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Escape));
} else if (tmplName.compare("help", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Help));
} else if (tmplName.compare("numlock", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_NumLock));
} else if (tmplName.compare("ptrsc", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_Print));
} else if (tmplName.compare("scrolllock", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeKey(Qt::Key_ScrollLock));
}
// Qt doesn't know about keypad keys so use the normal ones instead
else if (tmplName.compare("add", Qt::CaseInsensitive) == 0 || tmplName.compare("+", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeChar('+'));
} else if (tmplName.compare("subtract", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeChar('-'));
} else if (tmplName.compare("multiply", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeChar('*'));
} else if (tmplName.compare("divide", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeChar('/'));
} else if (tmplName.compare("^", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeChar('^'));
} else if (tmplName.compare("%", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeChar('%'));
} else if (tmplName.compare("~", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeChar('~'));
} else if (tmplName.compare("(", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeChar('('));
} else if (tmplName.compare(")", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeChar(')'));
} else if (tmplName.compare("leftbrace", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeChar('{'));
} else if (tmplName.compare("rightbrace", Qt::CaseInsensitive) == 0) {
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.compare("delay", Qt::CaseInsensitive) == 0 && num > 0) {
list.append(new AutoTypeDelay(num));
} else if (tmplName.compare("clearfield", Qt::CaseInsensitive) == 0) {
list.append(new AutoTypeClearField());
} else if (tmplName.compare("totp", Qt::CaseInsensitive) == 0) {
QString totp = entry->totp();
if (!totp.isEmpty()) {
for (const QChar& ch : totp) {
list.append(new AutoTypeChar(ch));
}
}
}
if (!list.isEmpty()) {
return list;
}
const QString placeholder = QString("{%1}").arg(tmplName);
const QString resolved = entry->resolvePlaceholder(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) {
const QString window = entry->resolveMultiplePlaceholders(assoc.window);
if (windowMatches(windowTitle, window)) {
if (!assoc.sequence.isEmpty()) {
sequence = assoc.sequence;
} else {
sequence = entry->defaultAutoTypeSequence();
}
match = true;
break;
}
}
if (!match && config()->get("AutoTypeEntryTitleMatch").toBool() &&
windowMatchesTitle(windowTitle, entry->resolvePlaceholder(entry->title()))) {
sequence = entry->defaultAutoTypeSequence();
match = true;
}
if (!match && config()->get("AutoTypeEntryURLMatch").toBool() &&
windowMatchesUrl(windowTitle, entry->resolvePlaceholder(entry->url()))) {
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->resolvePlaceholder(entry->username()).isEmpty() ||
!entry->resolvePlaceholder(entry->password()).isEmpty())) {
if (entry->resolvePlaceholder(entry->username()).isEmpty()) {
sequence = "{PASSWORD}{ENTER}";
} else if (entry->resolvePlaceholder(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);
}
}
bool AutoType::windowMatchesTitle(const QString& windowTitle, const QString& resolvedTitle)
{
return !resolvedTitle.isEmpty() && windowTitle.contains(resolvedTitle, Qt::CaseInsensitive);
}
bool AutoType::windowMatchesUrl(const QString& windowTitle, const QString& resolvedUrl)
{
if (!resolvedUrl.isEmpty() && windowTitle.contains(resolvedUrl, Qt::CaseInsensitive)) {
return true;
}
QUrl url(resolvedUrl);
if (url.isValid() && !url.host().isEmpty()) {
return windowTitle.contains(url.host(), Qt::CaseInsensitive);
}
return false;
}
bool AutoType::checkSyntax(const QString& string)
{
// checks things like {word 23}{F1 23}{~ 23}{% 23}{^}{F12}{(}{) 23}{[}{[}{]}{Delay=23}{+}{-}~+%@fixedstring
QString allowRepetition = "(\\s\\d*){0,1}";
QString normalCommands = "[A-Z:]*" + allowRepetition; // the ":" allows custom commands
QString specialLiterals = "[\\^\\%\\(\\)~\\{\\}\\[\\]\\+-]" + allowRepetition;
QString functionKeys = "(F[1-9]" + allowRepetition + "|F1[0-2])" + allowRepetition;
QString numpad = "NUMPAD\\d" + allowRepetition;
QString delay = "DELAY=\\d+";
QString beep = "BEEP\\s\\d*\\s\\d*";
QString vkey = "VKEY(-[EN]X){0,1}" + allowRepetition;
// these arent in parenthesis
QString shortcutKeys = "[\\^\\%~\\+@]";
QString fixedStrings = "[^\\^\\%~\\+@\\{\\}]*";
QRegExp autoTypeSyntax("(" + shortcutKeys + "|" + fixedStrings + "|\\{(" + normalCommands + "|" + specialLiterals +
"|" + functionKeys + "|" + numpad + "|" + delay + "|" + beep + "|" + vkey + ")\\})*");
autoTypeSyntax.setCaseSensitivity(Qt::CaseInsensitive);
autoTypeSyntax.setPatternSyntax(QRegExp::RegExp);
return autoTypeSyntax.exactMatch(string);
}
bool AutoType::checkHighDelay(const QString& string)
{
QRegExp highDelay("\\{DELAY\\s\\d{5,}\\}"); // 5 digit numbers(10 seconds) are too much
highDelay.setCaseSensitivity(Qt::CaseInsensitive);
highDelay.setPatternSyntax(QRegExp::RegExp);
return highDelay.exactMatch(string);
}
bool AutoType::checkHighRepetition(const QString& string)
{
QRegExp highRepetition("\\{(?!DELAY\\s)\\w*\\s\\d{3,}\\}"); // 3 digit numbers are too much
highRepetition.setCaseSensitivity(Qt::CaseInsensitive);
highRepetition.setPatternSyntax(QRegExp::RegExp);
return highRepetition.exactMatch(string);
}
void AutoType::performAutoType(const Entry* entry, QWidget* hideWindow, const QString& customSequence, WId window)
{
if (!AutoType::checkSyntax(entry->effectiveAutoTypeSequence())) {
QMessageBox messageBox;
messageBox.critical(0, tr("AutoType"), tr("The Syntax of your AutoType statement is incorrect!"));
return;
} else if (AutoType::checkHighDelay(entry->effectiveAutoTypeSequence())) {
QMessageBox::StandardButton reply;
reply = QMessageBox::question(
0,
tr("AutoType"),
tr("This AutoType command contains a very long delay. Do you really want to execute it?"));
if (reply == QMessageBox::No) {
return;
}
} else if (AutoType::checkHighRepetition(entry->effectiveAutoTypeSequence())) {
QMessageBox::StandardButton reply;
reply = QMessageBox::question(0, tr("AutoType"), tr("This AutoType command contains arguments which are "
"repeated very often. Do you really want to execute it?"));
if (reply == QMessageBox::No) {
return;
}
}
executeAutoType(entry, hideWindow, customSequence, window);
}