diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index 98ae83a6c..48cd0d8d7 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -22,13 +22,15 @@ #include "core/Config.h" #include +#include #include -#include #include +#include #include #include #include "autotype/AutoType.h" +#include "core/Global.h" #if defined(Q_OS_UNIX) #include @@ -36,6 +38,11 @@ #include #endif +namespace { +constexpr int WaitTimeoutMSec = 150; +const char BlockSizeProperty[] = "blockSize"; +} + #if defined(Q_OS_UNIX) && !defined(Q_OS_OSX) class XcbEventFilter : public QAbstractNativeEventFilter { @@ -105,8 +112,8 @@ Application::Application(int& argc, char** argv) // In DEBUG mode don't interfere with Release instances identifier += "-DEBUG"; #endif - QString socketName = identifier + ".socket"; QString lockName = identifier + ".lock"; + m_socketName = identifier + ".socket"; // According to documentation we should use RuntimeLocation on *nixes, but even Qt doesn't respect // this and creates sockets in TempLocation, so let's be consistent. @@ -114,20 +121,22 @@ Application::Application(int& argc, char** argv) m_lockFile->setStaleLockTime(0); m_lockFile->tryLock(); + m_lockServer.setSocketOptions(QLocalServer::UserAccessOption); + connect(&m_lockServer, SIGNAL(newConnection()), this, SIGNAL(anotherInstanceStarted())); + connect(&m_lockServer, SIGNAL(newConnection()), this, SLOT(processIncomingConnection())); + switch (m_lockFile->error()) { case QLockFile::NoError: // No existing lock was found, start listener - m_lockServer.setSocketOptions(QLocalServer::UserAccessOption); - m_lockServer.listen(socketName); - connect(&m_lockServer, SIGNAL(newConnection()), this, SIGNAL(anotherInstanceStarted())); + m_lockServer.listen(m_socketName); break; case QLockFile::LockFailedError: { if (config()->get("SingleInstance").toBool()) { // Attempt to connect to the existing instance QLocalSocket client; - for (int i = 0; i < 3; i++) { - client.connectToServer(socketName); - if (client.waitForConnected(150)) { + for (int i = 0; i < 3; ++i) { + client.connectToServer(m_socketName); + if (client.waitForConnected(WaitTimeoutMSec)) { // Connection succeeded, this will raise the existing window if minimized client.abort(); m_alreadyRunning = true; @@ -145,9 +154,7 @@ Application::Application(int& argc, char** argv) m_lockFile->removeStaleLockFile(); m_lockFile->tryLock(); // start the listen server - m_lockServer.setSocketOptions(QLocalServer::UserAccessOption); - m_lockServer.listen(socketName); - connect(&m_lockServer, SIGNAL(newConnection()), this, SIGNAL(anotherInstanceStarted())); + m_lockServer.listen(m_socketName); } } break; @@ -187,12 +194,8 @@ bool Application::event(QEvent* event) } #ifdef Q_OS_MAC // restore main window when clicking on the docker icon - else if ((event->type() == QEvent::ApplicationActivate) && m_mainWindow) { - m_mainWindow->ensurePolished(); - m_mainWindow->setWindowState(m_mainWindow->windowState() & ~Qt::WindowMinimized); - m_mainWindow->show(); - m_mainWindow->raise(); - m_mainWindow->activateWindow(); + else if (event->type() == QEvent::ApplicationActivate) { + emit applicationActivated(); } #endif @@ -247,12 +250,54 @@ void Application::quitBySignal() m_unixSignalNotifier->setEnabled(false); char buf; Q_UNUSED(::read(unixSignalSocket[1], &buf, sizeof(buf))); - - if (nullptr != m_mainWindow) - static_cast(m_mainWindow)->appExit(); + emit quitSignalReceived(); } #endif +void Application::processIncomingConnection() +{ + if (m_lockServer.hasPendingConnections()) { + QLocalSocket* socket = m_lockServer.nextPendingConnection(); + socket->setProperty(BlockSizeProperty, 0); + connect(socket, SIGNAL(readyRead()), this, SLOT(socketReadyRead())); + } +} + +void Application::socketReadyRead() +{ + QLocalSocket* socket = qobject_cast(sender()); + if (!socket) { + return; + } + + QDataStream in(socket); + in.setVersion(QDataStream::Qt_5_0); + + int blockSize = socket->property(BlockSizeProperty).toInt(); + if (blockSize == 0) { + // Relies on the fact that QDataStream format streams a quint32 into sizeof(quint32) bytes + if (socket->bytesAvailable() < qint64(sizeof(quint32))) { + return; + } + in >> blockSize; + } + + if (socket->bytesAvailable() < blockSize || in.atEnd()) { + socket->setProperty(BlockSizeProperty, blockSize); + return; + } + + QStringList fileNames; + in >> fileNames; + for (const QString& fileName: asConst(fileNames)) { + const QFileInfo fInfo(fileName); + if (fInfo.isFile() && fInfo.suffix().toLower() == "kdbx") { + emit openFile(fileName); + } + } + socket->deleteLater(); +} + bool Application::isAlreadyRunning() const { #ifdef QT_DEBUG @@ -262,3 +307,26 @@ bool Application::isAlreadyRunning() const return config()->get("SingleInstance").toBool() && m_alreadyRunning; } +bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames) +{ + QLocalSocket client; + client.connectToServer(m_socketName); + const bool connected = client.waitForConnected(WaitTimeoutMSec); + if (!connected) { + return false; + } + + QByteArray data; + QDataStream out(&data, QIODevice::WriteOnly); + out.setVersion(QDataStream::Qt_5_0); + out << quint32(0) + << fileNames; + out.device()->seek(0); + out << quint32(data.size() - sizeof(quint32)); + + const bool writeOk = client.write(data) != -1 && client.waitForBytesWritten(WaitTimeoutMSec); + client.disconnectFromServer(); + const bool disconnected = client.waitForDisconnected(WaitTimeoutMSec); + return writeOk && disconnected; +} + diff --git a/src/gui/Application.h b/src/gui/Application.h index 670342ca4..1f5759368 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -39,14 +39,20 @@ public: bool event(QEvent* event) override; bool isAlreadyRunning() const; + bool sendFileNamesToRunningInstance(const QStringList& fileNames); + signals: void openFile(const QString& filename); void anotherInstanceStarted(); + void applicationActivated(); + void quitSignalReceived(); private slots: #if defined(Q_OS_UNIX) void quitBySignal(); #endif + void processIncomingConnection(); + void socketReadyRead(); private: QWidget* m_mainWindow; @@ -63,6 +69,7 @@ private: bool m_alreadyRunning; QLockFile* m_lockFile; QLocalServer m_lockServer; + QString m_socketName; }; #endif // KEEPASSX_APPLICATION_H diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index bc7b3a89e..72be3a4aa 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -114,6 +114,8 @@ MainWindow::MainWindow() { m_ui->setupUi(this); + setAcceptDrops(true); + m_ui->toolBar->setContextMenuPolicy(Qt::PreventContextMenu); // Setup the search widget in the toolbar @@ -879,11 +881,7 @@ void MainWindow::toggleWindow() if ((QApplication::activeWindow() == this) && isVisible() && !isMinimized()) { hideWindow(); } else { - ensurePolished(); - setWindowState(windowState() & ~Qt::WindowMinimized); - show(); - raise(); - activateWindow(); + bringToFront(); #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) && !defined(QT_NO_DBUS) && (QT_VERSION < QT_VERSION_CHECK(5, 9, 0)) // re-register global D-Bus menu (needed on Ubuntu with Unity) @@ -991,9 +989,57 @@ void MainWindow::hideYubiKeyPopup() setEnabled(true); } +void MainWindow::bringToFront() +{ + ensurePolished(); + setWindowState(windowState() & ~Qt::WindowMinimized); + show(); + raise(); + activateWindow(); +} + void MainWindow::handleScreenLock() { if (config()->get("security/lockdatabasescreenlock").toBool()){ lockDatabasesAfterInactivity(); } } + +QStringList MainWindow::kdbxFilesFromUrls(const QList& urls) +{ + QStringList kdbxFiles; + for (const QUrl& url: urls) { + const QFileInfo fInfo(url.toLocalFile()); + const bool isKdbxFile = fInfo.isFile() && fInfo.suffix().toLower() == "kdbx"; + if (isKdbxFile) { + kdbxFiles.append(fInfo.absoluteFilePath()); + } + } + + return kdbxFiles; +} + +void MainWindow::dragEnterEvent(QDragEnterEvent* event) +{ + const QMimeData* mimeData = event->mimeData(); + if (mimeData->hasUrls()) { + const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls()); + if (!kdbxFiles.isEmpty()) { + event->acceptProposedAction(); + } + } +} + +void MainWindow::dropEvent(QDropEvent* event) +{ + const QMimeData* mimeData = event->mimeData(); + if (mimeData->hasUrls()) { + const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls()); + if (!kdbxFiles.isEmpty()) { + event->acceptProposedAction(); + } + for (const QString& kdbxFile: kdbxFiles) { + openDatabase(kdbxFile); + } + } +} diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index ade339c56..b70dd9072 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -61,6 +61,7 @@ public slots: void hideGlobalMessage(); void showYubiKeyPopup(); void hideYubiKeyPopup(); + void bringToFront(); protected: void closeEvent(QCloseEvent* event) override; @@ -107,6 +108,10 @@ private: void updateTrayIcon(); bool isTrayIconEnabled() const; + static QStringList kdbxFilesFromUrls(const QList& urls); + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; + const QScopedPointer m_ui; SignalMultiplexer m_actionMultiplexer; QAction* m_clearHistoryAction; diff --git a/src/main.cpp b/src/main.cpp index c3b1c6987..82331f072 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -57,22 +57,6 @@ int main(int argc, char** argv) // don't set organizationName as that changes the return value of // QStandardPaths::writableLocation(QDesktopServices::DataLocation) - if (app.isAlreadyRunning()) { - qWarning() << QCoreApplication::translate("Main", "Another instance of KeePassXC is already running.").toUtf8().constData(); - return 0; - } - - QApplication::setQuitOnLastWindowClosed(false); - - if (!Crypto::init()) { - QString error = QCoreApplication::translate("Main", - "Fatal error while testing the cryptographic functions."); - error.append("\n"); - error.append(Crypto::errorString()); - MessageBox::critical(nullptr, QCoreApplication::translate("Main", "KeePassXC - Error"), error); - return 1; - } - QCommandLineParser parser; parser.setApplicationDescription(QCoreApplication::translate("main", "KeePassXC - cross-platform password manager")); parser.addPositionalArgument("filename", QCoreApplication::translate("main", "filenames of the password databases to open (*.kdbx)"), "[filename(s)]"); @@ -93,7 +77,26 @@ int main(int argc, char** argv) parser.addOption(pwstdinOption); parser.process(app); - const QStringList args = parser.positionalArguments(); + const QStringList fileNames = parser.positionalArguments(); + + if (app.isAlreadyRunning()) { + if (!fileNames.isEmpty()) { + app.sendFileNamesToRunningInstance(fileNames); + } + qWarning() << QCoreApplication::translate("Main", "Another instance of KeePassXC is already running.").toUtf8().constData(); + return 0; + } + + QApplication::setQuitOnLastWindowClosed(false); + + if (!Crypto::init()) { + QString error = QCoreApplication::translate("Main", + "Fatal error while testing the cryptographic functions."); + error.append("\n"); + error.append(Crypto::errorString()); + MessageBox::critical(nullptr, QCoreApplication::translate("Main", "KeePassXC - Error"), error); + return 1; + } if (parser.isSet(configOption)) { Config::createConfigFromFile(parser.value(configOption)); @@ -109,15 +112,10 @@ int main(int argc, char** argv) MainWindow mainWindow; app.setMainWindow(&mainWindow); - QObject::connect(&app, &Application::anotherInstanceStarted, - [&]() { - mainWindow.ensurePolished(); - mainWindow.setWindowState(mainWindow.windowState() & ~Qt::WindowMinimized); - mainWindow.show(); - mainWindow.raise(); - mainWindow.activateWindow(); - }); + QObject::connect(&app, SIGNAL(anotherInstanceStarted()), &mainWindow, SLOT(bringToFront())); + QObject::connect(&app, SIGNAL(applicationActivated()), &mainWindow, SLOT(bringToFront())); QObject::connect(&app, SIGNAL(openFile(QString)), &mainWindow, SLOT(openDatabase(QString))); + QObject::connect(&app, SIGNAL(quitSignalReceived()), &mainWindow, SLOT(appExit()), Qt::DirectConnection); // start minimized if configured bool minimizeOnStartup = config()->get("GUI/MinimizeOnStartup").toBool(); @@ -130,20 +128,19 @@ int main(int argc, char** argv) } if (config()->get("OpenPreviousDatabasesOnStartup").toBool()) { - const QStringList filenames = config()->get("LastOpenedDatabases").toStringList(); - for (int ii = filenames.size()-1; ii >= 0; ii--) { - QString filename = filenames.at(ii); + const QStringList fileNames = config()->get("LastOpenedDatabases").toStringList(); + for (const QString& filename: fileNames) { if (!filename.isEmpty() && QFile::exists(filename)) { - mainWindow.openDatabase(filename, QString(), QString()); + mainWindow.openDatabase(filename); } } } - for (int ii=0; ii < args.length(); ii++) { - QString filename = args[ii]; + const bool pwstdin = parser.isSet(pwstdinOption); + for (const QString& filename: fileNames) { if (!filename.isEmpty() && QFile::exists(filename)) { QString password; - if (parser.isSet(pwstdinOption)) { + if (pwstdin) { static QTextStream in(stdin, QIODevice::ReadOnly); password = in.readLine(); } diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 664bfd654..17b2736cc 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -952,6 +952,43 @@ void TestGui::testDatabaseLocking() QCOMPARE(m_tabWidget->tabText(0).remove('&'), origDbName); } +void TestGui::testDragAndDropKdbxFiles() +{ + const int openedDatabasesCount = m_tabWidget->count(); + + const QString badDatabaseFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/NotDatabase.notkdbx")); + QMimeData badMimeData; + badMimeData.setUrls({QUrl::fromLocalFile(badDatabaseFilePath)}); + QDragEnterEvent badDragEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier); + qApp->notify(m_mainWindow, &badDragEvent); + QCOMPARE(badDragEvent.isAccepted(), false); + + QDropEvent badDropEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier); + qApp->notify(m_mainWindow, &badDropEvent); + QCOMPARE(badDropEvent.isAccepted(), false); + + QCOMPARE(m_tabWidget->count(), openedDatabasesCount); + + const QString goodDatabaseFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx")); + QMimeData goodMimeData; + goodMimeData.setUrls({QUrl::fromLocalFile(goodDatabaseFilePath)}); + QDragEnterEvent goodDragEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier); + qApp->notify(m_mainWindow, &goodDragEvent); + QCOMPARE(goodDragEvent.isAccepted(), true); + + QDropEvent goodDropEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier); + qApp->notify(m_mainWindow, &goodDropEvent); + QCOMPARE(goodDropEvent.isAccepted(), true); + + QCOMPARE(m_tabWidget->count(), openedDatabasesCount + 1); + + MessageBox::setNextAnswer(QMessageBox::No); + triggerAction("actionDatabaseClose"); + Tools::wait(100); + + QCOMPARE(m_tabWidget->count(), openedDatabasesCount); +} + void TestGui::cleanupTestCase() { delete m_mainWindow; diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 5ec8237b3..1a2b24e74 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -60,6 +60,7 @@ private slots: void testDatabaseSettings(); void testKeePass1Import(); void testDatabaseLocking(); + void testDragAndDropKdbxFiles(); private: int addCannedEntries();