From 18f2accc7f5ac0941c703a7cb4dd9cab2a2bca77 Mon Sep 17 00:00:00 2001 From: dsc Date: Fri, 22 Mar 2019 21:02:08 +0100 Subject: [PATCH] IPC and custom protocol handler for monero:// --- filter.cpp | 7 +++ filter.h | 1 + main.cpp | 41 ++++++++++++---- main.qml | 50 ++++++++++++++++++++ monero-wallet-gui.pro | 10 +++- pages/Transfer.qml | 18 +++++-- share/Info.plist | 20 ++++++++ src/qt/ipc.cpp | 106 ++++++++++++++++++++++++++++++++++++++++++ src/qt/ipc.h | 35 ++++++++++++++ src/qt/mime.cpp | 42 +++++++++++++++++ src/qt/mime.h | 8 ++++ src/qt/utils.cpp | 20 ++++++++ src/qt/utils.h | 11 +++++ 13 files changed, 354 insertions(+), 15 deletions(-) create mode 100644 src/qt/ipc.cpp create mode 100644 src/qt/ipc.h create mode 100644 src/qt/mime.cpp create mode 100644 src/qt/mime.h create mode 100644 src/qt/utils.cpp create mode 100644 src/qt/utils.h diff --git a/filter.cpp b/filter.cpp index e66e5ade..581d0750 100644 --- a/filter.cpp +++ b/filter.cpp @@ -43,6 +43,13 @@ filter::filter(QObject *parent) : } bool filter::eventFilter(QObject *obj, QEvent *ev) { + // macOS sends fileopen signal for incoming uri handlers + if (ev->type() == QEvent::FileOpen) { + QFileOpenEvent *openEvent = static_cast(ev); + QUrl scheme = openEvent->url(); + emit uriHandler(scheme); + } + if(ev->type() == QEvent::KeyPress || ev->type() == QEvent::MouseButtonRelease){ emit userActivity(); } diff --git a/filter.h b/filter.h index bb3a78b7..b83926a3 100644 --- a/filter.h +++ b/filter.h @@ -49,6 +49,7 @@ signals: void mousePressed(const QVariant &o, const QVariant &x, const QVariant &y); void mouseReleased(const QVariant &o, const QVariant &x, const QVariant &y); void userActivity(); + void uriHandler(const QUrl &url); }; #endif // FILTER_H diff --git a/main.cpp b/main.cpp index eca65333..8ec59e2b 100644 --- a/main.cpp +++ b/main.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +61,9 @@ #include "wallet/api/wallet2_api.h" #include "Logger.h" #include "MainApp.h" +#include "qt/ipc.h" +#include "qt/utils.h" +#include "qt/mime.h" // IOS exclusions #ifndef Q_OS_IOS @@ -82,6 +86,8 @@ int main(int argc, char *argv[]) // platform dependant settings #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) bool isDesktop = true; +#elif defined(Q_OS_LINUX) + bool isLinux = true; #elif defined(Q_OS_ANDROID) bool isAndroid = true; #elif defined(Q_OS_IOS) @@ -127,6 +133,7 @@ int main(int argc, char *argv[]) QCommandLineOption logPathOption(QStringList() << "l" << "log-file", QCoreApplication::translate("main", "Log to specified file"), QCoreApplication::translate("main", "file")); + parser.addOption(logPathOption); parser.addHelpOption(); parser.process(app); @@ -138,11 +145,33 @@ int main(int argc, char *argv[]) Monero::Wallet::init(argv[0], "monero-wallet-gui", logPath.toStdString().c_str(), true); qInstallMessageHandler(messageHandler); + // Get default account name + QString accountName = getAccountName(); // loglevel is configured in main.qml. Anything lower than // qWarning is not shown here. qWarning().noquote() << "app startd" << "(log: " + logPath + ")"; +#ifdef Q_OS_LINUX + registerXdgMime(app); +#endif + + IPC *ipc = new IPC(&app); + QStringList posArgs = parser.positionalArguments(); + + for(int i = 0; i != posArgs.count(); i++){ + QString arg = QString(posArgs.at(i)); + if(arg.isEmpty() || arg.length() >= 512) continue; + if(arg.contains(reURI)){ + if(!ipc->saveCommand(arg)){ + return 0; + } + } + } + + // start listening + QTimer::singleShot(0, ipc, SLOT(bind())); + // screen settings // Mobile is designed on 128dpi qreal ref_dpi = 128; @@ -252,6 +281,8 @@ int main(int argc, char *argv[]) engine.rootContext()->setContextProperty("mainApp", &app); + engine.rootContext()->setContextProperty("IPC", ipc); + engine.rootContext()->setContextProperty("qtRuntimeVersion", qVersion()); engine.rootContext()->setContextProperty("walletLogPath", logPath); @@ -295,14 +326,6 @@ int main(int argc, char *argv[]) engine.rootContext()->setContextProperty("moneroAccountsDir", moneroAccountsDir); } - - // Get default account name - QString accountName = qgetenv("USER"); // mac/linux - if (accountName.isEmpty()) - accountName = qgetenv("USERNAME"); // Windows - if (accountName.isEmpty()) - accountName = "My monero Account"; - engine.rootContext()->setContextProperty("defaultAccountName", accountName); engine.rootContext()->setContextProperty("applicationDirectory", QApplication::applicationDirPath()); engine.rootContext()->setContextProperty("idealThreadCount", QThread::idealThreadCount()); @@ -312,7 +335,6 @@ int main(int argc, char *argv[]) builtWithScanner = true; #endif engine.rootContext()->setContextProperty("builtWithScanner", builtWithScanner); - // Load main window (context properties needs to be defined obove this line) engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); if (engine.rootObjects().isEmpty()) @@ -345,5 +367,6 @@ int main(int argc, char *argv[]) QObject::connect(eventFilter, SIGNAL(mousePressed(QVariant,QVariant,QVariant)), rootObject, SLOT(mousePressed(QVariant,QVariant,QVariant))); QObject::connect(eventFilter, SIGNAL(mouseReleased(QVariant,QVariant,QVariant)), rootObject, SLOT(mouseReleased(QVariant,QVariant,QVariant))); QObject::connect(eventFilter, SIGNAL(userActivity()), rootObject, SLOT(userActivity())); + QObject::connect(eventFilter, SIGNAL(uriHandler(QUrl)), ipc, SLOT(parseCommand(QUrl))); return app.exec(); } diff --git a/main.qml b/main.qml index 94c4a0d1..9776269d 100644 --- a/main.qml +++ b/main.qml @@ -399,6 +399,49 @@ ApplicationWindow { leftPanel.balanceLabelText = qsTr("Balance (#%1%2)").arg(currentWallet.currentSubaddressAccount).arg(accountLabel === "" ? "" : (" – " + accountLabel)); } + function onUriHandler(uri){ + if(uri.startsWith("monero://")){ + var address = uri.substring("monero://".length); + + var params = {} + if(address.length === 0) return; + var spl = address.split("?"); + + if(spl.length > 2) return; + if(spl.length >= 1) { + // parse additional params + address = spl[0]; + + if(spl.length === 2){ + spl.shift(); + var item = spl[0]; + + var _spl = item.split("&"); + for (var param in _spl){ + var _item = _spl[param]; + if(!_item.indexOf("=") > 0) continue; + + var __spl = _item.split("="); + if(__spl.length !== 2) continue; + + params[__spl[0]] = __spl[1]; + } + } + } + + // Fill fields + middlePanel.transferView.sendTo(address, params["tx_payment_id"], params["tx_description"], params["tx_amount"]); + + // Raise window + appWindow.raise(); + appWindow.show(); + + // @TODO: remove after paymentID deprecation + if(params.hasOwnProperty("tx_payment_id")) + persistentSettings.showPid = true; + } + } + function onWalletConnectionStatusChanged(status){ console.log("Wallet connection status changed " + status) middlePanel.updateStatus(); @@ -489,6 +532,12 @@ ApplicationWindow { // Force switch normal view rootItem.state = "normal"; + + // Process queued IPC command + if(typeof IPC !== "undefined" && IPC.queuedCmd().length > 0){ + var queuedCmd = IPC.queuedCmd(); + if(/^\w+:\/\/(.*)$/.test(queuedCmd)) appWindow.onUriHandler(queuedCmd); // uri + } } function onWalletClosed(walletAddress) { @@ -1086,6 +1135,7 @@ ApplicationWindow { walletManager.deviceButtonPressed.connect(onDeviceButtonPressed); walletManager.checkUpdatesComplete.connect(onWalletCheckUpdatesComplete); walletManager.walletPassphraseNeeded.connect(onWalletPassphraseNeeded); + IPC.uriHandler.connect(onUriHandler); if(typeof daemonManager != "undefined") { daemonManager.daemonStarted.connect(onDaemonStarted); diff --git a/monero-wallet-gui.pro b/monero-wallet-gui.pro index 06049f08..5deb5964 100644 --- a/monero-wallet-gui.pro +++ b/monero-wallet-gui.pro @@ -61,7 +61,10 @@ HEADERS += \ src/zxcvbn-c/zxcvbn.h \ src/libwalletqt/UnsignedTransaction.h \ Logger.h \ - MainApp.h + MainApp.h \ + src/qt/ipc.h \ + src/qt/mime.h \ + src/qt/utils.h SOURCES += main.cpp \ filter.cpp \ @@ -89,7 +92,10 @@ SOURCES += main.cpp \ src/zxcvbn-c/zxcvbn.c \ src/libwalletqt/UnsignedTransaction.cpp \ Logger.cpp \ - MainApp.cpp + MainApp.cpp \ + src/qt/ipc.cpp \ + src/qt/mime.cpp \ + src/qt/utils.cpp CONFIG(DISABLE_PASS_STRENGTH_METER) { HEADERS -= src/zxcvbn-c/zxcvbn.h diff --git a/pages/Transfer.qml b/pages/Transfer.qml index 6a8ea1be..09b3df58 100644 --- a/pages/Transfer.qml +++ b/pages/Transfer.qml @@ -708,10 +708,20 @@ Rectangle { } // Popuplate fields from addressbook. - function sendTo(address, paymentId, description){ - addressLine.text = address - setPaymentId(paymentId); - setDescription(description); + function sendTo(address, paymentId, description, amount){ + middlePanel.state = 'Transfer'; + + if(typeof address !== 'undefined') + addressLine.text = address + + if(typeof paymentId !== 'undefined') + setPaymentId(paymentId); + + if(typeof description !== 'undefined') + setDescription(description); + + if(typeof amount !== 'undefined') + amountLine.text = amount; } function updateSendButton(){ diff --git a/share/Info.plist b/share/Info.plist index 5b7a0b3f..438443d2 100644 --- a/share/Info.plist +++ b/share/Info.plist @@ -31,5 +31,25 @@ NSRequiresAquaSystemAppearance True + + CFBundleURLTypes + + + CFBundleURLName + monero Handler + CFBundleURLSchemes + + monero + + + + CFBundleURLName + moneroseed Handler + CFBundleURLSchemes + + moneroseed + + + diff --git a/src/qt/ipc.cpp b/src/qt/ipc.cpp new file mode 100644 index 00000000..c7d1aa86 --- /dev/null +++ b/src/qt/ipc.cpp @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include + +#include "ipc.h" +#include "utils.h" + +// Start listening for incoming IPC commands on UDS (Unix) or named pipe (Windows) +void IPC::bind(){ + QString path = QString(this->m_socketFile.absoluteFilePath()); + qDebug() << path; + + this->m_server = new QLocalServer(this); + this->m_server->setSocketOptions(QLocalServer::UserAccessOption); + + bool restarted = false; + if(!this->m_server->listen(path)){ + // On Unix if the server crashes without closing listen will fail with AddressInUseError. + // To create a new server the file should be removed. On Windows two local servers can listen + // to the same pipe at the same time, but any connections will go to one of the server. +#ifdef Q_OS_UNIX + qDebug() << QString("Unable to start IPC server in \"%1\": \"%2\". Retrying.").arg(path).arg(this->m_server->errorString()); + if(this->m_socketFile.exists()){ + QFile file(path); + file.remove(); + + if(this->m_server->listen(path)){ + restarted = true; + } + } +#endif + if(!restarted) + qDebug() << QString("Unable to start IPC server in \"%1\": \"%2\".").arg(path).arg(this->m_server->errorString()); + } + + connect(this->m_server, &QLocalServer::newConnection, this, &IPC::handleConnection); +} + +// Process incoming IPC command. First check if monero-wallet-gui is +// already running. If it is, send it to that instance instead, if not, +// queue the command for later use inside our QML engine. Returns true +// when queued, false if sent to another instance, at which point we can +// kill the current process. +bool IPC::saveCommand(QString cmdString){ + qDebug() << QString("saveCommand called: %1").arg(cmdString); + + QLocalSocket ls; + QByteArray buffer; + buffer = buffer.append(cmdString); + QString socketFilePath = this->socketFile().filePath(); + + ls.connectToServer(socketFilePath, QIODevice::WriteOnly); + if(ls.waitForConnected(1000)){ + ls.write(buffer); + if (!ls.waitForBytesWritten(1000)){ + qDebug() << QString("Could not send command \"%1\" over IPC %2: \"%3\"").arg(cmdString, socketFilePath, ls.errorString()); + return false; + } + + qDebug() << QString("Sent command \"%1\" over IPC \"%2\"").arg(cmdString, socketFilePath); + return false; + } + + if(ls.isOpen()) + ls.disconnectFromServer(); + + // Queue for later + this->SetQueuedCmd(cmdString); + return true; +} + +bool IPC::saveCommand(const QUrl &url){; + this->saveCommand(url.toString()); +} + +void IPC::handleConnection(){ + QLocalSocket *clientConnection = this->m_server->nextPendingConnection(); + connect(clientConnection, &QLocalSocket::disconnected, + clientConnection, &QLocalSocket::deleteLater); + + clientConnection->waitForReadyRead(2); + QByteArray cmdArray = clientConnection->readAll(); + QString cmdString = QTextCodec::codecForMib(106)->toUnicode(cmdArray); // UTF-8 + qDebug() << cmdString; + + this->parseCommand(cmdString); + + clientConnection->close(); + delete clientConnection; +} + +void IPC::parseCommand(const QUrl &url){ + this->parseCommand(url.toString()); +} + +void IPC::parseCommand(QString cmdString){ + if(cmdString.contains(reURI)){ + this->emitUriHandler(cmdString); + } +} + +void IPC::emitUriHandler(QString uriString){ + emit uriHandler(uriString); +} diff --git a/src/qt/ipc.h b/src/qt/ipc.h new file mode 100644 index 00000000..5c3cc73a --- /dev/null +++ b/src/qt/ipc.h @@ -0,0 +1,35 @@ +#ifndef IPC_H +#define IPC_H + +#include +#include +#include + +class IPC : public QObject +{ +Q_OBJECT +public: + IPC(QObject *parent = 0) : QObject(parent) {} + QFileInfo socketFile() const { return m_socketFile; } + Q_INVOKABLE QString queuedCmd() { return m_queuedCmd; } + void SetQueuedCmd(const QString cmdString) { m_queuedCmd = cmdString; } + +public slots: + void bind(); + void handleConnection(); + bool saveCommand(QString cmdString); + bool saveCommand(const QUrl &url); + void parseCommand(QString cmdString); + void parseCommand(const QUrl &url); + void emitUriHandler(QString uriString); + +signals: + void uriHandler(QString uriString); + +private: + QLocalServer *m_server; + QString m_queuedCmd; + QFileInfo m_socketFile = QFileInfo(QString(QDir::tempPath() + "/xmr-gui_%2.sock").arg(getAccountName())); +}; + +#endif // IPC_H diff --git a/src/qt/mime.cpp b/src/qt/mime.cpp new file mode 100644 index 00000000..76fbb19c --- /dev/null +++ b/src/qt/mime.cpp @@ -0,0 +1,42 @@ +#include +#include +#include +#include + +#include "mime.h" +#include "utils.h" + +void registerXdgMime(QApplication &app){ + // MacOS handled via Info.plist + // Windows handled in the installer by rbrunner7 + + QString xdg = QString( + "[Desktop Entry]\n" + "Name=Monero GUI\n" + "GenericName=Monero-GUI\n" + "X-GNOME-FullName=Monero-GUI\n" + "Comment=Monero GUI\n" + "Keywords=Monero;\n" + "Exec=%1 %u\n" + "Terminal=false\n" + "Type=Application\n" + "Icon=monero\n" + "Categories=Network;GNOME;Qt;\n" + "MimeType=x-scheme-handler/monero;x-scheme-handler/moneroseed\n" + "StartupNotify=true\n" + "X-GNOME-Bugzilla-Bugzilla=GNOME\n" + "X-GNOME-UsesNotifications=true\n" + ).arg(app.applicationFilePath()); + + QString appPath = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); + QString filePath = QString("%1/monero-gui.desktop").arg(appPath); + + qDebug() << QString("Writing %1").arg(filePath); + QFile file(filePath); + if(file.open(QIODevice::WriteOnly)){ + QTextStream out(&file); out << xdg << endl; + file.close(); + } + else + file.close(); +} diff --git a/src/qt/mime.h b/src/qt/mime.h new file mode 100644 index 00000000..a6c79587 --- /dev/null +++ b/src/qt/mime.h @@ -0,0 +1,8 @@ +#ifndef MIME_H +#define MIME_H + +#include + +void registerXdgMime(QApplication &app); + +#endif // MIME_H diff --git a/src/qt/utils.cpp b/src/qt/utils.cpp new file mode 100644 index 00000000..cb0ad4f9 --- /dev/null +++ b/src/qt/utils.cpp @@ -0,0 +1,20 @@ +#include + +#include "utils.h" + +bool fileExists(QString path) { + QFileInfo check_file(path); + if (check_file.exists() && check_file.isFile()) + return true; + else + return false; +} + +QString getAccountName(){ + QString accountName = qgetenv("USER"); // mac/linux + if (accountName.isEmpty()) + accountName = qgetenv("USERNAME"); // Windows + if (accountName.isEmpty()) + accountName = "My monero Account"; + return accountName; +} diff --git a/src/qt/utils.h b/src/qt/utils.h new file mode 100644 index 00000000..6a014ffc --- /dev/null +++ b/src/qt/utils.h @@ -0,0 +1,11 @@ +#ifndef UTILS_H +#define UTILS_H + +#include +#include + +bool fileExists(QString path); +QString getAccountName(); +const static QRegExp reURI = QRegExp("^\\w+:\\/\\/([\\w+\\-?\\-_\\-=\\-&]+)"); + +#endif // UTILS_H