File indexing completed on 2024-04-28 05:50:07
0001 /* 0002 * SPDX-License-Identifier: GPL-3.0-or-later 0003 * SPDX-FileCopyrightText: 2020-2021 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> 0004 */ 0005 #include "cli.h" 0006 #include "../logging_p.h" 0007 #include "../model/qr.h" 0008 #include "flows_p.h" 0009 #include "state_p.h" 0010 0011 #include <KLocalizedString> 0012 #include <QCommandLineOption> 0013 #include <QtConcurrent> 0014 0015 #ifdef ENABLE_DBUS_INTERFACE 0016 #include <QWindow> 0017 #include <KDBusService> 0018 #include <KStartupInfo> 0019 #include <KWindowSystem> 0020 #endif 0021 0022 KEYSMITH_LOGGER(logger, ".app.cli") 0023 0024 namespace app 0025 { 0026 void CommandLineOptions::addOptions(QCommandLineParser &parser) 0027 { 0028 Proxy::addOptions(parser); 0029 } 0030 0031 CommandLineOptions::CommandLineOptions(QCommandLineParser &parser, bool parseOk, QObject *parent) : 0032 QObject(parent), m_parseOk(parseOk), m_errorText(parseOk ? QString() : parser.errorText()), m_parser(parser) 0033 { 0034 } 0035 0036 QString CommandLineOptions::errorText(void) const 0037 { 0038 return m_errorText; 0039 } 0040 0041 bool CommandLineOptions::optionsOk(void) const 0042 { 0043 return m_parseOk; 0044 } 0045 0046 bool CommandLineOptions::newAccountRequested(void) const 0047 { 0048 return optionsOk() && !m_parser.positionalArguments().isEmpty(); 0049 } 0050 0051 void CommandLineOptions::handleNewAccount(model::AccountInput *recipient) 0052 { 0053 if (!newAccountRequested()) { 0054 qCDebug(logger) << "Ignoring request to handle new account:" 0055 << "Invalid commandline options or no URI was received on the commandline"; 0056 return; 0057 } 0058 0059 auto job = new CommandLineAccountJob(recipient); 0060 const auto argv = m_parser.positionalArguments(); 0061 QObject::connect(job, &CommandLineAccountJob::newAccountProcessed, this, &CommandLineOptions::newAccountProcessed); 0062 QObject::connect(job, &CommandLineAccountJob::newAccountInvalid, this, &CommandLineOptions::newAccountInvalid); 0063 job->run(argv[0]); 0064 } 0065 0066 CommandLineAccountJob::CommandLineAccountJob(model::AccountInput *recipient) : 0067 QObject(), m_alive(true), m_recipient(recipient) 0068 { 0069 QObject::connect(recipient, &QObject::destroyed, this, &CommandLineAccountJob::expired); 0070 } 0071 0072 void CommandLineAccountJob::expired(void) 0073 { 0074 m_alive = false; 0075 } 0076 0077 void CommandLineAccountJob::run(const QString &uri) 0078 { 0079 QtConcurrent::run(&CommandLineAccountJob::processNewAccount, this, uri); 0080 } 0081 0082 void CommandLineAccountJob::processNewAccount(CommandLineAccountJob *target, const QString &uri) 0083 { 0084 const auto result = model::QrParameters::parse(uri); 0085 bool invoked = false; 0086 if (result) { 0087 qCInfo(logger) << "Successfully parsed the URI passed on the commandline"; 0088 invoked = QMetaObject::invokeMethod(target, [target, result](void) -> void { 0089 if (target->m_alive) { 0090 result->populate(target->m_recipient); 0091 qCDebug(logger) << "Reporting success parsing URI from commandline"; 0092 Q_EMIT target->newAccountProcessed(); 0093 } else { 0094 qCDebug(logger) << "Not reporting success parsing URI from commandline: recipient has expired"; 0095 } 0096 QTimer::singleShot(0, target, &QObject::deleteLater); 0097 }); 0098 } else { 0099 qCInfo(logger) << "Failed to parse the URI passed on the commandline"; 0100 invoked = QMetaObject::invokeMethod(target, [target](void) -> void { 0101 if (target->m_alive) { 0102 qCDebug(logger) << "Reporting failure to parse URI from commandline"; 0103 Q_EMIT target->newAccountInvalid(); 0104 } else { 0105 qCDebug(logger) << "Not reporting failure to parse URI from commandline: recipient has expired"; 0106 } 0107 QTimer::singleShot(0, target, &QObject::deleteLater); 0108 }); 0109 } 0110 0111 if (!invoked) { 0112 Q_ASSERT_X(false, Q_FUNC_INFO, "should be able to invoke meta method"); 0113 qCDebug(logger) << "Failed to signal result of processing the URI passed on the commandline"; 0114 } 0115 } 0116 0117 Proxy::Proxy(QGuiApplication *app, QObject *parent) : 0118 QObject(parent), m_keysmith(nullptr), m_app(app) 0119 { 0120 Q_ASSERT_X(m_app, Q_FUNC_INFO, "Should have a valid QGuiApplication instance"); 0121 } 0122 0123 void Proxy::addOptions(QCommandLineParser &parser) 0124 { 0125 parser.addPositionalArgument( 0126 QStringLiteral("<uri>"), 0127 i18nc("@info (<uri> placeholder)", "Optional account to add, formatted as otpauth:// URI (e.g. from a QR code)") 0128 ); 0129 } 0130 0131 bool Proxy::parseCommandLine(QCommandLineParser &parser, const QStringList &argv) 0132 { 0133 // options that will be handled via UI interaction 0134 app::Proxy::addOptions(parser); 0135 return parser.parse(argv); 0136 } 0137 0138 void Proxy::disable(void) 0139 { 0140 m_keysmith = nullptr; 0141 } 0142 0143 bool Proxy::enable(Keysmith *keysmith) 0144 { 0145 Q_ASSERT_X(keysmith, Q_FUNC_INFO, "should be given a valid Keysmith instance"); 0146 if (m_keysmith) { 0147 qCDebug(logger) 0148 << "Not (re)initialising proxy with new Keysmith instance:" << keysmith 0149 << "Already initialised with:" << m_keysmith; 0150 return false; 0151 } 0152 0153 m_keysmith = keysmith; 0154 QObject::connect(m_keysmith, &QObject::destroyed, this, &Proxy::disable); 0155 return true; 0156 } 0157 0158 bool Proxy::proxy(const QCommandLineParser &parser, bool parsedOk) 0159 { 0160 if (!parsedOk) { 0161 qCDebug(logger) << "Not proxying to Keysmith instance: invalid command line"; 0162 return false; 0163 } 0164 if (!m_keysmith) { 0165 qCDebug(logger) << "Not proxying command line arguments: not initialised with a Keysmith instance"; 0166 return false; 0167 } 0168 0169 auto state = flowStateOf(m_keysmith); 0170 if (state->flowRunning()) { 0171 qCDebug(logger) << "Not proxying command line arguments: a 'competing' flow is already running"; 0172 return false; 0173 } 0174 0175 if (state->initialFlowDone()) { 0176 (new ExternalCommandLineFlow(m_keysmith))->run(parser); 0177 } else { 0178 (new InitialFlow(m_keysmith))->run(parser); 0179 } 0180 return true; 0181 } 0182 0183 0184 #ifdef ENABLE_DBUS_INTERFACE 0185 0186 static QWindow * getMainWindow(QGuiApplication *app) 0187 { 0188 if (!app) { 0189 qCDebug(logger) << "Cannot find a valid main window without a QGuiApplication"; 0190 return nullptr; 0191 } 0192 0193 const auto windows = app->topLevelWindows(); 0194 for (auto *window: windows) { 0195 if (window && window->type() == Qt::Window) { 0196 return window; 0197 } 0198 } 0199 qCDebug(logger) << "Unable to find main window for QGuiApplication:" << app; 0200 return nullptr; 0201 } 0202 0203 void Proxy::handleDBusActivation(const QStringList &arguments, const QString &workingDirectory) 0204 { 0205 Q_UNUSED(workingDirectory); 0206 qCInfo(logger) << "Handling Keysmith activation request"; 0207 0208 auto *s = sender(); 0209 Q_ASSERT_X(s, Q_FUNC_INFO, "should be triggered with a valid sender()"); 0210 0211 auto *svc = qobject_cast<KDBusService*>(s); 0212 Q_ASSERT_X(svc, Q_FUNC_INFO, "should be triggered by a KDBusService instance"); 0213 0214 QCommandLineParser cliParser; 0215 bool parseOk = parseCommandLine(cliParser, arguments); 0216 if (!proxy(cliParser, parseOk)) { 0217 qCDebug(logger) << "Rejected command line arguments"; 0218 svc->setExitValue(1); 0219 } 0220 0221 const auto mainWindow = getMainWindow(m_app); 0222 if (!mainWindow) { 0223 qCWarning(logger) << "Unable to activate Keysmith main window: unable to find it"; 0224 svc->setExitValue(1); 0225 return; 0226 } 0227 0228 qCDebug(logger) << "Activating Keysmith main window"; 0229 KWindowSystem::updateStartupId(mainWindow); 0230 KWindowSystem::activateWindow(mainWindow); 0231 } 0232 #endif 0233 }