diff --git a/src/conf/kleopageconfigdialog.cpp b/src/conf/kleopageconfigdialog.cpp index e2b03913c..5e95d8c22 100644 --- a/src/conf/kleopageconfigdialog.cpp +++ b/src/conf/kleopageconfigdialog.cpp @@ -1,250 +1,244 @@ /* kleopageconfigdialog.cpp This file is part of Kleopatra SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-only It is derived from KCMultidialog which is: SPDX-FileCopyrightText: 2000 Matthias Elter SPDX-FileCopyrightText: 2003 Daniel Molkentin SPDX-FileCopyrightText: 2003, 2006 Matthias Kretz SPDX-FileCopyrightText: 2004 Frans Englich SPDX-FileCopyrightText: 2006 Tobias Koenig SPDX-License-Identifier: LGPL-2.0-or-later */ #include #include "kleopageconfigdialog.h" #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" KleoPageConfigDialog::KleoPageConfigDialog(QWidget *parent) : KPageDialog(parent) { setModal(false); QDialogButtonBox *buttonBox = new QDialogButtonBox(this); buttonBox->setStandardButtons(QDialogButtonBox::Help | QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Cancel | QDialogButtonBox::Apply | QDialogButtonBox::Ok | QDialogButtonBox::Reset); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::ok()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::RestoreDefaults), KStandardGuiItem::defaults()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Apply), KStandardGuiItem::apply()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Reset), KStandardGuiItem::reset()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Help), KStandardGuiItem::help()); buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &KleoPageConfigDialog::slotApplyClicked); connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this, &KleoPageConfigDialog::slotOkClicked); connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &KleoPageConfigDialog::slotDefaultClicked); connect(buttonBox->button(QDialogButtonBox::Help), &QAbstractButton::clicked, this, &KleoPageConfigDialog::slotHelpClicked); connect(buttonBox->button(QDialogButtonBox::Reset), &QAbstractButton::clicked, this, &KleoPageConfigDialog::slotUser1Clicked); setButtonBox(buttonBox); connect(this, &KPageDialog::currentPageChanged, this, &KleoPageConfigDialog::slotCurrentPageChanged); } void KleoPageConfigDialog::slotCurrentPageChanged(KPageWidgetItem *current, KPageWidgetItem *previous) { if (!previous) { return; } blockSignals(true); setCurrentPage(previous); KCModule *previousModule = qobject_cast(previous->widget()); bool canceled = false; if (previousModule && mChangedModules.contains(previousModule)) { const int queryUser = KMessageBox::warningTwoActionsCancel( this, i18n("The settings of the current module have changed.\n" "Do you want to apply the changes or discard them?"), i18n("Apply Settings"), KStandardGuiItem::apply(), KStandardGuiItem::discard(), KStandardGuiItem::cancel()); if (queryUser == KMessageBox::ButtonCode::PrimaryAction) { previousModule->save(); } else if (queryUser == KMessageBox::ButtonCode::SecondaryAction) { previousModule->load(); } canceled = queryUser == KMessageBox::Cancel; } if (!canceled) { mChangedModules.removeAll(previousModule); setCurrentPage(current); } blockSignals(false); clientChanged(); } void KleoPageConfigDialog::apply() { QPushButton *applyButton = buttonBox()->button(QDialogButtonBox::Apply); applyButton->setFocus(); for (KCModule *module : mChangedModules) { module->save(); } mChangedModules.clear(); Q_EMIT configCommitted(); clientChanged(); } void KleoPageConfigDialog::slotDefaultClicked() { const KPageWidgetItem *item = currentPage(); if (!item) { return; } KCModule *module = qobject_cast(item->widget()); if (!module) { return; } module->defaults(); clientChanged(); } void KleoPageConfigDialog::slotUser1Clicked() { const KPageWidgetItem *item = currentPage(); if (!item) { return; } KCModule *module = qobject_cast(item->widget()); if (!module) { return; } module->load(); mChangedModules.removeAll(module); clientChanged(); } void KleoPageConfigDialog::slotApplyClicked() { apply(); } void KleoPageConfigDialog::slotOkClicked() { apply(); accept(); } void KleoPageConfigDialog::slotHelpClicked() { const KPageWidgetItem *item = currentPage(); if (!item) { return; } const QString docPath = mHelpUrls.value(item->name()); QUrl docUrl; #ifdef Q_OS_WIN docUrl = QUrl(QLatin1String("https://docs.kde.org/index.php?branch=stable5&language=") + QLocale().name() + QLatin1String("&application=kleopatra")); #else docUrl = QUrl(QStringLiteral("help:/")).resolved(QUrl(docPath)); // same code as in KHelpClient::invokeHelp #endif if (docUrl.scheme() == QLatin1String("help") || docUrl.scheme() == QLatin1String("man") || docUrl.scheme() == QLatin1String("info")) { - const QString exec = - QStandardPaths::findExecutable(QStringLiteral("khelpcenter")); - if (exec.isEmpty()) { - qCWarning(KLEOPATRA_LOG) << "Could not find khelpcenter in PATH."; - } else { - QProcess::startDetached(QStringLiteral("khelpcenter"), - QStringList() << docUrl.toString()); - } + // Warning: Don't assume that the program needs to be in PATH. On Windows, it will also be found next to the calling process. + QProcess::startDetached(QStringLiteral("khelpcenter"), QStringList() << docUrl.toString()); } else { QDesktopServices::openUrl(docUrl); } } void KleoPageConfigDialog::addModule(const QString &name, const QString &docPath, const QString &icon, KCModule *module) { mModules << module; KPageWidgetItem *item = addPage(module, name); item->setIcon(QIcon::fromTheme(icon)); connect(module, SIGNAL(changed(bool)), this, SLOT(moduleChanged(bool))); mHelpUrls.insert(name, docPath); } void KleoPageConfigDialog::moduleChanged(bool state) { KCModule *module = qobject_cast(sender()); qCDebug(KLEOPATRA_LOG) << "Module changed: " << state << " mod " << module; if (mChangedModules.contains(module)) { if (!state) { mChangedModules.removeAll(module); } else { return; } } if (state) { mChangedModules << module; } clientChanged(); } void KleoPageConfigDialog::clientChanged() { const KPageWidgetItem *item = currentPage(); if (!item) { return; } KCModule *module = qobject_cast(item->widget()); if (!module) { return; } qCDebug(KLEOPATRA_LOG) << "Client changed: " << " mod " << module; bool change = mChangedModules.contains(module); QPushButton *resetButton = buttonBox()->button(QDialogButtonBox::Reset); if (resetButton) { resetButton->setEnabled(change); } QPushButton *applyButton = buttonBox()->button(QDialogButtonBox::Apply); if (applyButton) { applyButton->setEnabled(change); } } diff --git a/src/libkleopatraclient/core/command.cpp b/src/libkleopatraclient/core/command.cpp index cc8193a56..0510d0e35 100644 --- a/src/libkleopatraclient/core/command.cpp +++ b/src/libkleopatraclient/core/command.cpp @@ -1,710 +1,704 @@ /* -*- mode: c++; c-basic-offset:4 -*- command.cpp This file is part of KleopatraClient, the Kleopatra interface library SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: LGPL-2.0-or-later */ #include #include "command.h" #include "command_p.h" #include // Q_OS_WIN #ifdef Q_OS_WIN // HACK: AllowSetForegroundWindow needs _WIN32_WINDOWS >= 0x0490 set # ifndef _WIN32_WINDOWS # define _WIN32_WINDOWS 0x0500 # define _WIN32_WINNT 0x0500 // good enough for Vista too # endif # include # include #endif #include #include #include "libkleopatraclientcore_debug.h" #include #include #include #include #include #include -#include #include -#include -#include #include +#include +#include #include using namespace KleopatraClientCopy; // copied from kleopatra/utils/hex.cpp static std::string hexencode(const std::string &in) { std::string result; result.reserve(3 * in.size()); static const char hex[] = "0123456789ABCDEF"; for (std::string::const_iterator it = in.begin(), end = in.end(); it != end; ++it) switch (const unsigned char ch = *it) { default: if ((ch >= '!' && ch <= '~') || ch > 0xA0) { result += ch; break; } Q_FALLTHROUGH(); // else fall through case ' ': result += '+'; break; case '"': case '#': case '$': case '%': case '\'': case '+': case '=': result += '%'; result += hex[(ch & 0xF0) >> 4 ]; result += hex[(ch & 0x0F) ]; break; } return result; } #ifdef UNUSED static std::string hexencode(const char *in) { if (!in) { return std::string(); } return hexencode(std::string(in)); } #endif // changed from returning QByteArray to returning std::string static std::string hexencode(const QByteArray &in) { if (in.isNull()) { return std::string(); } return hexencode(std::string(in.data(), in.size())); } // end copied from kleopatra/utils/hex.cpp Command::Command(QObject *p) : QObject(p), d(new Private(this)) { d->init(); } Command::Command(Private *pp, QObject *p) : QObject(p), d(pp) { d->init(); } Command::~Command() { delete d; d = nullptr; } void Command::Private::init() { connect(this, &QThread::started, q, &Command::started); connect(this, &QThread::finished, q, &Command::finished); } void Command::setParentWId(WId wid) { const QMutexLocker locker(&d->mutex); d->inputs.parentWId = wid; } WId Command::parentWId() const { const QMutexLocker locker(&d->mutex); return d->inputs.parentWId; } void Command::setServerLocation(const QString &location) { const QMutexLocker locker(&d->mutex); d->outputs.serverLocation = location; } QString Command::serverLocation() const { const QMutexLocker locker(&d->mutex); return d->outputs.serverLocation; } bool Command::waitForFinished() { return d->wait(); } bool Command::waitForFinished(unsigned long ms) { return d->wait(ms); } bool Command::error() const { const QMutexLocker locker(&d->mutex); return !d->outputs.errorString.isEmpty(); } bool Command::wasCanceled() const { const QMutexLocker locker(&d->mutex); return d->outputs.canceled; } QString Command::errorString() const { const QMutexLocker locker(&d->mutex); return d->outputs.errorString; } qint64 Command::serverPid() const { const QMutexLocker locker(&d->mutex); return d->outputs.serverPid; } void Command::start() { d->start(); } void Command::cancel() { qCDebug(LIBKLEOPATRACLIENTCORE_LOG) << "Sorry, not implemented: KleopatraClient::Command::Cancel"; } void Command::setOptionValue(const char *name, const QVariant &value, bool critical) { if (!name || !*name) { return; } const Private::Option opt = { value, true, critical }; const QMutexLocker locker(&d->mutex); d->inputs.options[name] = opt; } QVariant Command::optionValue(const char *name) const { if (!name || !*name) { return QVariant(); } const QMutexLocker locker(&d->mutex); const auto it = d->inputs.options.find(name); if (it == d->inputs.options.end()) { return QVariant(); } else { return it->second.value; } } void Command::setOption(const char *name, bool critical) { if (!name || !*name) { return; } const QMutexLocker locker(&d->mutex); if (isOptionSet(name)) { unsetOption(name); } const Private::Option opt = { QVariant(), false, critical }; d->inputs.options[name] = opt; } void Command::unsetOption(const char *name) { if (!name || !*name) { return; } const QMutexLocker locker(&d->mutex); d->inputs.options.erase(name); } bool Command::isOptionSet(const char *name) const { if (!name || !*name) { return false; } const QMutexLocker locker(&d->mutex); return d->inputs.options.count(name); } bool Command::isOptionCritical(const char *name) const { if (!name || !*name) { return false; } const QMutexLocker locker(&d->mutex); const auto it = d->inputs.options.find(name); return it != d->inputs.options.end() && it->second.isCritical; } void Command::setFilePaths(const QStringList &filePaths) { const QMutexLocker locker(&d->mutex); d->inputs.filePaths = filePaths; } QStringList Command::filePaths() const { const QMutexLocker locker(&d->mutex); return d->inputs.filePaths; } void Command::setRecipients(const QStringList &recipients, bool informative) { const QMutexLocker locker(&d->mutex); d->inputs.recipients = recipients; d->inputs.areRecipientsInformative = informative; } QStringList Command::recipients() const { const QMutexLocker locker(&d->mutex); return d->inputs.recipients; } bool Command::areRecipientsInformative() const { const QMutexLocker locker(&d->mutex); return d->inputs.areRecipientsInformative; } void Command::setSenders(const QStringList &senders, bool informative) { const QMutexLocker locker(&d->mutex); d->inputs.senders = senders; d->inputs.areSendersInformative = informative; } QStringList Command::senders() const { const QMutexLocker locker(&d->mutex); return d->inputs.senders; } bool Command::areSendersInformative() const { const QMutexLocker locker(&d->mutex); return d->inputs.areSendersInformative; } void Command::setInquireData(const char *what, const QByteArray &data) { const QMutexLocker locker(&d->mutex); d->inputs.inquireData[what] = data; } void Command::unsetInquireData(const char *what) { const QMutexLocker locker(&d->mutex); d->inputs.inquireData.erase(what); } QByteArray Command::inquireData(const char *what) const { const QMutexLocker locker(&d->mutex); const auto it = d->inputs.inquireData.find(what); if (it == d->inputs.inquireData.end()) { return QByteArray(); } else { return it->second; } } bool Command::isInquireDataSet(const char *what) const { const QMutexLocker locker(&d->mutex); const auto it = d->inputs.inquireData.find(what); return it != d->inputs.inquireData.end(); } QByteArray Command::receivedData() const { const QMutexLocker locker(&d->mutex); return d->outputs.data; } void Command::setCommand(const char *command) { const QMutexLocker locker(&d->mutex); d->inputs.command = command; } QByteArray Command::command() const { const QMutexLocker locker(&d->mutex); return d->inputs.command; } // // here comes the ugly part // #ifdef HAVE_ASSUAN2 static void my_assuan_release(assuan_context_t ctx) { if (ctx) { assuan_release(ctx); } } #endif using AssuanContextBase = std::shared_ptr::type>; namespace { struct AssuanClientContext : AssuanContextBase { AssuanClientContext() : AssuanContextBase() {} #ifndef HAVE_ASSUAN2 explicit AssuanClientContext(assuan_context_t ctx) : AssuanContextBase(ctx, &assuan_disconnect) {} void reset(assuan_context_t ctx = nullptr) { AssuanContextBase::reset(ctx, &assuan_disconnect); } #else explicit AssuanClientContext(assuan_context_t ctx) : AssuanContextBase(ctx, &my_assuan_release) {} void reset(assuan_context_t ctx = nullptr) { AssuanContextBase::reset(ctx, &my_assuan_release); } #endif }; } #ifdef HAVE_ASSUAN2 // compatibility typedef - remove when we require assuan v2... using assuan_error_t = gpg_error_t; #endif static assuan_error_t my_assuan_transact(const AssuanClientContext &ctx, const char *command, assuan_error_t (*data_cb)(void *, const void *, size_t) = nullptr, void *data_cb_arg = nullptr, assuan_error_t (*inquire_cb)(void *, const char *) = nullptr, void *inquire_cb_arg = nullptr, assuan_error_t (*status_cb)(void *, const char *) = nullptr, void *status_cb_arg = nullptr) { return assuan_transact(ctx.get(), command, data_cb, data_cb_arg, inquire_cb, inquire_cb_arg, status_cb, status_cb_arg); } static QString to_error_string(int err) { char buffer[1024]; gpg_strerror_r(static_cast(err), buffer, sizeof buffer); buffer[sizeof buffer - 1] = '\0'; return QString::fromLocal8Bit(buffer); } static QString gnupg_home_directory() { static const char *hDir = GpgME::dirInfo("homedir"); return QFile::decodeName(hDir); } static QString get_default_socket_name() { const QString socketPath{QString::fromUtf8(GpgME::dirInfo("uiserver-socket"))}; if (!socketPath.isEmpty()) { // Note: The socket directory exists after GpgME::dirInfo() has been called. return socketPath; } const QString homeDir = gnupg_home_directory(); if (homeDir.isEmpty()) { return QString(); } return QDir(homeDir).absoluteFilePath(QStringLiteral("S.uiserver")); } static QString default_socket_name() { static QString name = get_default_socket_name(); return name; } static QString uiserver_executable() { return QStringLiteral("kleopatra"); } static QString start_uiserver() { - const QString executable = uiserver_executable(); - const QString exec = QStandardPaths::findExecutable(executable); - if (exec.isEmpty()) { - qCWarning(LIBKLEOPATRACLIENTCORE_LOG) - << "Could not find " << executable << " in PATH."; - return i18n("Failed to start uiserver %1", executable); - } else { - QProcess::startDetached(executable, QStringList() - << QStringLiteral("--daemon")); - } - return QString(); + // Warning: Don't assume that the program needs to be in PATH. On Windows, it will also be found next to the calling process. + if (!QProcess::startDetached(uiserver_executable(), QStringList() << QStringLiteral("--daemon"))) { + return i18n("Failed to start uiserver %1", uiserver_executable()); + } else { + return QString(); + } } static assuan_error_t getinfo_pid_cb(void *opaque, const void *buffer, size_t length) { qint64 &pid = *static_cast(opaque); pid = QByteArray(static_cast(buffer), length).toLongLong(); return 0; } static assuan_error_t command_data_cb(void *opaque, const void *buffer, size_t length) { QByteArray &ba = *static_cast(opaque); ba.append(QByteArray(static_cast(buffer), length)); return 0; } namespace { struct inquire_data { const std::map *map; const AssuanClientContext *ctx; }; } static assuan_error_t command_inquire_cb(void *opaque, const char *what) { if (!opaque) { return 0; } const inquire_data &id = *static_cast(opaque); const auto it = id.map->find(what); if (it != id.map->end()) { const QByteArray &v = it->second; assuan_send_data(id.ctx->get(), v.data(), v.size()); } return 0; } static inline std::ostream &operator<<(std::ostream &s, const QByteArray &ba) { return s << std::string(ba.data(), ba.size()); } static assuan_error_t send_option(const AssuanClientContext &ctx, const char *name, const QVariant &value) { std::stringstream ss; ss << "OPTION " << name; if (value.isValid()) { ss << '=' << value.toString().toUtf8(); } return my_assuan_transact(ctx, ss.str().c_str()); } static assuan_error_t send_file(const AssuanClientContext &ctx, const QString &file) { std::stringstream ss; ss << "FILE " << hexencode(QFile::encodeName(file)); return my_assuan_transact(ctx, ss.str().c_str()); } static assuan_error_t send_recipient(const AssuanClientContext &ctx, const QString &recipient, bool info) { std::stringstream ss; ss << "RECIPIENT "; if (info) { ss << "--info "; } ss << "--" << hexencode(recipient.toUtf8()); return my_assuan_transact(ctx, ss.str().c_str()); } static assuan_error_t send_sender(const AssuanClientContext &ctx, const QString &sender, bool info) { std::stringstream ss; ss << "SENDER "; if (info) { ss << "--info "; } ss << "--" << hexencode(sender.toUtf8()); return my_assuan_transact(ctx, ss.str().c_str()); } void Command::Private::run() { // Take a snapshot of the input data, and clear the output data: Inputs in; Outputs out; { const QMutexLocker locker(&mutex); in = inputs; outputs = out; } out.canceled = false; if (out.serverLocation.isEmpty()) { out.serverLocation = default_socket_name(); } #ifndef HAVE_ASSUAN2 assuan_context_t naked_ctx = 0; #endif AssuanClientContext ctx; assuan_error_t err = 0; inquire_data id = { &in.inquireData, &ctx }; const QString socketName = out.serverLocation; if (socketName.isEmpty()) { out.errorString = i18n("Invalid socket name!"); goto leave; } #ifndef HAVE_ASSUAN2 err = assuan_socket_connect(&naked_ctx, socketName.toUtf8().constData(), -1); #else { assuan_context_t naked_ctx = nullptr; err = assuan_new(&naked_ctx); if (err) { out.errorString = i18n("Could not allocate resources to connect to Kleopatra UI server at %1: %2" , socketName, to_error_string(err)); goto leave; } ctx.reset(naked_ctx); } err = assuan_socket_connect(ctx.get(), socketName.toUtf8().constData(), -1, 0); #endif if (err) { qDebug("UI server not running, starting it"); const QString errorString = start_uiserver(); if (!errorString.isEmpty()) { out.errorString = errorString; goto leave; } // give it a bit of time to start up and try a couple of times for (int i = 0; err && i < 20; ++i) { msleep(500); err = assuan_socket_connect(ctx.get(), socketName.toUtf8().constData(), -1, 0); } } if (err) { out.errorString = i18n("Could not connect to Kleopatra UI server at %1: %2", socketName, to_error_string(err)); goto leave; } #ifndef HAVE_ASSUAN2 ctx.reset(naked_ctx); naked_ctx = 0; #endif out.serverPid = -1; err = my_assuan_transact(ctx, "GETINFO pid", &getinfo_pid_cb, &out.serverPid); if (err || out.serverPid <= 0) { out.errorString = i18n("Could not get the process-id of the Kleopatra UI server at %1: %2", socketName, to_error_string(err)); goto leave; } qCDebug(LIBKLEOPATRACLIENTCORE_LOG) << "Server PID =" << out.serverPid; #if defined(Q_OS_WIN) if (!AllowSetForegroundWindow((pid_t)out.serverPid)) { qCDebug(LIBKLEOPATRACLIENTCORE_LOG) << "AllowSetForegroundWindow(" << out.serverPid << ") failed: " << GetLastError(); } #endif if (in.command.isEmpty()) { goto leave; } if (in.parentWId) { #if defined(Q_OS_WIN32) err = send_option(ctx, "window-id", QString::asprintf("%lx", reinterpret_cast(in.parentWId))); #else err = send_option(ctx, "window-id", QString::asprintf("%lx", static_cast(in.parentWId))); #endif if (err) { qDebug("sending option window-id failed - ignoring"); } } for (auto it = in.options.begin(), end = in.options.end(); it != end; ++it) if ((err = send_option(ctx, it->first.c_str(), it->second.hasValue ? it->second.value.toString() : QVariant()))) { if (it->second.isCritical) { out.errorString = i18n("Failed to send critical option %1: %2", QString::fromLatin1(it->first.c_str()), to_error_string(err)); goto leave; } else { qCDebug(LIBKLEOPATRACLIENTCORE_LOG) << "Failed to send non-critical option" << it->first.c_str() << ":" << to_error_string(err); } } for (const QString &filePath : std::as_const(in.filePaths)) { if ((err = send_file(ctx, filePath))) { out.errorString = i18n("Failed to send file path %1: %2", filePath, to_error_string(err)); goto leave; } } for (const QString &sender : std::as_const(in.senders)) { if ((err = send_sender(ctx, sender, in.areSendersInformative))) { out.errorString = i18n("Failed to send sender %1: %2", sender, to_error_string(err)); goto leave; } } for (const QString &recipient : std::as_const(in.recipients)) { if ((err = send_recipient(ctx, recipient, in.areRecipientsInformative))) { out.errorString = i18n("Failed to send recipient %1: %2", recipient, to_error_string(err)); goto leave; } } #if 0 setup I / O; #endif err = my_assuan_transact(ctx, in.command.constData(), &command_data_cb, &out.data, &command_inquire_cb, &id); if (err) { if (gpg_err_code(err) == GPG_ERR_CANCELED) { out.canceled = true; } else { out.errorString = i18n("Command (%1) failed: %2", QString::fromLatin1(in.command.constData()), to_error_string(err)); } goto leave; } leave: const QMutexLocker locker(&mutex); // copy outputs to where Command can see them: outputs = out; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 1a8a87b5f..cd63062f7 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,820 +1,814 @@ /* -*- mode: c++; c-basic-offset:4 -*- mainwindow.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "mainwindow.h" #include "aboutdata.h" #include "settings.h" #include #include "view/padwidget.h" #include "view/searchbar.h" #include "view/tabwidget.h" #include "view/keylistcontroller.h" #include "view/keycacheoverlay.h" #include "view/smartcardwidget.h" #include "view/welcomewidget.h" #include "commands/selftestcommand.h" #include "commands/importcrlcommand.h" #include "commands/importcertificatefromfilecommand.h" #include "commands/decryptverifyfilescommand.h" #include "commands/signencryptfilescommand.h" #include "conf/groupsconfigdialog.h" #include "utils/detail_p.h" #include #include "utils/action_data.h" #include "utils/filedialog.h" #include "utils/clipboardmenu.h" #include "utils/gui-helper.h" #include "utils/qt-cxx20-compat.h" #include "dialogs/updatenotification.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std::chrono_literals; using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; static KGuiItem KStandardGuiItem_quit() { static const QString app = KAboutData::applicationData().displayName(); KGuiItem item = KStandardGuiItem::quit(); item.setText(xi18nc("@action:button", "&Quit %1", app)); return item; } static KGuiItem KStandardGuiItem_close() { KGuiItem item = KStandardGuiItem::close(); item.setText(i18nc("@action:button", "Only &Close Window")); return item; } static bool isQuitting = false; namespace { static const std::vector mainViewActionNames = { QStringLiteral("view_certificate_overview"), QStringLiteral("manage_smartcard"), QStringLiteral("pad_view") }; class CertificateView : public QWidget, public FocusFirstChild { Q_OBJECT public: CertificateView(QWidget* parent = nullptr) : QWidget{parent} , ui{this} { } SearchBar *searchBar() const { return ui.searchBar; } TabWidget *tabWidget() const { return ui.tabWidget; } void focusFirstChild(Qt::FocusReason reason) override { ui.searchBar->lineEdit()->setFocus(reason); } private: struct UI { TabWidget *tabWidget = nullptr; SearchBar *searchBar = nullptr; explicit UI(CertificateView *q) { auto vbox = new QVBoxLayout{q}; vbox->setSpacing(0); searchBar = new SearchBar{q}; vbox->addWidget(searchBar); tabWidget = new TabWidget{q}; vbox->addWidget(tabWidget); tabWidget->connectSearchBar(searchBar); } } ui; }; } class MainWindow::Private { friend class ::MainWindow; MainWindow *const q; public: explicit Private(MainWindow *qq); ~Private(); template void createAndStart() { (new T(this->currentView(), &this->controller))->start(); } template void createAndStart(QAbstractItemView *view) { (new T(view, &this->controller))->start(); } template void createAndStart(const QStringList &a) { (new T(a, this->currentView(), &this->controller))->start(); } template void createAndStart(const QStringList &a, QAbstractItemView *view) { (new T(a, view, &this->controller))->start(); } void closeAndQuit() { const QString app = KAboutData::applicationData().displayName(); const int rc = KMessageBox::questionTwoActionsCancel(q, xi18n("%1 may be used by other applications as a service." "You may instead want to close this window without exiting %1.", app), i18nc("@title:window", "Really Quit?"), KStandardGuiItem_close(), KStandardGuiItem_quit(), KStandardGuiItem::cancel(), QLatin1String("really-quit-") + app.toLower()); if (rc == KMessageBox::Cancel) { return; } isQuitting = true; if (!q->close()) { return; } // WARNING: 'this' might be deleted at this point! if (rc == KMessageBox::ButtonCode::SecondaryAction) { qApp->quit(); } } void configureToolbars() { KEditToolBar dlg(q->factory()); dlg.exec(); } void editKeybindings() { KShortcutsDialog::showDialog(q->actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, q); updateSearchBarClickMessage(); } void updateSearchBarClickMessage() { const QString shortcutStr = focusToClickSearchAction->shortcut().toString(); ui.searchTab->searchBar()->updateClickMessage(shortcutStr); } void updateStatusBar() { if (DeVSCompliance::isActive()) { auto statusBar = std::make_unique(); auto statusLbl = std::make_unique(DeVSCompliance::name()); if (!SystemInfo::isHighContrastModeActive()) { const auto color = KColorScheme(QPalette::Active, KColorScheme::View).foreground( DeVSCompliance::isCompliant() ? KColorScheme::NormalText: KColorScheme::NegativeText ).color(); const auto background = KColorScheme(QPalette::Active, KColorScheme::View).background( DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground ).color(); statusLbl->setStyleSheet(QStringLiteral("QLabel { color: %1; background-color: %2; }"). arg(color.name()).arg(background.name())); } statusBar->insertPermanentWidget(0, statusLbl.release()); q->setStatusBar(statusBar.release()); // QMainWindow takes ownership } else { q->setStatusBar(nullptr); } } void selfTest() { createAndStart(); } void configureGroups() { if (KConfigDialog::showDialog(GroupsConfigDialog::dialogName())) { return; } KConfigDialog *dialog = new GroupsConfigDialog(q); dialog->show(); } void showHandbook(); void gnupgLogViewer() { - const QString exec = - QStandardPaths::findExecutable(QStringLiteral("kwatchgnupg")); - if (exec.isEmpty()) { - KMessageBox::error( - q, - i18n("Could not start the GnuPG Log Viewer (kwatchgnupg). " - "Please check your installation."), - i18n("Error Starting KWatchGnuPG")); - } else { - QProcess::startDetached(QStringLiteral("kwatchgnupg"), QStringList()); - } + // Warning: Don't assume that the program needs to be in PATH. On Windows, it will also be found next to the calling process. + if (!QProcess::startDetached(QStringLiteral("kwatchgnupg"), QStringList())) + KMessageBox::error(q, i18n("Could not start the GnuPG Log Viewer (kwatchgnupg). " + "Please check your installation."), + i18n("Error Starting KWatchGnuPG")); } void forceUpdateCheck() { UpdateNotification::forceUpdateCheck(q); } void slotConfigCommitted(); void slotContextMenuRequested(QAbstractItemView *, const QPoint &p) { if (auto const menu = qobject_cast(q->factory()->container(QStringLiteral("listview_popup"), q))) { menu->exec(p); } else { qCDebug(KLEOPATRA_LOG) << "no \"listview_popup\" in kleopatra's ui.rc file"; } } void slotFocusQuickSearch() { ui.searchTab->searchBar()->lineEdit()->setFocus(); } void showView(const QString &actionName, QWidget *widget) { const auto coll = q->actionCollection(); if (coll) { for ( const QString &name : mainViewActionNames ) { if (auto action = coll->action(name)) { action->setChecked(name == actionName); } } } ui.stackWidget->setCurrentWidget(widget); if (auto ffci = dynamic_cast(widget)) { ffci->focusFirstChild(Qt::TabFocusReason); } } void showCertificateView() { if (KeyCache::instance()->keys().empty()) { showView(QStringLiteral("view_certificate_overview"), ui.welcomeWidget); } else { showView(QStringLiteral("view_certificate_overview"), ui.searchTab); } } void showSmartcardView() { showView(QStringLiteral("manage_smartcard"), ui.scWidget); } void showPadView() { if (!ui.padWidget) { ui.padWidget = new PadWidget; ui.stackWidget->addWidget(ui.padWidget); } showView(QStringLiteral("pad_view"), ui.padWidget); ui.stackWidget->resize(ui.padWidget->sizeHint()); } void restartDaemons() { Kleo::killDaemons(); } private: void setupActions(); QAbstractItemView *currentView() const { return ui.searchTab->tabWidget()->currentView(); } void keyListingDone() { const auto curWidget = ui.stackWidget->currentWidget(); if (curWidget == ui.scWidget || curWidget == ui.padWidget) { return; } showCertificateView(); } private: Kleo::KeyListController controller; bool firstShow : 1; struct UI { CertificateView *searchTab = nullptr; PadWidget *padWidget = nullptr; SmartCardWidget *scWidget = nullptr; WelcomeWidget *welcomeWidget = nullptr; QStackedWidget *stackWidget = nullptr; explicit UI(MainWindow *q); } ui; QAction *focusToClickSearchAction = nullptr; ClipboardMenu *clipboadMenu = nullptr; }; MainWindow::Private::UI::UI(MainWindow *q) : padWidget(nullptr) { auto mainWidget = new QWidget{q}; auto mainLayout = new QVBoxLayout(mainWidget); stackWidget = new QStackedWidget{q}; searchTab = new CertificateView{q}; stackWidget->addWidget(searchTab); new KeyCacheOverlay(mainWidget, q); scWidget = new SmartCardWidget{q}; stackWidget->addWidget(scWidget); welcomeWidget = new WelcomeWidget{q}; stackWidget->addWidget(welcomeWidget); mainLayout->addWidget(stackWidget); q->setCentralWidget(mainWidget); } MainWindow::Private::Private(MainWindow *qq) : q(qq), controller(q), firstShow(true), ui(q) { KDAB_SET_OBJECT_NAME(controller); AbstractKeyListModel *flatModel = AbstractKeyListModel::createFlatKeyListModel(q); AbstractKeyListModel *hierarchicalModel = AbstractKeyListModel::createHierarchicalKeyListModel(q); KDAB_SET_OBJECT_NAME(flatModel); KDAB_SET_OBJECT_NAME(hierarchicalModel); controller.setFlatModel(flatModel); controller.setHierarchicalModel(hierarchicalModel); controller.setTabWidget(ui.searchTab->tabWidget()); ui.searchTab->tabWidget()->setFlatModel(flatModel); ui.searchTab->tabWidget()->setHierarchicalModel(hierarchicalModel); setupActions(); ui.stackWidget->setCurrentWidget(ui.searchTab); if (auto action = q->actionCollection()->action(QStringLiteral("view_certificate_overview"))) { action->setChecked(true); } connect(&controller, SIGNAL(contextMenuRequested(QAbstractItemView*,QPoint)), q, SLOT(slotContextMenuRequested(QAbstractItemView*,QPoint))); connect(KeyCache::instance().get(), &KeyCache::keyListingDone, q, [this] () {keyListingDone();}); q->createGUI(QStringLiteral("kleopatra.rc")); // make toolbar buttons accessible by keyboard auto toolbar = q->findChild(); if (toolbar) { auto toolbarButtons = toolbar->findChildren(); for (auto b : toolbarButtons) { b->setFocusPolicy(Qt::TabFocus); } // move toolbar and its child widgets before the central widget in the tab order; // this is necessary to make Shift+Tab work as expected forceSetTabOrder(q, toolbar); auto toolbarChildren = toolbar->findChildren(); std::for_each(std::rbegin(toolbarChildren), std::rend(toolbarChildren), [toolbar](auto w) { forceSetTabOrder(toolbar, w); }); } const auto title = Kleo::brandingWindowTitle(); if (!title.isEmpty()) { QApplication::setApplicationDisplayName(title); } const auto icon = Kleo::brandingIcon(); if (!icon.isEmpty()) { const auto dir = QDir(Kleo::gpg4winInstallPath() + QStringLiteral("/../share/kleopatra/pics")); qCDebug(KLEOPATRA_LOG) << "Loading branding icon:" << dir.absoluteFilePath(icon); QPixmap brandingIcon(dir.absoluteFilePath(icon)); if (!brandingIcon.isNull()) { auto *w = new QWidget; auto *hl = new QHBoxLayout; auto *lbl = new QLabel; w->setLayout(hl); hl->addWidget(lbl); lbl->setPixmap(brandingIcon); toolbar->addSeparator(); toolbar->addWidget(w); } } q->setAcceptDrops(true); // set default window size q->resize(QSize(1024, 500)); q->setAutoSaveSettings(); updateSearchBarClickMessage(); updateStatusBar(); if (KeyCache::instance()->initialized()) { keyListingDone(); } } MainWindow::Private::~Private() {} MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) : KXmlGuiWindow(parent, flags), d(new Private(this)) {} MainWindow::~MainWindow() {} void MainWindow::Private::setupActions() { KActionCollection *const coll = q->actionCollection(); const std::vector action_data = { // see keylistcontroller.cpp for more actions // Tools menu #ifndef Q_OS_WIN { "tools_start_kwatchgnupg", i18n("GnuPG Log Viewer"), QString(), "kwatchgnupg", q, [this](bool) { gnupgLogViewer(); }, QString() }, #endif { "tools_restart_backend", i18nc("@action:inmenu", "Restart Background Processes"), i18nc("@info:tooltip", "Restart the background processes, e.g. after making changes to the configuration."), "view-refresh", q, [this](bool) { restartDaemons(); }, {} }, // Help menu #ifdef Q_OS_WIN { "help_check_updates", i18n("Check for updates"), QString(), "gpg4win-compact", q, [this](bool) { forceUpdateCheck(); }, QString() }, #endif // View menu { "view_certificate_overview", i18nc("@action show certificate overview", "Certificates"), i18n("Show certificate overview"), "view-certificate", q, [this](bool) { showCertificateView(); }, QString() }, { "pad_view", i18nc("@action show input / output area for encrypting/signing resp. decrypting/verifying text", "Notepad"), i18n("Show pad for encrypting/decrypting and signing/verifying text"), "note", q, [this](bool) { showPadView(); }, QString() }, { "manage_smartcard", i18nc("@action show smartcard management view", "Smartcards"), i18n("Show smartcard management"), "auth-sim-locked", q, [this](bool) { showSmartcardView(); }, QString() }, // Settings menu { "settings_self_test", i18n("Perform Self-Test"), QString(), nullptr, q, [this](bool) { selfTest(); }, QString() }, { "configure_groups", i18n("Configure Groups..."), QString(), "group", q, [this](bool) { configureGroups(); }, QString() } }; make_actions_from_data(action_data, coll); if (!Settings().groupsEnabled()) { if (auto action = coll->action(QStringLiteral("configure_groups"))) { delete action; } } for ( const QString &name : mainViewActionNames ) { if (auto action = coll->action(name)) { action->setCheckable(true); } } KStandardAction::close(q, SLOT(close()), coll); KStandardAction::quit(q, SLOT(closeAndQuit()), coll); KStandardAction::configureToolbars(q, SLOT(configureToolbars()), coll); KStandardAction::keyBindings(q, SLOT(editKeybindings()), coll); KStandardAction::preferences(qApp, SLOT(openOrRaiseConfigDialog()), coll); focusToClickSearchAction = new QAction(i18n("Set Focus to Quick Search"), q); coll->addAction(QStringLiteral("focus_to_quickseach"), focusToClickSearchAction); coll->setDefaultShortcut(focusToClickSearchAction, QKeySequence(Qt::ALT | Qt::Key_Q)); connect(focusToClickSearchAction, SIGNAL(triggered(bool)), q, SLOT(slotFocusQuickSearch())); clipboadMenu = new ClipboardMenu(q); clipboadMenu->setMainWindow(q); clipboadMenu->clipboardMenu()->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste"))); clipboadMenu->clipboardMenu()->setPopupMode(QToolButton::InstantPopup); coll->addAction(QStringLiteral("clipboard_menu"), clipboadMenu->clipboardMenu()); /* Add additional help actions for documentation */ const auto compendium = new DocAction(QIcon::fromTheme(QStringLiteral("gpg4win-compact")), i18n("Gpg4win Compendium"), i18nc("The Gpg4win compendium is only available" "at this point (24.7.2017) in german and english." "Please check with Gpg4win before translating this filename.", "gpg4win-compendium-en.pdf"), QStringLiteral("../share/gpg4win")); coll->addAction(QStringLiteral("help_doc_compendium"), compendium); /* Documentation centered around the german approved VS-NfD mode for official * RESTRICTED communication. This is only available in some distributions with * the focus on official communications. */ const auto symguide = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("Password-based encryption"), i18nc("Only available in German and English. Leave to English for other languages.", "handout_symmetric_encryption_gnupg_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); coll->addAction(QStringLiteral("help_doc_symenc"), symguide); const auto quickguide = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("Quickguide"), i18nc("Only available in German and English. Leave to English for other languages.", "handout_sign_encrypt_gnupg_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); coll->addAction(QStringLiteral("help_doc_quickguide"), quickguide); const auto vsa10573 = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("SecOps VSA-10573"), i18nc("Only available in German and English. Leave to English for other languages.", "BSI-VSA-10573-ENG_secops-20220207.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); coll->addAction(QStringLiteral("help_doc_vsa10573"), vsa10573); const auto vsa10584 = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("SecOps VSA-10584"), i18nc("Only available in German and English. Leave to English for other languages.", "BSI-VSA-10584-ENG_secops-20220207.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); coll->addAction(QStringLiteral("help_doc_vsa10584"), vsa10584); const auto smartcard = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("Smartcard Management"), i18nc("Only available in German and English. Leave to English for other languages.", "smartcard_howto_de.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); coll->addAction(QStringLiteral("help_doc_smartcard"), smartcard); const auto groups = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("Group configuration"), i18nc("Only available in German and English. Leave to English for other languages.", "handout_group-feature_gnupg_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); coll->addAction(QStringLiteral("help_doc_groups"), groups); const auto man_gpg = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("GnuPG Manual"), QStringLiteral("gnupg.pdf"), QStringLiteral("../share/doc/gnupg")); coll->addAction(QStringLiteral("help_doc_gnupg"), man_gpg); q->setStandardToolBarMenuEnabled(true); controller.createActions(coll); ui.searchTab->tabWidget()->createActions(coll); } void MainWindow::Private::slotConfigCommitted() { controller.updateConfig(); updateStatusBar(); } void MainWindow::closeEvent(QCloseEvent *e) { // KMainWindow::closeEvent() insists on quitting the application, // so do not let it touch the event... qCDebug(KLEOPATRA_LOG); if (d->controller.hasRunningCommands()) { if (d->controller.shutdownWarningRequired()) { const int ret = KMessageBox::warningContinueCancel(this, i18n("There are still some background operations ongoing. " "These will be terminated when closing the window. " "Proceed?"), i18n("Ongoing Background Tasks")); if (ret != KMessageBox::Continue) { e->ignore(); return; } } d->controller.cancelCommands(); if (d->controller.hasRunningCommands()) { // wait for them to be finished: setEnabled(false); QEventLoop ev; QTimer::singleShot(100ms, &ev, &QEventLoop::quit); connect(&d->controller, &KeyListController::commandsExecuting, &ev, &QEventLoop::quit); ev.exec(); if (d->controller.hasRunningCommands()) qCWarning(KLEOPATRA_LOG) << "controller still has commands running, this may crash now..."; setEnabled(true); } } if (isQuitting || qApp->isSavingSession()) { d->ui.searchTab->tabWidget()->saveViews(KSharedConfig::openConfig().data()); KConfigGroup grp(KConfigGroup(KSharedConfig::openConfig(), autoSaveGroup())); saveMainWindowSettings(grp); e->accept(); } else { e->ignore(); hide(); } } void MainWindow::showEvent(QShowEvent *e) { KXmlGuiWindow::showEvent(e); if (d->firstShow) { d->ui.searchTab->tabWidget()->loadViews(KSharedConfig::openConfig().data()); d->firstShow = false; } if (!savedGeometry.isEmpty()) { restoreGeometry(savedGeometry); } } void MainWindow::hideEvent(QHideEvent *e) { savedGeometry = saveGeometry(); KXmlGuiWindow::hideEvent(e); } void MainWindow::importCertificatesFromFile(const QStringList &files) { if (!files.empty()) { d->createAndStart(files); } } static QStringList extract_local_files(const QMimeData *data) { const QList urls = data->urls(); // begin workaround KDE/Qt misinterpretation of text/uri-list QList::const_iterator end = urls.end(); if (urls.size() > 1 && !urls.back().isValid()) { --end; } // end workaround QStringList result; std::transform(urls.begin(), end, std::back_inserter(result), std::mem_fn(&QUrl::toLocalFile)); result.erase(std::remove_if(result.begin(), result.end(), std::mem_fn(&QString::isEmpty)), result.end()); return result; } static bool can_decode_local_files(const QMimeData *data) { if (!data) { return false; } return !extract_local_files(data).empty(); } void MainWindow::dragEnterEvent(QDragEnterEvent *e) { qCDebug(KLEOPATRA_LOG); if (can_decode_local_files(e->mimeData())) { e->acceptProposedAction(); } } void MainWindow::dropEvent(QDropEvent *e) { qCDebug(KLEOPATRA_LOG); if (!can_decode_local_files(e->mimeData())) { return; } e->setDropAction(Qt::CopyAction); const QStringList files = extract_local_files(e->mimeData()); const unsigned int classification = classify(files); QMenu menu; QAction *const signEncrypt = menu.addAction(i18n("Sign/Encrypt...")); QAction *const decryptVerify = mayBeAnyMessageType(classification) ? menu.addAction(i18n("Decrypt/Verify...")) : nullptr; if (signEncrypt || decryptVerify) { menu.addSeparator(); } QAction *const importCerts = mayBeAnyCertStoreType(classification) ? menu.addAction(i18n("Import Certificates")) : nullptr; QAction *const importCRLs = mayBeCertificateRevocationList(classification) ? menu.addAction(i18n("Import CRLs")) : nullptr; if (importCerts || importCRLs) { menu.addSeparator(); } if (!signEncrypt && !decryptVerify && !importCerts && !importCRLs) { return; } menu.addAction(i18n("Cancel")); const QAction *const chosen = menu.exec(mapToGlobal(e->pos())); if (!chosen) { return; } if (chosen == signEncrypt) { d->createAndStart(files); } else if (chosen == decryptVerify) { d->createAndStart(files); } else if (chosen == importCerts) { d->createAndStart(files); } else if (chosen == importCRLs) { d->createAndStart(files); } e->accept(); } void MainWindow::readProperties(const KConfigGroup &cg) { qCDebug(KLEOPATRA_LOG); KXmlGuiWindow::readProperties(cg); setHidden(cg.readEntry("hidden", false)); } void MainWindow::saveProperties(KConfigGroup &cg) { qCDebug(KLEOPATRA_LOG); KXmlGuiWindow::saveProperties(cg); cg.writeEntry("hidden", isHidden()); } #include "mainwindow.moc" #include "moc_mainwindow.cpp"