If relock after performing Auto-Type is enabled it will wait until specified timeout before doing so. Retype time is now configurable and is decreased from the old hardcoded 30 seconds down to 15 seconds to keep the default a bit more secure while still allowing the user to set it higher for their liking. To restore old behavior the user can set retype time to 0 which will make the database relock instantly. Auto-Type relock setting relocated to Auto-Type tab to group it better with the other Auto-Type settings.
759 lines
29 KiB
C++
759 lines
29 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 <QDebug>
|
|
#include <QPluginLoader>
|
|
#include <QUrl>
|
|
|
|
#include "config-keepassx.h"
|
|
|
|
#include "autotype/AutoTypePlatformPlugin.h"
|
|
#include "autotype/AutoTypeSelectDialog.h"
|
|
#include "autotype/PickcharsDialog.h"
|
|
#include "core/Resources.h"
|
|
#include "core/Tools.h"
|
|
#include "gui/MainWindow.h"
|
|
#include "gui/MessageBox.h"
|
|
#include "gui/osutils/OSUtils.h"
|
|
|
|
namespace
|
|
{
|
|
// Basic Auto-Type placeholder associations
|
|
const QHash<QString, Qt::Key> g_placeholderToKey = {{"tab", Qt::Key_Tab},
|
|
{"enter", Qt::Key_Enter},
|
|
{"space", Qt::Key_Space},
|
|
{"up", Qt::Key_Up},
|
|
{"down", Qt::Key_Down},
|
|
{"left", Qt::Key_Left},
|
|
{"right", Qt::Key_Right},
|
|
{"insert", Qt::Key_Insert},
|
|
{"ins", Qt::Key_Insert},
|
|
{"delete", Qt::Key_Delete},
|
|
{"del", Qt::Key_Delete},
|
|
{"home", Qt::Key_Home},
|
|
{"end", Qt::Key_End},
|
|
{"pgup", Qt::Key_PageUp},
|
|
{"pgdown", Qt::Key_PageDown},
|
|
{"backspace", Qt::Key_Backspace},
|
|
{"bs", Qt::Key_Backspace},
|
|
{"bksp", Qt::Key_Backspace},
|
|
{"break", Qt::Key_Pause},
|
|
{"capslock", Qt::Key_CapsLock},
|
|
{"esc", Qt::Key_Escape},
|
|
{"help", Qt::Key_Help},
|
|
{"numlock", Qt::Key_NumLock},
|
|
{"ptrsc", Qt::Key_Print},
|
|
{"scrolllock", Qt::Key_ScrollLock},
|
|
{"win", Qt::Key_Meta},
|
|
{"lwin", Qt::Key_Meta},
|
|
{"rwin", Qt::Key_Meta},
|
|
{"apps", Qt::Key_Menu},
|
|
{"help", Qt::Key_Help},
|
|
{"add", Qt::Key_Plus},
|
|
{"subtract", Qt::Key_Minus},
|
|
{"multiply", Qt::Key_Asterisk},
|
|
{"divide", Qt::Key_Slash},
|
|
{"leftbrace", Qt::Key_BraceLeft},
|
|
{"{", Qt::Key_BraceLeft},
|
|
{"rightbrace", Qt::Key_BraceRight},
|
|
{"}", Qt::Key_BraceRight},
|
|
{"leftparen", Qt::Key_ParenLeft},
|
|
{"(", Qt::Key_ParenLeft},
|
|
{"rightparen", Qt::Key_ParenRight},
|
|
{")", Qt::Key_ParenRight},
|
|
{"[", Qt::Key_BracketLeft},
|
|
{"]", Qt::Key_BracketRight},
|
|
{"+", Qt::Key_Plus},
|
|
{"%", Qt::Key_Percent},
|
|
{"^", Qt::Key_AsciiCircum},
|
|
{"~", Qt::Key_AsciiTilde},
|
|
{"numpad0", Qt::Key_0},
|
|
{"numpad1", Qt::Key_1},
|
|
{"numpad2", Qt::Key_2},
|
|
{"numpad3", Qt::Key_3},
|
|
{"numpad4", Qt::Key_4},
|
|
{"numpad5", Qt::Key_5},
|
|
{"numpad6", Qt::Key_6},
|
|
{"numpad7", Qt::Key_7},
|
|
{"numpad8", Qt::Key_8},
|
|
{"numpad9", Qt::Key_9},
|
|
{"f1", Qt::Key_F1},
|
|
{"f2", Qt::Key_F2},
|
|
{"f3", Qt::Key_F3},
|
|
{"f4", Qt::Key_F4},
|
|
{"f5", Qt::Key_F5},
|
|
{"f6", Qt::Key_F6},
|
|
{"f7", Qt::Key_F7},
|
|
{"f8", Qt::Key_F8},
|
|
{"f9", Qt::Key_F9},
|
|
{"f10", Qt::Key_F10},
|
|
{"f11", Qt::Key_F11},
|
|
{"f12", Qt::Key_F12},
|
|
{"f13", Qt::Key_F13},
|
|
{"f14", Qt::Key_F14},
|
|
{"f15", Qt::Key_F15},
|
|
{"f16", Qt::Key_F16}};
|
|
} // namespace
|
|
|
|
AutoType* AutoType::m_instance = nullptr;
|
|
|
|
AutoType::AutoType(QObject* parent, bool test)
|
|
: QObject(parent)
|
|
, m_pluginLoader(new QPluginLoader(this))
|
|
, m_plugin(nullptr)
|
|
, m_executor(nullptr)
|
|
, m_windowState(WindowState::Normal)
|
|
, m_windowForGlobal(0)
|
|
, m_lastMatch(nullptr, QString())
|
|
, m_lastMatchRetypeTimer(nullptr)
|
|
{
|
|
// configure timer to reset last match
|
|
m_lastMatchRetypeTimer.setSingleShot(true);
|
|
connect(&m_lastMatchRetypeTimer, &QTimer::timeout, this, [this] {
|
|
m_lastMatch = {nullptr, QString()};
|
|
emit autotypeRetypeTimeout();
|
|
});
|
|
|
|
// prevent crash when the plugin has unresolved symbols
|
|
m_pluginLoader->setLoadHints(QLibrary::ResolveAllSymbolsHint);
|
|
|
|
QString pluginName = "keepassxc-autotype-";
|
|
if (!test) {
|
|
pluginName += QApplication::platformName();
|
|
} else {
|
|
pluginName += "test";
|
|
}
|
|
|
|
QString pluginPath = resources()->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(osUtils,
|
|
&OSUtilsBase::globalShortcutTriggered,
|
|
this,
|
|
[this](const QString& name, const QString& initialSearch) {
|
|
if (name == "autotype") {
|
|
startGlobalAutoType(initialSearch);
|
|
}
|
|
});
|
|
} else {
|
|
unloadPlugin();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!m_plugin) {
|
|
qWarning("Unable to load auto-type plugin:\n%s", qPrintable(m_pluginLoader->errorString()));
|
|
}
|
|
}
|
|
|
|
void AutoType::unloadPlugin()
|
|
{
|
|
if (m_executor) {
|
|
delete m_executor;
|
|
m_executor = nullptr;
|
|
}
|
|
|
|
if (m_plugin) {
|
|
m_plugin->unload();
|
|
m_plugin = nullptr;
|
|
}
|
|
}
|
|
|
|
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::raiseWindow()
|
|
{
|
|
#if defined(Q_OS_MACOS)
|
|
m_plugin->raiseOwnWindow();
|
|
#endif
|
|
}
|
|
|
|
bool AutoType::registerGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers, QString* error)
|
|
{
|
|
if (!m_plugin) {
|
|
return false;
|
|
}
|
|
|
|
return osUtils->registerGlobalShortcut("autotype", key, modifiers, error);
|
|
}
|
|
|
|
void AutoType::unregisterGlobalShortcut()
|
|
{
|
|
osUtils->unregisterGlobalShortcut("autotype");
|
|
}
|
|
|
|
/**
|
|
* Core Autotype function that will execute actions
|
|
*/
|
|
void AutoType::executeAutoTypeActions(const Entry* entry, QWidget* hideWindow, const QString& sequence, WId window)
|
|
{
|
|
QString error;
|
|
auto actions = parseSequence(sequence, entry, error);
|
|
|
|
if (!error.isEmpty()) {
|
|
auto errorMsg = tr("The requested Auto-Type sequence cannot be used due to an error:");
|
|
errorMsg.append(QString("\n%1\n%2").arg(sequence, error));
|
|
if (getMainWindow()) {
|
|
MessageBox::critical(getMainWindow(), tr("Auto-Type Error"), errorMsg);
|
|
}
|
|
qWarning() << errorMsg;
|
|
emit autotypeRejected();
|
|
return;
|
|
}
|
|
|
|
if (!m_inAutoType.tryLock()) {
|
|
return;
|
|
}
|
|
|
|
if (hideWindow) {
|
|
#if defined(Q_OS_MACOS)
|
|
// Check for accessibility permission
|
|
if (!macUtils()->enableAccessibility()) {
|
|
MessageBox::information(nullptr,
|
|
tr("Permission Required"),
|
|
tr("KeePassXC requires the Accessibility permission in order to perform entry "
|
|
"level Auto-Type. If you already granted permission, you may have to restart "
|
|
"KeePassXC."));
|
|
return;
|
|
}
|
|
|
|
macUtils()->raiseLastActiveWindow();
|
|
m_plugin->hideOwnWindow();
|
|
#else
|
|
getMainWindow()->minimizeOrHide();
|
|
#endif
|
|
}
|
|
|
|
// Restore window state in case app stole focus
|
|
restoreWindowState();
|
|
QCoreApplication::processEvents();
|
|
m_plugin->raiseWindow(m_windowForGlobal);
|
|
|
|
// Used only for selected entry auto-type
|
|
if (!window) {
|
|
window = m_plugin->activeWindow();
|
|
}
|
|
|
|
int delay = qMax(100, config()->get(Config::AutoTypeStartDelay).toInt());
|
|
Tools::wait(delay);
|
|
|
|
for (const auto& action : asConst(actions)) {
|
|
if (m_plugin->activeWindow() != window) {
|
|
qWarning("Active window changed, interrupting auto-type.");
|
|
emit autotypeRejected();
|
|
m_inAutoType.unlock();
|
|
return;
|
|
}
|
|
|
|
constexpr int max_retries = 5;
|
|
for (int i = 1; i <= max_retries; i++) {
|
|
auto result = action->exec(m_executor);
|
|
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
|
|
|
|
if (result.isOk()) {
|
|
break;
|
|
}
|
|
|
|
if (!result.canRetry() || i == max_retries) {
|
|
MessageBox::critical(getMainWindow(), tr("Auto-Type Error"), result.errorString());
|
|
emit autotypeRejected();
|
|
m_inAutoType.unlock();
|
|
return;
|
|
}
|
|
|
|
Tools::wait(delay);
|
|
}
|
|
}
|
|
|
|
m_windowForGlobal = 0;
|
|
m_windowTitleForGlobal.clear();
|
|
|
|
emit autotypePerformed();
|
|
m_inAutoType.unlock();
|
|
}
|
|
|
|
/**
|
|
* Single Autotype entry-point function
|
|
* Look up the Auto-Type sequence for the given entry then perfom Auto-Type in the active window
|
|
*/
|
|
void AutoType::performAutoType(const Entry* entry, QWidget* hideWindow)
|
|
{
|
|
if (!m_plugin) {
|
|
return;
|
|
}
|
|
|
|
auto sequences = entry->autoTypeSequences();
|
|
if (!sequences.isEmpty()) {
|
|
executeAutoTypeActions(entry, hideWindow, sequences.first());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extra Autotype entry-point function
|
|
* Perfom Auto-Type of the directly specified sequence in the active window
|
|
*/
|
|
void AutoType::performAutoTypeWithSequence(const Entry* entry, const QString& sequence, QWidget* hideWindow)
|
|
{
|
|
if (!m_plugin) {
|
|
return;
|
|
}
|
|
|
|
executeAutoTypeActions(entry, hideWindow, sequence);
|
|
}
|
|
|
|
void AutoType::startGlobalAutoType(const QString& search)
|
|
{
|
|
// Never Auto-Type into KeePassXC itself
|
|
if (qApp->focusWindow()) {
|
|
return;
|
|
}
|
|
|
|
m_windowForGlobal = m_plugin->activeWindow();
|
|
m_windowTitleForGlobal = m_plugin->activeWindowTitle();
|
|
#ifdef Q_OS_MACOS
|
|
// Determine if the user has given proper permissions to KeePassXC to perform Auto-Type
|
|
static bool accessibilityChecked = false;
|
|
if (!accessibilityChecked) {
|
|
if (macUtils()->enableAccessibility() && macUtils()->enableScreenRecording()) {
|
|
accessibilityChecked = true;
|
|
} else if (getMainWindow()) {
|
|
// Does not have required permissions to Auto-Type, ignore the event
|
|
MessageBox::information(
|
|
nullptr,
|
|
tr("Permission Required"),
|
|
tr("KeePassXC requires the Accessibility and Screen Recorder permission in order to perform global "
|
|
"Auto-Type. Screen Recording is necessary to use the window title to find entries. If you "
|
|
"already granted permission, you may have to restart KeePassXC."));
|
|
qDebug() << "Oh noes macOS.";
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_windowState = WindowState::Normal;
|
|
if (getMainWindow()) {
|
|
if (getMainWindow()->isMinimized()) {
|
|
m_windowState = WindowState::Minimized;
|
|
}
|
|
if (getMainWindow()->isHidden()) {
|
|
m_windowState = WindowState::Hidden;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
emit globalAutoTypeTriggered(search);
|
|
}
|
|
|
|
/**
|
|
* Global Autotype entry-point function
|
|
* Perform global Auto-Type on the active window
|
|
*/
|
|
void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbList, const QString& search)
|
|
{
|
|
if (!m_plugin) {
|
|
return;
|
|
}
|
|
|
|
if (!m_inGlobalAutoTypeDialog.tryLock()) {
|
|
return;
|
|
}
|
|
|
|
if (m_windowTitleForGlobal.isEmpty()) {
|
|
m_inGlobalAutoTypeDialog.unlock();
|
|
return;
|
|
}
|
|
|
|
QList<AutoTypeMatch> matchList;
|
|
bool hideExpired = config()->get(Config::AutoTypeHideExpiredEntry).toBool();
|
|
|
|
for (const auto& db : dbList) {
|
|
const QList<Entry*> dbEntries = db->rootGroup()->entriesRecursive();
|
|
for (auto entry : dbEntries) {
|
|
auto group = entry->group();
|
|
if (!group || !group->resolveAutoTypeEnabled() || !entry->autoTypeEnabled()) {
|
|
continue;
|
|
}
|
|
|
|
if (hideExpired && entry->isExpired()) {
|
|
continue;
|
|
}
|
|
auto sequences = entry->autoTypeSequences(m_windowTitleForGlobal).toSet();
|
|
for (const auto& sequence : sequences) {
|
|
matchList << AutoTypeMatch(entry, sequence);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show the selection dialog if we always ask, have multiple matches, or no matches
|
|
if (getMainWindow()
|
|
&& (config()->get(Config::Security_AutoTypeAsk).toBool() || matchList.size() > 1 || matchList.isEmpty())) {
|
|
// Close any open modal windows that would interfere with the process
|
|
getMainWindow()->closeModalWindow();
|
|
|
|
auto* selectDialog = new AutoTypeSelectDialog();
|
|
selectDialog->setMatches(matchList, dbList, m_lastMatch);
|
|
|
|
if (!search.isEmpty()) {
|
|
selectDialog->setSearchString(search);
|
|
}
|
|
|
|
connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject);
|
|
connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](const AutoTypeMatch& match) {
|
|
m_lastMatch = match;
|
|
m_lastMatchRetypeTimer.start(config()->get(Config::GlobalAutoTypeRetypeTime).toInt() * 1000);
|
|
executeAutoTypeActions(match.first, nullptr, match.second, m_windowForGlobal);
|
|
resetAutoTypeState();
|
|
});
|
|
connect(selectDialog, &QDialog::rejected, this, [this] {
|
|
restoreWindowState();
|
|
resetAutoTypeState();
|
|
emit autotypeRejected();
|
|
});
|
|
|
|
#ifdef Q_OS_MACOS
|
|
m_plugin->raiseOwnWindow();
|
|
Tools::wait(50);
|
|
#endif
|
|
selectDialog->show();
|
|
selectDialog->raise();
|
|
selectDialog->activateWindow();
|
|
} else if (!matchList.isEmpty()) {
|
|
// Only one match and not asking, do it!
|
|
executeAutoTypeActions(matchList.first().first, nullptr, matchList.first().second, m_windowForGlobal);
|
|
resetAutoTypeState();
|
|
} else {
|
|
// We should never get here
|
|
resetAutoTypeState();
|
|
emit autotypeRejected();
|
|
}
|
|
}
|
|
|
|
void AutoType::restoreWindowState()
|
|
{
|
|
#ifdef Q_OS_MAC
|
|
if (getMainWindow()) {
|
|
if (m_windowState == WindowState::Minimized) {
|
|
getMainWindow()->showMinimized();
|
|
} else if (m_windowState == WindowState::Hidden) {
|
|
getMainWindow()->hideWindow();
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void AutoType::resetAutoTypeState()
|
|
{
|
|
m_windowForGlobal = 0;
|
|
m_windowTitleForGlobal.clear();
|
|
Q_UNUSED(m_inGlobalAutoTypeDialog.tryLock());
|
|
m_inGlobalAutoTypeDialog.unlock();
|
|
}
|
|
|
|
/**
|
|
* Parse an autotype sequence into a list of AutoTypeActions.
|
|
* If error is provided then syntax checking will be performed.
|
|
*/
|
|
QList<QSharedPointer<AutoTypeAction>>
|
|
AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QString& error, bool syntaxOnly)
|
|
{
|
|
if (!entry) {
|
|
error = tr("Invalid entry provided");
|
|
return {};
|
|
}
|
|
|
|
const int maxTypeDelay = 500;
|
|
const int maxWaitDelay = 10000;
|
|
const int maxRepetition = 100;
|
|
|
|
QList<QSharedPointer<AutoTypeAction>> actions;
|
|
actions << QSharedPointer<AutoTypeBegin>::create();
|
|
actions << QSharedPointer<AutoTypeDelay>::create(qMax(0, config()->get(Config::AutoTypeDelay).toInt()), true);
|
|
|
|
// Replace escaped braces with a template for easier regex
|
|
QString sequence = entrySequence;
|
|
sequence.replace("{{}", "{LEFTBRACE}");
|
|
sequence.replace("{}}", "{RIGHTBRACE}");
|
|
|
|
// Quick test for bracket syntax
|
|
if (sequence.count("{") != sequence.count("}")) {
|
|
error = tr("Bracket imbalance detected, found extra { or }");
|
|
return {};
|
|
}
|
|
|
|
// Group 1 = modifier key (opt)
|
|
// Group 2 = full placeholder
|
|
// Group 3 = inner placeholder (allows nested placeholders)
|
|
// Group 4 = repeat (opt)
|
|
// Group 5 = character
|
|
QRegularExpression regex("([+%^#]*)(?:({((?>[^{}]+?|(?2))+?)(?:\\s+(\\d+))?})|(.))");
|
|
auto results = regex.globalMatch(sequence);
|
|
while (results.hasNext()) {
|
|
auto match = results.next();
|
|
|
|
// Parse modifier keys
|
|
Qt::KeyboardModifiers modifiers;
|
|
if (match.captured(1).contains('+')) {
|
|
modifiers |= Qt::ShiftModifier;
|
|
}
|
|
if (match.captured(1).contains('^')) {
|
|
modifiers |= Qt::ControlModifier;
|
|
}
|
|
if (match.captured(1).contains('%')) {
|
|
modifiers |= Qt::AltModifier;
|
|
}
|
|
if (match.captured(1).contains('#')) {
|
|
modifiers |= Qt::MetaModifier;
|
|
}
|
|
|
|
const auto fullPlaceholder = match.captured(2);
|
|
auto placeholder = match.captured(3).toLower();
|
|
auto repeat = 1;
|
|
if (!match.captured(4).isEmpty()) {
|
|
repeat = match.captured(4).toInt();
|
|
}
|
|
auto character = match.captured(5);
|
|
|
|
if (placeholder.isEmpty()) {
|
|
if (!character.isEmpty()) {
|
|
// Type a single character with modifier
|
|
actions << QSharedPointer<AutoTypeKey>::create(character[0], modifiers);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (g_placeholderToKey.contains(placeholder)) {
|
|
// Basic key placeholder, allow repeat
|
|
if (repeat > maxRepetition) {
|
|
error = tr("Too many repetitions detected, max is %1: %2").arg(maxRepetition).arg(fullPlaceholder);
|
|
return {};
|
|
}
|
|
auto action = QSharedPointer<AutoTypeKey>::create(g_placeholderToKey[placeholder], modifiers);
|
|
for (int i = 1; i <= repeat && i <= maxRepetition; ++i) {
|
|
actions << action;
|
|
}
|
|
} else if (placeholder.startsWith("delay=")) {
|
|
// Change keypress delay
|
|
int delay = placeholder.replace("delay=", "").toInt();
|
|
if (delay > maxTypeDelay) {
|
|
error = tr("Very slow key press detected, max is %1: %2").arg(maxTypeDelay).arg(fullPlaceholder);
|
|
return {};
|
|
}
|
|
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, delay, maxTypeDelay), true);
|
|
} else if (placeholder == "delay") {
|
|
// Mid typing delay (wait)
|
|
if (repeat > maxWaitDelay) {
|
|
error = tr("Very long delay detected, max is %1: %2").arg(maxWaitDelay).arg(fullPlaceholder);
|
|
return {};
|
|
}
|
|
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, repeat, maxWaitDelay));
|
|
} else if (placeholder == "clearfield") {
|
|
// Platform-specific field clearing
|
|
actions << QSharedPointer<AutoTypeClearField>::create();
|
|
} else if (placeholder == "totp") {
|
|
// Entry totp (requires special handling)
|
|
QString totp = entry->totp();
|
|
for (const auto& ch : totp) {
|
|
actions << QSharedPointer<AutoTypeKey>::create(ch);
|
|
}
|
|
} else if (placeholder == "pickchars") {
|
|
// Ignore this if we are syntax checking
|
|
if (syntaxOnly) {
|
|
continue;
|
|
}
|
|
// Show pickchars dialog for entry's password
|
|
auto password = entry->resolvePlaceholder(entry->password());
|
|
if (!password.isEmpty()) {
|
|
PickcharsDialog pickcharsDialog(password);
|
|
if (pickcharsDialog.exec() == QDialog::Accepted && !pickcharsDialog.selectedChars().isEmpty()) {
|
|
for (const auto& ch : pickcharsDialog.selectedChars()) {
|
|
actions << QSharedPointer<AutoTypeKey>::create(ch);
|
|
if (pickcharsDialog.pressTab()) {
|
|
actions << QSharedPointer<AutoTypeKey>::create(g_placeholderToKey["tab"]);
|
|
}
|
|
}
|
|
if (pickcharsDialog.pressTab()) {
|
|
// Remove extra tab
|
|
actions.removeLast();
|
|
}
|
|
}
|
|
}
|
|
} else if (placeholder.startsWith("t-conv:")) {
|
|
// Reset to the original capture to preserve case
|
|
placeholder = match.captured(3);
|
|
placeholder.replace("t-conv:", "", Qt::CaseInsensitive);
|
|
if (!placeholder.isEmpty()) {
|
|
auto sep = placeholder[0];
|
|
auto parts = placeholder.split(sep);
|
|
if (parts.size() >= 4) {
|
|
auto resolved = entry->resolveMultiplePlaceholders(parts[1]);
|
|
auto type = parts[2].toLower();
|
|
|
|
if (type == "base64") {
|
|
resolved = resolved.toUtf8().toBase64();
|
|
} else if (type == "hex") {
|
|
resolved = resolved.toUtf8().toHex();
|
|
} else if (type == "uri") {
|
|
resolved = QUrl::toPercentEncoding(resolved.toUtf8());
|
|
} else if (type == "uri-dec") {
|
|
resolved = QUrl::fromPercentEncoding(resolved.toUtf8());
|
|
} else if (type.startsWith("u")) {
|
|
resolved = resolved.toUpper();
|
|
} else if (type.startsWith("l")) {
|
|
resolved = resolved.toLower();
|
|
} else {
|
|
error = tr("Invalid conversion type: %1").arg(type);
|
|
return {};
|
|
}
|
|
for (const QChar& ch : resolved) {
|
|
actions << QSharedPointer<AutoTypeKey>::create(ch);
|
|
}
|
|
} else {
|
|
error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder);
|
|
return {};
|
|
}
|
|
} else {
|
|
error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder);
|
|
return {};
|
|
}
|
|
} else if (placeholder.startsWith("t-replace-rx:")) {
|
|
// Reset to the original capture to preserve case
|
|
placeholder = match.captured(3);
|
|
placeholder.replace("t-replace-rx:", "", Qt::CaseInsensitive);
|
|
if (!placeholder.isEmpty()) {
|
|
auto sep = placeholder[0];
|
|
auto parts = placeholder.split(sep);
|
|
if (parts.size() >= 5) {
|
|
auto resolvedText = entry->resolveMultiplePlaceholders(parts[1]);
|
|
auto resolvedSearch = entry->resolveMultiplePlaceholders(parts[2]);
|
|
auto resolvedReplace = entry->resolveMultiplePlaceholders(parts[3]);
|
|
// Replace $<num> with \\<num> to support Qt substitutions
|
|
resolvedReplace.replace(QRegularExpression(R"(\$(\d+))"), R"(\\1)");
|
|
|
|
auto searchRegex = QRegularExpression(resolvedSearch);
|
|
if (!searchRegex.isValid()) {
|
|
error = tr("Invalid regular expression syntax %1\n%2")
|
|
.arg(resolvedSearch, searchRegex.errorString());
|
|
return {};
|
|
}
|
|
|
|
auto resolved = resolvedText.replace(searchRegex, resolvedReplace);
|
|
for (const QChar& ch : resolved) {
|
|
actions << QSharedPointer<AutoTypeKey>::create(ch);
|
|
}
|
|
} else {
|
|
error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder);
|
|
return {};
|
|
}
|
|
} else {
|
|
error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder);
|
|
return {};
|
|
}
|
|
} else if (placeholder == "beep" || placeholder.startsWith("vkey") || placeholder.startsWith("appactivate")
|
|
|| placeholder.startsWith("c:")) {
|
|
// Ignore these commands
|
|
} else {
|
|
// Attempt to resolve an entry attribute
|
|
auto resolved = entry->resolvePlaceholder(fullPlaceholder);
|
|
if (resolved != fullPlaceholder) {
|
|
// Attribute resolved, add characters to action list
|
|
for (const QChar& ch : resolved) {
|
|
if (ch == '\n') {
|
|
actions << QSharedPointer<AutoTypeKey>::create(g_placeholderToKey["enter"]);
|
|
} else if (ch == '\t') {
|
|
actions << QSharedPointer<AutoTypeKey>::create(g_placeholderToKey["tab"]);
|
|
} else {
|
|
actions << QSharedPointer<AutoTypeKey>::create(ch);
|
|
}
|
|
}
|
|
} else {
|
|
// Invalid placeholder, issue error and stop processing
|
|
error = tr("Invalid placeholder: %1").arg(fullPlaceholder);
|
|
return {};
|
|
}
|
|
}
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
/**
|
|
* Verify if the syntax of an autotype sequence is correct and doesn't have invalid parameters
|
|
*/
|
|
bool AutoType::verifyAutoTypeSyntax(const QString& sequence, const Entry* entry, QString& error)
|
|
{
|
|
error.clear();
|
|
if (!sequence.isEmpty()) {
|
|
parseSequence(sequence, entry, error, true);
|
|
}
|
|
return error.isEmpty();
|
|
}
|