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 }