diff --git a/CMakeLists.txt b/CMakeLists.txt index 95d6e0b96..c02d29142 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,11 +121,6 @@ if(NOT WITH_XC_NETWORKING AND WITH_XC_UPDATECHECK) set(WITH_XC_UPDATECHECK OFF) endif() -if(UNIX AND NOT APPLE AND NOT WITH_XC_X11) - message(STATUS "Disabling WITH_XC_AUTOTYPE because WITH_XC_X11 is disabled") - set(WITH_XC_AUTOTYPE OFF) -endif() - set(KEEPASSXC_VERSION_MAJOR "2") set(KEEPASSXC_VERSION_MINOR "8") set(KEEPASSXC_VERSION_PATCH "0") diff --git a/cmake/FindXkbcommon.cmake b/cmake/FindXkbcommon.cmake new file mode 100644 index 000000000..7e8badec4 --- /dev/null +++ b/cmake/FindXkbcommon.cmake @@ -0,0 +1,34 @@ +# Copyright (C) 2021 KeePassXC Team +# +# 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 . + +# This only works with CMake 3.18 and newer +if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.18") + find_package(PkgConfig QUIET) + pkg_check_modules(Xkbcommon xkbcommon) +endif() + +if(NOT Xkbcommon_FOUND) + find_path(Xkbcommon_INCLUDE_DIRS xkbcommon/xkbcommon.h) + find_library(Xkbcommon_LIBRARIES xkbcommon) + + if(Xkbcommon_INCLUDE_DIRS AND Xkbcommon_LIBRARIES) + set(Xkbcommon_FOUND TRUE) + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Xkbcommon DEFAULT_MSG Xkbcommon_LIBRARIES Xkbcommon_INCLUDE_DIRS) + +mark_as_advanced(Xkbcommon_LIBRARIES Xkbcommon_INCLUDE_DIRS) diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index 44815fa6f..cd0598f7d 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -437,6 +437,15 @@ void AutoType::performGlobalAutoType(const QList>& dbLi return; } + if (!m_inGlobalAutoTypeDialog.tryLock()) { + return; + } + + if (m_windowTitleForGlobal.isEmpty() && QApplication::platformName().compare("wayland", Qt::CaseInsensitive) != 0) { + m_inGlobalAutoTypeDialog.unlock(); + return; + } + QList matchList; // Generate entry/sequence match list if there is a valid window title if (!m_windowTitleForGlobal.isEmpty()) { diff --git a/src/autotype/CMakeLists.txt b/src/autotype/CMakeLists.txt index 79bb50372..c859839e1 100644 --- a/src/autotype/CMakeLists.txt +++ b/src/autotype/CMakeLists.txt @@ -1,14 +1,7 @@ if(WITH_XC_AUTOTYPE) if(UNIX AND NOT APPLE AND NOT HAIKU) - find_package(X11 REQUIRED COMPONENTS Xi XTest) - find_package(Qt5X11Extras 5.2 REQUIRED) - if(PRINT_SUMMARY) - add_feature_info(libXi X11_Xi_FOUND "The X11 Xi Protocol library is required for auto-type") - add_feature_info(libXtst X11_XTest_FOUND "The X11 XTEST Protocol library is required for auto-type") - add_feature_info(Qt5X11Extras Qt5X11Extras_FOUND "The Qt5X11Extras library is required for auto-type") - endif() - add_subdirectory(xcb) + add_subdirectory(wayland) elseif(APPLE) add_subdirectory(mac) elseif(WIN32) diff --git a/src/autotype/wayland/AutoTypeWayland.cpp b/src/autotype/wayland/AutoTypeWayland.cpp new file mode 100644 index 000000000..1b593940f --- /dev/null +++ b/src/autotype/wayland/AutoTypeWayland.cpp @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#include "AutoTypeWayland.h" + +#include "autotype/AutoTypeAction.h" +#include "core/Tools.h" +#include "gui/osutils/nixutils/X11Funcs.h" + +#include +#include +#include + +QString generateToken() +{ + static uint next = 0; + return QString("keepassxc_%1_%2").arg(next++).arg(QRandomGenerator::system()->generate()); +} + +AutoTypePlatformWayland::AutoTypePlatformWayland() + : m_bus(QDBusConnection::sessionBus()) + , m_remote_desktop("org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.RemoteDesktop", + m_bus, + this) +{ + m_bus.connect("org.freedesktop.portal.Desktop", + "", + "org.freedesktop.portal.Request", + "Response", + this, + SLOT(portalResponse(uint, QVariantMap, QDBusMessage))); + + createSession(); +} + +void AutoTypePlatformWayland::createSession() +{ + QString requestHandle = generateToken(); + + m_handlers.insert(requestHandle, + [this](uint _response, QVariantMap _result) { handleCreateSession(_response, _result); }); + + m_remote_desktop.call("CreateSession", + QVariantMap{{"handle_token", requestHandle}, {"session_handle_token", generateToken()}}); +} + +void AutoTypePlatformWayland::handleCreateSession(uint response, QVariantMap result) +{ + qDebug() << "Got response and result" << response << result; + if (response == 0) { + m_session_handle = QDBusObjectPath(result["session_handle"].toString()); + + QString selectDevicesRequestHandle = generateToken(); + m_handlers.insert(selectDevicesRequestHandle, + [this](uint _response, QVariantMap _result) { handleSelectDevices(_response, _result); }); + + QVariantMap selectDevicesOptions{ + {"handle_token", selectDevicesRequestHandle}, + {"types", uint(1)}, + {"persist_mode", uint(2)}, + }; + + // TODO: Store restore token in database/some other persistent data so the dialog doesn't appear every launch + if (!m_restore_token.isEmpty()) { + selectDevicesOptions.insert("restore_token", m_restore_token); + } + + m_remote_desktop.call("SelectDevices", QVariant::fromValue(m_session_handle), selectDevicesOptions); + + QString startRequestHandle = generateToken(); + m_handlers.insert(startRequestHandle, + [this](uint _response, QVariantMap _result) { handleStart(_response, _result); }); + + QVariantMap startOptions{ + {"handle_token", startRequestHandle}, + }; + + // TODO: Pass window identifier here instead of empty string if we want the dialog to appear on top of the + // application window, need to be able to get active window and handle from Wayland + m_remote_desktop.call("Start", QVariant::fromValue(m_session_handle), "", startOptions); + } +} + +void AutoTypePlatformWayland::handleSelectDevices(uint response, QVariantMap result) +{ + Q_UNUSED(result) + qDebug() << "Select Devices: " << response << result; +} + +void AutoTypePlatformWayland::handleStart(uint response, QVariantMap result) +{ + qDebug() << "Start: " << response << result; + if (response == 0) { + m_session_started = true; + m_restore_token = result["restore_token"].toString(); + } +} + +void AutoTypePlatformWayland::portalResponse(uint response, QVariantMap results, QDBusMessage message) +{ + Q_UNUSED(response) + Q_UNUSED(results) + qDebug() << "Received message: " << message; + auto index = message.path().lastIndexOf("/"); + auto handle = message.path().right(message.path().length() - index - 1); + if (m_handlers.contains(handle)) { + m_handlers.take(handle)(response, results); + } +} + +AutoTypeAction::Result AutoTypePlatformWayland::sendKey(xkb_keysym_t keysym, QVector modifiers) +{ + for (auto modifier : modifiers) { + m_remote_desktop.call( + "NotifyKeyboardKeysym", QVariant::fromValue(m_session_handle), QVariantMap(), int(modifier), uint(1)); + } + + m_remote_desktop.call( + "NotifyKeyboardKeysym", QVariant::fromValue(m_session_handle), QVariantMap(), int(keysym), uint(1)); + + m_remote_desktop.call( + "NotifyKeyboardKeysym", QVariant::fromValue(m_session_handle), QVariantMap(), int(keysym), uint(0)); + + for (auto modifier : modifiers) { + m_remote_desktop.call( + "NotifyKeyboardKeysym", QVariant::fromValue(m_session_handle), QVariantMap(), int(modifier), uint(0)); + } + return AutoTypeAction::Result::Ok(); +} + +bool AutoTypePlatformWayland::isAvailable() +{ + return true; +} + +void AutoTypePlatformWayland::unload() +{ +} + +QString AutoTypePlatformWayland::activeWindowTitle() +{ + return {}; +} + +WId AutoTypePlatformWayland::activeWindow() +{ + return 0; +} + +AutoTypeExecutor* AutoTypePlatformWayland::createExecutor() +{ + return new AutoTypeExecutorWayland(this); +} + +bool AutoTypePlatformWayland::raiseWindow(WId window) +{ + Q_UNUSED(window) + return false; +} + +QStringList AutoTypePlatformWayland::windowTitles() +{ + return {}; +} + +AutoTypeExecutorWayland::AutoTypeExecutorWayland(AutoTypePlatformWayland* platform) + : m_platform(platform) +{ +} + +AutoTypeAction::Result AutoTypeExecutorWayland::execBegin(const AutoTypeBegin* action) +{ + Q_UNUSED(action) + return AutoTypeAction::Result::Ok(); +} + +AutoTypeAction::Result AutoTypeExecutorWayland::execType(const AutoTypeKey* action) +{ + Q_UNUSED(action) + + QVector modifiers{}; + + if (action->modifiers.testFlag(Qt::ShiftModifier)) { + modifiers.append(XKB_KEY_Shift_L); + } + if (action->modifiers.testFlag(Qt::ControlModifier)) { + modifiers.append(XKB_KEY_Control_L); + } + if (action->modifiers.testFlag(Qt::AltModifier)) { + modifiers.append(XKB_KEY_Alt_L); + } + if (action->modifiers.testFlag(Qt::MetaModifier)) { + modifiers.append(XKB_KEY_Meta_L); + } + + // TODO: Replace these with proper lookups to xkbcommon keysyms instead of just reusing the X11 ones + // They're mostly the same for most things, but strictly speaking differ slightly + if (action->key != Qt::Key_unknown) { + m_platform->sendKey(qtToNativeKeyCode(action->key), modifiers); + } else { + m_platform->sendKey(qcharToNativeKeyCode(action->character), modifiers); + } + + Tools::sleep(execDelayMs); + + return AutoTypeAction::Result::Ok(); +} + +AutoTypeAction::Result AutoTypeExecutorWayland::execClearField(const AutoTypeClearField* action) +{ + Q_UNUSED(action) + execType(new AutoTypeKey(Qt::Key_Home)); + execType(new AutoTypeKey(Qt::Key_End, Qt::ShiftModifier)); + execType(new AutoTypeKey(Qt::Key_Backspace)); + + return AutoTypeAction::Result::Ok(); +} diff --git a/src/autotype/wayland/AutoTypeWayland.h b/src/autotype/wayland/AutoTypeWayland.h new file mode 100644 index 000000000..d8975718f --- /dev/null +++ b/src/autotype/wayland/AutoTypeWayland.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include + +#include + +#include "autotype/AutoTypePlatformPlugin.h" + +class AutoTypePlatformWayland : public QObject, public AutoTypePlatformInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.keepassx.AutoTypePlatformWaylnd") + Q_INTERFACES(AutoTypePlatformInterface) + +public: + AutoTypePlatformWayland(); + bool isAvailable() override; + void unload() override; + QStringList windowTitles() override; + WId activeWindow() override; + QString activeWindowTitle() override; + bool raiseWindow(WId window) override; + AutoTypeExecutor* createExecutor() override; + + AutoTypeAction::Result sendKey(xkb_keysym_t keysym, QVector modifiers = {}); + void createSession(); + +private slots: + void portalResponse(uint response, QVariantMap results, QDBusMessage message); + +private: + bool m_loaded; + QDBusConnection m_bus; + QMap> m_handlers; + QDBusInterface m_remote_desktop; + QDBusObjectPath m_session_handle; + QString m_restore_token; + bool m_session_started = false; + + void handleCreateSession(uint response, QVariantMap results); + void handleSelectDevices(uint response, QVariantMap results); + void handleStart(uint response, QVariantMap results); +}; + +class AutoTypeExecutorWayland : public AutoTypeExecutor +{ +public: + explicit AutoTypeExecutorWayland(AutoTypePlatformWayland* platform); + + AutoTypeAction::Result execBegin(const AutoTypeBegin* action) override; + AutoTypeAction::Result execType(const AutoTypeKey* action) override; + AutoTypeAction::Result execClearField(const AutoTypeClearField* action) override; + +private: + AutoTypePlatformWayland* const m_platform; +}; diff --git a/src/autotype/wayland/CMakeLists.txt b/src/autotype/wayland/CMakeLists.txt new file mode 100644 index 000000000..9ed645e25 --- /dev/null +++ b/src/autotype/wayland/CMakeLists.txt @@ -0,0 +1,9 @@ +find_package(Xkbcommon REQUIRED) + +set(autotype_WAYLAND_SOURCES AutoTypeWayland.cpp) + +add_library(keepassxc-autotype-wayland MODULE ${autotype_WAYLAND_SOURCES}) +target_link_libraries(keepassxc-autotype-wayland keepassxc_gui Qt5::Core Qt5::Widgets Qt5::DBus ${Xkbcommon_LIBRARIES}) +install(TARGETS keepassxc-autotype-wayland + BUNDLE DESTINATION . COMPONENT Runtime + LIBRARY DESTINATION ${PLUGIN_INSTALL_DIR} COMPONENT Runtime) diff --git a/src/autotype/wayland/org.freedesktop.portal.RemoteDesktop.xml b/src/autotype/wayland/org.freedesktop.portal.RemoteDesktop.xml new file mode 100644 index 000000000..9ef6671a0 --- /dev/null +++ b/src/autotype/wayland/org.freedesktop.portal.RemoteDesktop.xml @@ -0,0 +1,493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/autotype/xcb/CMakeLists.txt b/src/autotype/xcb/CMakeLists.txt index f14017f63..5d2ab4590 100644 --- a/src/autotype/xcb/CMakeLists.txt +++ b/src/autotype/xcb/CMakeLists.txt @@ -1,9 +1,18 @@ -include_directories(SYSTEM ${X11_X11_INCLUDE_PATH}) +if(WITH_XC_X11) + find_package(X11 REQUIRED COMPONENTS Xi XTest) + + if(PRINT_SUMMARY) + add_feature_info(libXi X11_Xi_FOUND "The X11 Xi Protocol library is required for auto-type") + add_feature_info(libXtst X11_XTest_FOUND "The X11 XTEST Protocol library is required for auto-type") + endif() -set(autotype_XCB_SOURCES AutoTypeXCB.cpp) + include_directories(SYSTEM ${X11_X11_INCLUDE_PATH}) -add_library(keepassxc-autotype-xcb MODULE ${autotype_XCB_SOURCES}) -target_link_libraries(keepassxc-autotype-xcb keepassxc_gui 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) + set(autotype_XCB_SOURCES AutoTypeXCB.cpp) + + add_library(keepassxc-autotype-xcb MODULE ${autotype_XCB_SOURCES}) + target_link_libraries(keepassxc-autotype-xcb keepassxc_gui 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) +endif() diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index d51588ac5..189ea393d 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -1978,6 +1978,11 @@ void MainWindow::lockAllDatabases() m_ui->tabWidget->lockDatabases(); } +void MainWindow::requestGlobalAutoType(const QString& search) +{ + emit osUtils->globalShortcutTriggered("autotype", search); +} + void MainWindow::displayDesktopNotification(const QString& msg, QString title, int msTimeoutHint) { if (!m_trayIcon || !QSystemTrayIcon::supportsMessages()) { diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 8effd06f4..047829480 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -92,6 +92,7 @@ public slots: void toggleWindow(); void bringToFront(); void closeAllDatabases(); + void requestGlobalAutoType(const QString& search = ""); void lockAllDatabases(); void closeModalWindow(); void displayDesktopNotification(const QString& msg, QString title = "", int msTimeoutHint = 10000); diff --git a/src/gui/org.keepassxc.KeePassXC.MainWindow.xml b/src/gui/org.keepassxc.KeePassXC.MainWindow.xml index 651018149..1be9da42c 100644 --- a/src/gui/org.keepassxc.KeePassXC.MainWindow.xml +++ b/src/gui/org.keepassxc.KeePassXC.MainWindow.xml @@ -21,6 +21,11 @@ + + + + + diff --git a/vcpkg.json b/vcpkg.json index f98c1880e..5b9d2307c 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -54,6 +54,11 @@ ], "platform": "linux | freebsd" }, + { + "name": "libxkbcommon", + "version>=": "1.4.1", + "platform": "linux | freebsd" + }, { "name": "readline", "version>=": "0#5"