Additional Auto-Type improvements based on PR feedback

* Improve documentation and remove external links to keepass.info documentation
This commit is contained in:
Jonathan White 2021-02-15 17:26:23 -05:00
parent 02446af743
commit 8d058cbd04
No known key found for this signature in database
GPG key ID: 440FC65F2E0C6E01
11 changed files with 292 additions and 194 deletions

View file

@ -20,7 +20,6 @@
#include <QApplication>
#include <QPluginLoader>
#include <QRegularExpression>
#include <QWindow>
#include "config-keepassx.h"
@ -32,7 +31,6 @@
#include "core/Database.h"
#include "core/Entry.h"
#include "core/Group.h"
#include "core/ListDeleter.h"
#include "core/Resources.h"
#include "core/Tools.h"
#include "gui/MainWindow.h"
@ -131,7 +129,7 @@ AutoType::AutoType(QObject* parent, bool test)
// prevent crash when the plugin has unresolved symbols
m_pluginLoader->setLoadHints(QLibrary::ResolveAllSymbolsHint);
QString pluginName = "keepassx-autotype-";
QString pluginName = "keepassxc-autotype-";
if (!test) {
pluginName += QApplication::platformName();
} else {
@ -169,7 +167,7 @@ void AutoType::loadPlugin(const QString& pluginPath)
if (m_plugin) {
if (m_plugin->isAvailable()) {
m_executor = m_plugin->createExecutor();
connect(osUtils, &OSUtilsBase::globalShortcutTriggered, this, [this](QString name) {
connect(osUtils, &OSUtilsBase::globalShortcutTriggered, this, [this](const QString& name) {
if (name == "autotype") {
startGlobalAutoType();
}
@ -249,6 +247,20 @@ void AutoType::unregisterGlobalShortcut()
*/
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;
}
@ -272,11 +284,9 @@ void AutoType::executeAutoTypeActions(const Entry* entry, QWidget* hideWindow, c
#endif
}
auto actions = parseActions(sequence, entry);
// Restore window state in case app stole focus
restoreWindowState();
QApplication::processEvents();
QCoreApplication::processEvents();
m_plugin->raiseWindow(m_windowForGlobal);
// Used only for selected entry auto-type
@ -286,7 +296,7 @@ void AutoType::executeAutoTypeActions(const Entry* entry, QWidget* hideWindow, c
Tools::wait(qMax(100, config()->get(Config::AutoTypeStartDelay).toInt()));
for (auto action : asConst(actions)) {
for (const auto& action : asConst(actions)) {
if (m_plugin->activeWindow() != window) {
qWarning("Active window changed, interrupting auto-type.");
emit autotypeRejected();
@ -301,7 +311,6 @@ void AutoType::executeAutoTypeActions(const Entry* entry, QWidget* hideWindow, c
m_windowForGlobal = 0;
m_windowTitleForGlobal.clear();
// emit signal only if autotype performed correctly
emit autotypePerformed();
m_inAutoType.unlock();
}
@ -426,7 +435,7 @@ void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbLi
selectDialog->setMatches(matchList, dbList);
connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject);
connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](AutoTypeMatch match) {
connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](const AutoTypeMatch& match) {
executeAutoTypeActions(match.first, nullptr, match.second, m_windowForGlobal);
resetAutoTypeState();
});
@ -480,7 +489,7 @@ void AutoType::resetAutoTypeState()
* If error is provided then syntax checking will be performed.
*/
QList<QSharedPointer<AutoTypeAction>>
AutoType::parseActions(const QString& entrySequence, const Entry* entry, QString* error)
AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QString& error, bool syntaxOnly)
{
const int maxTypeDelay = 100;
const int maxWaitDelay = 10000;
@ -495,8 +504,8 @@ AutoType::parseActions(const QString& entrySequence, const Entry* entry, QString
sequence.replace("{}}", "{RIGHTBRACE}");
// Quick test for bracket syntax
if ((sequence.count("{") + sequence.count("}")) % 2 != 0 && error) {
*error = tr("Bracket imbalance detected, found extra { or }");
if (sequence.count("{") != sequence.count("}")) {
error = tr("Bracket imbalance detected, found extra { or }");
return {};
}
@ -530,147 +539,163 @@ AutoType::parseActions(const QString& entrySequence, const Entry* entry, QString
}
auto character = match.captured(5);
if (!placeholder.isEmpty()) {
if (g_placeholderToKey.contains(placeholder)) {
// Basic key placeholder, allow repeat
if (repeat > maxRepetition && error) {
*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) {
*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) {
*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") {
if (error) {
// Ignore this if we are syntax checking
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()) {
auto chars = pickcharsDialog.selectedChars();
auto iter = chars.begin();
while (iter != chars.end()) {
actions << QSharedPointer<AutoTypeKey>::create(*iter);
++iter;
if (pickcharsDialog.pressTab() && iter != chars.end()) {
actions << QSharedPointer<AutoTypeKey>::create(g_placeholderToKey["tab"]);
}
}
}
}
} 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 (placeholder.isEmpty()) {
if (!character.isEmpty()) {
// Type a single character with modifier
actions << QSharedPointer<AutoTypeKey>::create(character[0], modifiers);
}
continue;
}
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 if (error) {
*error = tr("Invalid conversion type: %1").arg(type);
continue;
}
for (const QChar& ch : resolved) {
actions << QSharedPointer<AutoTypeKey>::create(ch);
}
} else if (error) {
*error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder);
}
} else if (error) {
*error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder);
}
} 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>s to support Qt substitutions
resolvedReplace.replace(QRegularExpression("\\$(\\d+)"), "\\\\1");
auto resolved = resolvedText.replace(QRegularExpression(resolvedSearch), resolvedReplace);
for (const QChar& ch : resolved) {
actions << QSharedPointer<AutoTypeKey>::create(ch);
}
} else if (error) {
*error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder);
}
} else if (error) {
*error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder);
}
} 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') {
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"]);
} else {
actions << QSharedPointer<AutoTypeKey>::create(ch);
}
}
} else if (error) {
// Invalid placeholder, issue error and stop processing
*error = tr("Invalid placeholder: %1").arg(fullPlaceholder);
return {};
if (pickcharsDialog.pressTab()) {
// Remove extra tab
actions.removeLast();
}
}
}
} else if (!character.isEmpty()) {
// Type a single character with modifier
actions << QSharedPointer<AutoTypeKey>::create(character[0], modifiers);
} 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 {};
}
}
}
@ -684,7 +709,7 @@ bool AutoType::verifyAutoTypeSyntax(const QString& sequence, const Entry* entry,
{
error.clear();
if (!sequence.isEmpty()) {
parseActions(sequence, entry, &error);
parseSequence(sequence, entry, error, true);
}
return error.isEmpty();
}

View file

@ -86,7 +86,7 @@ private:
void resetAutoTypeState();
static QList<QSharedPointer<AutoTypeAction>>
parseActions(const QString& entrySequence, const Entry* entry, QString* error = nullptr);
parseSequence(const QString& entrySequence, const Entry* entry, QString& error, bool syntaxOnly = false);
QMutex m_inAutoType;
QMutex m_inGlobalAutoTypeDialog;

View file

@ -1,18 +1,18 @@
set(autotype_mac_SOURCES AutoTypeMac.cpp)
add_library(keepassx-autotype-cocoa MODULE ${autotype_mac_SOURCES})
set_target_properties(keepassx-autotype-cocoa PROPERTIES LINK_FLAGS "-framework Foundation -framework AppKit -framework Carbon")
target_link_libraries(keepassx-autotype-cocoa ${PROGNAME} Qt5::Core Qt5::Widgets)
add_library(keepassxc-autotype-cocoa MODULE ${autotype_mac_SOURCES})
set_target_properties(keepassxc-autotype-cocoa PROPERTIES LINK_FLAGS "-framework Foundation -framework AppKit -framework Carbon")
target_link_libraries(keepassxc-autotype-cocoa ${PROGNAME} Qt5::Core Qt5::Widgets)
if(WITH_APP_BUNDLE)
add_custom_command(TARGET keepassx-autotype-cocoa
add_custom_command(TARGET keepassxc-autotype-cocoa
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/libkeepassx-autotype-cocoa.so ${PLUGIN_INSTALL_DIR}/libkeepassx-autotype-cocoa.so
COMMAND ${MACDEPLOYQT_EXE} ${PROGNAME}.app -executable=${PLUGIN_INSTALL_DIR}/libkeepassx-autotype-cocoa.so -no-plugins 2> /dev/null
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/libkeepassxc-autotype-cocoa.so ${PLUGIN_INSTALL_DIR}/libkeepassxc-autotype-cocoa.so
COMMAND ${MACDEPLOYQT_EXE} ${PROGNAME}.app -executable=${PLUGIN_INSTALL_DIR}/libkeepassxc-autotype-cocoa.so -no-plugins 2> /dev/null
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/src
COMMENT "Deploying autotype plugin")
else()
install(TARGETS keepassx-autotype-cocoa
install(TARGETS keepassxc-autotype-cocoa
BUNDLE DESTINATION . COMPONENT Runtime
LIBRARY DESTINATION ${PLUGIN_INSTALL_DIR} COMPONENT Runtime)
endif()

View file

@ -1,4 +1,4 @@
set(autotype_test_SOURCES AutoTypeTest.cpp)
add_library(keepassx-autotype-test MODULE ${autotype_test_SOURCES})
target_link_libraries(keepassx-autotype-test keepassx_core ${autotype_LIB} Qt5::Core Qt5::Widgets)
add_library(keepassxc-autotype-test MODULE ${autotype_test_SOURCES})
target_link_libraries(keepassxc-autotype-test keepassx_core ${autotype_LIB} Qt5::Core Qt5::Widgets)

View file

@ -1,7 +1,7 @@
set(autotype_win_SOURCES AutoTypeWindows.cpp)
add_library(keepassx-autotype-windows MODULE ${autotype_win_SOURCES})
target_link_libraries(keepassx-autotype-windows keepassx_core ${autotype_LIB} Qt5::Core Qt5::Widgets)
install(TARGETS keepassx-autotype-windows
add_library(keepassxc-autotype-windows MODULE ${autotype_win_SOURCES})
target_link_libraries(keepassxc-autotype-windows keepassx_core ${autotype_LIB} Qt5::Core Qt5::Widgets)
install(TARGETS keepassxc-autotype-windows
BUNDLE DESTINATION . COMPONENT Runtime
LIBRARY DESTINATION ${PLUGIN_INSTALL_DIR} COMPONENT Runtime)

View file

@ -2,8 +2,8 @@ include_directories(SYSTEM ${X11_X11_INCLUDE_PATH})
set(autotype_XCB_SOURCES AutoTypeXCB.cpp)
add_library(keepassx-autotype-xcb MODULE ${autotype_XCB_SOURCES})
target_link_libraries(keepassx-autotype-xcb keepassx_core Qt5::Core Qt5::Widgets Qt5::X11Extras ${X11_X11_LIB} ${X11_Xi_LIB} ${X11_XTest_LIB})
install(TARGETS keepassx-autotype-xcb
add_library(keepassxc-autotype-xcb MODULE ${autotype_XCB_SOURCES})
target_link_libraries(keepassxc-autotype-xcb keepassx_core Qt5::Core Qt5::Widgets Qt5::X11Extras ${X11_X11_LIB} ${X11_Xi_LIB} ${X11_XTest_LIB})
install(TARGETS keepassxc-autotype-xcb
BUNDLE DESTINATION . COMPONENT Runtime
LIBRARY DESTINATION ${PLUGIN_INSTALL_DIR} COMPONENT Runtime)