Page MenuHome GnuPG

No OneTemporary

diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt
index 8075659..ad8203a 100644
--- a/client/CMakeLists.txt
+++ b/client/CMakeLists.txt
@@ -1,391 +1,394 @@
# SPDX-FileCopyrightText: 2023 g10 code GmbH
# SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
# SPDX-License-Identifier: BSD-2-Clause
add_definitions(-DDATAROUTDIR="${KDE_INSTALL_FULL_DATAROOTDIR}")
add_definitions(-DTRANSLATION_DOMAIN=\"gpgol-js-native\")
add_subdirectory(rootcagenerator)
add_library(gpgol-client-static STATIC)
target_sources(gpgol-client-static PRIVATE
emailviewer.cpp
emailviewer.h
websocketclient.cpp
websocketclient.h
messagedispatcher.cpp
messagedispatcher.h
firsttimedialog.cpp
firsttimedialog.h
mailapi.cpp
securitylevelinfo.cpp
+ setupdialogs.cpp
+ statusdialog.cpp
+ connectioncontroller.cpp
# Identity
identity/addressvalidationjob.cpp
identity/addressvalidationjob.h
identity/identitymanager.cpp
identity/identitymanager.h
identity/identitydialog.cpp
identity/identitydialog.h
identity/identity.cpp
identity/identity.h
identity/signature.h
identity/signature.cpp
identity/signatureconfigurator.cpp
identity/signatureconfigurator.h
identity/signaturerichtexteditor.cpp
identity/signaturerichtexteditor_p.h
# Draft
draft/draft.cpp
draft/draft.h
draft/draftmanager.cpp
draft/draftmanager.h
# EWS integration
ews/ewsattachment.cpp
ews/ewsattachment.h
ews/ewsattendee.cpp
ews/ewsattendee.h
ews/ewsclient_debug.cpp
ews/ewsclient_debug.h
ews/ewsid.cpp
ews/ewsid.h
ews/ewsitem.cpp
ews/ewsitem.h
ews/ewsitemshape.cpp
ews/ewsitemshape.h
ews/ewsitembase.cpp
ews/ewsitembase.h
ews/ewsitembase_p.h
ews/ewsmailbox.cpp
ews/ewsmailbox.h
ews/ewsmessagedispatcher.cpp
ews/ewsmessagedispatcher.h
ews/ewsoccurrence.cpp
ews/ewsoccurrence.h
ews/ewspropertyfield.cpp
ews/ewspropertyfield.h
ews/ewsrecurrence.cpp
ews/ewsrecurrence.h
ews/ewsserverversion.cpp
ews/ewsserverversion.h
ews/ewstypes.cpp
ews/ewstypes.h
ews/ewsxml.cpp
ews/ewsxml.h
ews/ewscreateitemrequest.cpp
ews/ewscreateitemrequest.h
ews/ewsgetfolderrequest.cpp
ews/ewsgetfolderrequest.h
ews/ewsupdateitemrequest.cpp
ews/ewsupdateitemrequest.h
ews/ewsrequest.cpp
ews/ewsrequest.h
ews/ewsjob.cpp
ews/ewsjob.h
ews/ewsclient.cpp
ews/ewsclient.h
ews/ewsfinditemrequest.cpp
ews/ewsfinditemrequest.h
ews/ewsgetitemrequest.cpp
ews/ewsgetitemrequest.h
ews/ewsgetfolderrequest.cpp
ews/ewsgetfolderrequest.h
ews/ewsfolder.cpp
ews/ewsfolder.h
ews/ewscreatefolderrequest.cpp
ews/ewscreatefolderrequest.h
ews/ewseffectiverights.h
ews/ewseffectiverights.cpp
ews/ewsfoldershape.cpp
ews/ewsfoldershape.h
ews/ewscopyitemrequest.cpp
ews/ewscopyitemrequest.h
ews/ewsgetfoldercontentrequest.cpp
ews/ewsgetfoldercontentrequest.h
reencrypt/reencryptjob.cpp
reencrypt/reencryptjob.h
reencrypt/choosekeydialog.cpp
reencrypt/choosekeydialog.h
reencrypt/certificatelineedit.cpp
reencrypt/certificatelineedit.h
reencrypt/reencryptprogressdialog.cpp
# Editor
editor/addresseelineedit.cpp
editor/addresseelineedit.h
editor/addresseelineeditmanager.cpp
editor/addresseelineeditmanager.h
editor/bodytexteditor.cpp
editor/bodytexteditor.h
editor/composerviewbase.cpp
editor/composerviewbase.h
editor/composerwindow.cpp
editor/composerwindow.h
editor/composerwindowfactory.cpp
editor/composerwindowfactory.h
editor/cryptostateindicatorwidget.cpp
editor/cryptostateindicatorwidget.h
editor/kmcomposerglobalaction.cpp
editor/kmcomposerglobalaction.h
editor/nearexpirywarning.cpp
editor/nearexpirywarning.h
editor/mailtemplates.cpp
editor/mailtemplates.h
editor/recipient.cpp
editor/recipient.h
editor/recipientline.cpp
editor/recipientline.h
editor/recipientseditor.cpp
editor/recipientseditor.h
editor/util.h
editor/util.cpp
editor/kmailcompletion.cpp
editor/kmailcompletion.h
editor/composersignatures.cpp
editor/composersignatures.h
editor/nodehelper.cpp
editor/nodehelper.h
editor/signaturecontroller.cpp
editor/signaturecontroller.h
editor/spellcheckerconfigdialog.cpp
editor/spellcheckerconfigdialog.h
# Editor job
editor/job/abstractencryptjob.h
editor/job/autocryptheadersjob.h
editor/job/contentjobbase.h
editor/job/contentjobbase_p.h
editor/job/composerjob.cpp
editor/job/composerjob.h
editor/job/encryptjob.h
editor/job/inserttextfilejob.h
editor/job/itipjob.h
editor/job/jobbase.h
editor/job/jobbase_p.h
editor/job/maintextjob.h
editor/job/multipartjob.h
editor/job/protectedheadersjob.h
editor/job/signencryptjob.h
editor/job/signjob.h
editor/job/singlepartjob.h
editor/job/skeletonmessagejob.h
editor/job/transparentjob.h
editor/job/autocryptheadersjob.cpp
editor/job/contentjobbase.cpp
editor/job/encryptjob.cpp
editor/job/inserttextfilejob.cpp
editor/job/itipjob.cpp
editor/job/jobbase.cpp
editor/job/maintextjob.cpp
editor/job/multipartjob.cpp
editor/job/protectedheadersjob.cpp
editor/job/saveasfilejob.cpp
editor/job/saveasfilejob.h
editor/job/signencryptjob.cpp
editor/job/signjob.cpp
editor/job/singlepartjob.cpp
editor/job/skeletonmessagejob.cpp
editor/job/transparentjob.cpp
## Editor Part
editor/part/globalpart.h
editor/part/infopart.h
editor/part/itippart.h
editor/part/messagepart.h
editor/part/textpart.h
editor/part/globalpart.cpp
editor/part/infopart.cpp
editor/part/itippart.cpp
editor/part/messagepart.cpp
editor/part/textpart.cpp
## Attachment
editor/attachment/attachmentjob.cpp
editor/attachment/attachmentjob.h
editor/attachment/attachmentclipboardjob.cpp
editor/attachment/attachmentclipboardjob.h
editor/attachment/attachmentcompressjob.cpp
editor/attachment/attachmentcompressjob.h
editor/attachment/attachmentcontroller.cpp
editor/attachment/attachmentcontroller.h
editor/attachment/attachmentcontrollerbase.cpp
editor/attachment/attachmentcontrollerbase.h
editor/attachment/attachmentfromfolderjob.cpp
editor/attachment/attachmentfromfolderjob.h
editor/attachment/attachmentfrommimecontentjob.cpp
editor/attachment/attachmentfrommimecontentjob.h
editor/attachment/attachmentfromurlbasejob.cpp
editor/attachment/attachmentfromurlbasejob.h
editor/attachment/attachmentfromurljob.cpp
editor/attachment/attachmentfromurljob.h
editor/attachment/attachmentfromurlutils.cpp
editor/attachment/attachmentfromurlutils.h
editor/attachment/attachmentfrompublickeyjob.cpp
editor/attachment/attachmentfrompublickeyjob.h
editor/attachment/attachmentloadjob.cpp
editor/attachment/attachmentloadjob.h
editor/attachment/attachmentmodel.cpp
editor/attachment/attachmentmodel.h
editor/attachment/attachmentpart.cpp
editor/attachment/attachmentpart.h
editor/attachment/attachmentpropertiesdialog.cpp
editor/attachment/attachmentpropertiesdialog.h
editor/attachment/attachmentupdatejob.cpp
editor/attachment/attachmentupdatejob.h
editor/attachment/attachmentview.cpp
editor/attachment/attachmentview.h
# Multiplyingline
multiplyingline/multiplyingline.cpp
multiplyingline/multiplyingline.h
multiplyingline/multiplyinglineeditor.cpp
multiplyingline/multiplyinglineeditor.h
multiplyingline/multiplyinglineview_p.cpp
multiplyingline/multiplyinglineview_p.h
# Utils
utils/kuniqueservice.h
utils/kuniqueservice.cpp
utils/systemtrayicon.h
utils/systemtrayicon.cpp
)
if (WIN32)
add_definitions(-DHAVE_QDBUS=false)
target_sources(gpgol-client-static PRIVATE utils/kuniqueservice_win.cpp)
else()
add_definitions(-DHAVE_QDBUS=true)
target_sources(gpgol-client-static PRIVATE utils/kuniqueservice_dbus.cpp)
endif()
ki18n_wrap_ui(gpgol-client-static
editor/attachment/ui/attachmentpropertiesdialog.ui
editor/attachment/ui/attachmentpropertiesdialog_readonly.ui
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER gpgol_client_debug.h
IDENTIFIER GPGOL_CLIENT_LOG
CATEGORY_NAME org.gpgol.client
DESCRIPTION "General client log"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER websocket_debug.h
IDENTIFIER WEBSOCKET_LOG
CATEGORY_NAME org.gpgol.client.websocket
DESCRIPTION "Websocket connection in the client"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER ewsresource_debug.h
IDENTIFIER EWSRES_LOG
CATEGORY_NAME org.gpgol.ews
DESCRIPTION "Ews mail client"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER ewscli_debug.h
IDENTIFIER EWSCLI_LOG
CATEGORY_NAME org.gpgol.ews.client
DESCRIPTION "ews client (gpgol-client)"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER editor_debug.h
IDENTIFIER EDITOR_LOG
CATEGORY_NAME org.gpgol.editor
DESCRIPTION "mail composer"
EXPORT GPGOL
)
set(WARN_TOOMANY_RECIPIENTS_DEFAULT true)
set(ALLOW_SEMICOLON_AS_ADDRESS_SEPARATOR_DEFAULT true)
configure_file(editor/settings/messagecomposer.kcfg.in ${CMAKE_CURRENT_BINARY_DIR}/messagecomposer.kcfg)
kconfig_add_kcfg_files(gpgol-client-static editor/settings/messagecomposersettings.kcfgc config.kcfgc)
install(FILES composerui.rc DESTINATION ${KDE_INSTALL_KXMLGUIDIR}/gpgol-client)
target_sources(gpgol-client-static PUBLIC ${gpgol-client-static_SRCS})
ki18n_wrap_ui(gpgol-client-static firsttimedialog.ui)
ki18n_wrap_ui(gpgol-client-static confpagewelcome.ui)
ki18n_wrap_ui(gpgol-client-static confpageinstalladdin.ui)
ki18n_wrap_ui(gpgol-client-static confpageproxyoptions.ui)
ki18n_wrap_ui(gpgol-client-static confpagetlscertificate.ui)
ki18n_wrap_ui(gpgol-client-static reencrypt/choosekeydialog.ui)
target_link_libraries(gpgol-client-static PUBLIC
common
rootcagenerator
LibGpgError::LibGpgError
Qt6::HttpServer
Qt6::Widgets
Qt6::PrintSupport
Qt6::WebSockets
KF6::I18n
KF6::JobWidgets
KF6::CalendarCore
KF6::ConfigCore
KF6::ConfigGui
KF6::Contacts
KF6::Completion
KF6::CoreAddons
KF6::ColorScheme
KF6::Codecs
KF6::GuiAddons
KF6::SonnetUi
KF6::SonnetCore
KF6::WidgetsAddons
KF6::XmlGui
KF6::Archive
KF6::KIOWidgets
KPim6::MimeTreeParserCore
KPim6::MimeTreeParserWidgets
KPim6::Libkleo
${_gpgol_dbusaddons_libs}
)
if(Gpgmepp_VERSION VERSION_GREATER_EQUAL "2.0.0")
target_compile_definitions(gpgol-client-static PUBLIC GPGME2)
endif()
set(GPGOLWEB_ICON_DIR "${CMAKE_CURRENT_SOURCE_DIR}/icons")
file(GLOB ICONS_PNGS "${GPGOLWEB_ICON_DIR}/*gpgolweb.png")
file(GLOB ICONS_SVGS "${GPGOLWEB_ICON_DIR}/*gpgolweb.svg")
ecm_add_app_icon(_gpgol-client_SRCS ICONS ${ICONS_PNGS} ${ICONS_SVGS})
ecm_install_icons(ICONS ${ICONS_PNGS} ${ICONS_SVGS} DESTINATION ${KDE_INSTALL_ICONDIR})
if (ICONS_SVGS)
list(GET ICONS_SVGS 0 app_icon_svg)
configure_file(icons/icons.qrc.in icons.qrc @ONLY)
set(_gpgol-client_SRCS ${_gpgol-client_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/icons.qrc)
endif()
add_executable(gpgol-client main.cpp ${_gpgol-client_SRCS})
qt_add_resources(gpgol-client "manifest"
PREFIX "/gpgol-client"
FILES manifest.xml.in
)
target_link_libraries(gpgol-client PRIVATE gpgol-client-static)
if (BUILD_TESTING)
add_subdirectory(autotests)
endif()
install(TARGETS gpgol-client ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES com.gnupg.gpgolweb.desktop DESTINATION ${KDE_INSTALL_APPDIR})
diff --git a/client/connectioncontroller.cpp b/client/connectioncontroller.cpp
new file mode 100644
index 0000000..a8f6abb
--- /dev/null
+++ b/client/connectioncontroller.cpp
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "connectioncontroller.h"
+
+#include "config.h"
+#include "websocketclient.h"
+
+#include <QUuid>
+#include <QUrl>
+
+using namespace Qt::StringLiterals;
+
+ConnectionController::ConnectionController()
+{
+}
+
+void ConnectionController::startLocalServer()
+{
+ if (m_serverProcess.state() != QProcess::NotRunning) {
+ return;
+ }
+
+ m_serverProcess.start(u"gpgol-server"_s);
+}
+
+void ConnectionController::startWebsocketClient()
+{
+ const auto clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
+ auto &websocketClient = WebsocketClient::self(QUrl(u"wss://"_s + serverDomain() + u"/websocket"_s), clientId);
+}
+
+QString ConnectionController::serverDomain()
+{
+ return (Config::self()->isLocalServer() ? u"localhost:5656"_s : Config::self()->remoteAddress().toString());
+}
diff --git a/client/connectioncontroller.h b/client/connectioncontroller.h
new file mode 100644
index 0000000..3878dbf
--- /dev/null
+++ b/client/connectioncontroller.h
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QObject>
+#include <QProcess>
+
+class ConnectionController : public QObject
+{
+ Q_OBJECT
+public:
+ explicit ConnectionController();
+
+ static QString serverDomain();
+
+ void startLocalServer();
+ void startWebsocketClient();
+private:
+ QProcess m_serverProcess;
+};
diff --git a/client/firsttimedialog.cpp b/client/firsttimedialog.cpp
index 1828d30..92d8c53 100644
--- a/client/firsttimedialog.cpp
+++ b/client/firsttimedialog.cpp
@@ -1,419 +1,385 @@
// SPDX-FileCopyrightText: 2024 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "firsttimedialog.h"
#include "config.h"
#include "gpgolweb_version.h"
#include "rootcagenerator/controller.h"
#include "ui_confpageinstalladdin.h"
#include "ui_confpageproxyoptions.h"
#include "ui_confpagetlscertificate.h"
#include "ui_confpagewelcome.h"
#include "ui_firsttimedialog.h"
#include "websocketclient.h"
+#include "statusdialog.h"
+#include "setupdialogs.h"
#include <QCheckBox>
#include <QClipboard>
#include <QCloseEvent>
#include <QDesktopServices>
#include <QFile>
#include <QMargins>
#include <QDialog>
#include <QDialogButtonBox>
#include <QSaveFile>
#include <QSettings>
#include <QStandardPaths>
#include <QStatusBar>
#include <QStyle>
#include <QTemporaryDir>
#include <QToolBar>
#include <Libkleo/Compliance>
+#include <KAssistantDialog>
#include <KColorScheme>
#include <KIO/OpenFileManagerWindowJob>
#include <KTitleWidget>
using namespace Qt::StringLiterals;
-class PairingDialog : public QDialog {
-public:
- PairingDialog(QWidget *parent) : QDialog(parent) {
- setWindowTitle(i18n("Pairing mode active"));
- auto l = new QVBoxLayout(this);
- auto lab = new QLabel(i18n("<p>Copy and paste the code shown below into the input field shown at the top of the Add-In:</p>"));
- lab->setWordWrap(true);
- l->addWidget(lab);
- m_pairingTokenLabel = new QLabel(i18nc("status message", "<p align='center'><b>Obtaining token...</b></p>"));
- l->addWidget(m_pairingTokenLabel);
- auto bb = new QDialogButtonBox;
- auto endButton = bb->addButton(i18nc("@button", "End pairing mode"), QDialogButtonBox::RejectRole);
- connect(endButton, &QAbstractButton::clicked, this, &QDialog::reject);
- m_copyButton = bb->addButton(i18nc("@button", "Copy code to clipboard"), QDialogButtonBox::ActionRole);
- m_copyButton->setEnabled(false);
- l->addWidget(bb);
- setFixedSize(sizeHint());
-
- connect(&WebsocketClient::self(), &WebsocketClient::pairingStatusChanged, this, &PairingDialog::pairingStatusChanged);
- WebsocketClient::self().enterPairingMode();
- }
- void pairingStatusChanged(const QString& token, bool pairingActive) {
- if (!pairingActive) {
- reject();
- return;
- }
- m_pairingTokenLabel->setText(QString(u"<p align='center'><b>%1</b></p>").arg(token));
- m_copyButton->setEnabled(true);
- connect(m_copyButton, &QAbstractButton::clicked, [this, token]() {
- qApp->clipboard()->setText(token);
- });
- }
-private:
- QLabel* m_pairingTokenLabel;
- QAbstractButton* m_copyButton;
-};
-
FirstTimeDialog::FirstTimeDialog(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::FirstTimeDialog)
, confPageWelcome(new Ui::ConfPageWelcome)
, confPageInstallAddin(new Ui::ConfPageInstallAddin)
, confPageProxyOptions(new Ui::ConfPageProxyOptions)
, confPageTLSCertificate(new Ui::ConfPageTLSCertificate)
, m_systemTrayIcon(QIcon::fromTheme(u"com.gnupg.gpgolweb"_s))
{
ui->setupUi(this);
confPageWelcome->setupUi(ui->confPageWelcome);
ui->confPageWelcome->setProperty("title", i18nc("@title", "Welcome to GpgOL/Web"));
confPageProxyOptions->setupUi(ui->confPageProxyOptions);
ui->confPageProxyOptions->setProperty("title", i18nc("@title", "Configure Proxy and Optional Features"));
confPageInstallAddin->setupUi(ui->confPageInstallAddin);
ui->confPageInstallAddin->setProperty("title", i18nc("@title", "Install Outlook Add-In"));
confPageTLSCertificate->setupUi(ui->confPageTLSCertificate);
ui->confPageTLSCertificate->setProperty("title", i18nc("@title", "Setting Up TLS Certificate for Local Proxy"));
if (ui->stack->indexOf(ui->confPageWelcome) != ConfPageWelcome) {
qFatal("Welcome page misplaced");
}
if (ui->stack->indexOf(ui->confPageTLSCertificate) != ConfPageTLSCertificate) {
qFatal("Tls certification page misplaced");
}
if (ui->stack->indexOf(ui->confPageProxyOptions) != ConfPageProxyOptions) {
qFatal("Proxy options page misplaced");
}
if (ui->stack->indexOf(ui->confPageInstallAddin) != ManifestPage) {
qFatal("Manifest install page misplaced");
}
confPageProxyOptions->reencryptOption->setChecked(Config::self()->reencrypt());
connect(confPageProxyOptions->reencryptOption, &QCheckBox::stateChanged, this, [](int state) {
Config::self()->setReencrypt(state == Qt::Checked);
Config::self()->save();
});
auto margins = confPageProxyOptions->remoteServerLayout->contentsMargins();
margins.setLeft(margins.left() + style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing) + style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth));
confPageProxyOptions->remoteServerLayout->setContentsMargins(margins);
m_systemTrayIcon.setMainWindow(this);
m_systemTrayIcon.show();
m_backAction = new QAction(this);
connect(m_backAction, &QAction::triggered, this, [this]() {
if (Controller::certificateAlreadyGenerated() || !Config::self()->isLocalServer()) {
ui->stack->setCurrentIndex(ui->stack->currentIndex() > 1 ? ConfPageProxyOptions : ConfPageWelcome);
} else {
ui->stack->setCurrentIndex(ui->stack->currentIndex() == ConfPageProxyOptions ? ConfPageWelcome : ConfPageProxyOptions);
}
});
connect(confPageTLSCertificate->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
connect(confPageInstallAddin->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
connect(confPageProxyOptions->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
auto toolbar = new QToolBar(this);
toolbar->setMovable(false);
auto titleWidget = new KTitleWidget(this);
toolbar->addWidget(titleWidget);
addToolBar(Qt::TopToolBarArea, toolbar);
titleWidget->setText(ui->stack->currentWidget()->property("title").toString());
connect(ui->stack, &QStackedWidget::currentChanged, this, [titleWidget, this]() {
titleWidget->setText(ui->stack->currentWidget()->property("title").toString());
});
QPixmap logo = QIcon::fromTheme(u"com.gnupg.gpgolweb"_s).pixmap(64, 64);
confPageWelcome->logo->setPixmap(logo);
confPageWelcome->titleWelcome->setText(i18nc("@info", "GpgOL/Web %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
auto statusBar = new QStatusBar(this);
confPageInstallAddin->showOnStartup->setChecked(Config::self()->showLauncher());
connect(confPageInstallAddin->showOnStartup, &QCheckBox::toggled, this, [](bool checked) {
Config::self()->setShowLauncher(checked);
Config::self()->save();
});
m_status = new QLabel;
statusBar->addPermanentWidget(m_status);
auto version = new QLabel(i18nc("@info", "Version: %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
statusBar->addPermanentWidget(version);
if (Kleo::DeVSCompliance::isActive()) {
auto statusLbl = std::make_unique<QLabel>(Kleo::DeVSCompliance::name());
{
auto statusPalette = qApp->palette();
KColorScheme::adjustForeground(statusPalette,
Kleo::DeVSCompliance::isCompliant() ? KColorScheme::NormalText : KColorScheme::NegativeText,
statusLbl->foregroundRole(),
KColorScheme::View);
statusLbl->setAutoFillBackground(true);
KColorScheme::adjustBackground(statusPalette,
Kleo::DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground,
QPalette::Window,
KColorScheme::View);
statusLbl->setPalette(statusPalette);
}
statusBar->addPermanentWidget(statusLbl.release());
}
setStatusBar(statusBar);
connect(confPageProxyOptions->continueButton, &QPushButton::clicked, this, &FirstTimeDialog::slotSetup);
connect(confPageWelcome->configureButton, &QPushButton::clicked, this, [this]() {
ui->stack->setCurrentIndex(ConfPageProxyOptions);
});
connect(confPageTLSCertificate->continueButton, &QPushButton::clicked, this, &FirstTimeDialog::slotSetup);
confPageTLSCertificate->continueButton->setEnabled(false);
confPageTLSCertificate->installButton->setVisible(false);
confPageTLSCertificate->label->setVisible(false);
connect(confPageTLSCertificate->installButton, &QPushButton::clicked, this, [this]() {
if (m_controller) {
m_controller->install();
}
});
confPageInstallAddin->manifestPath->setText(QLatin1StringView(DATAROUTDIR) + u"/gpgol/manifest.xml"_s);
connect(confPageInstallAddin->testPageButton, &QPushButton::clicked, this, [this]() {
QDesktopServices::openUrl(QUrl(u"https://"_s + serverDomain() + u"/test"_s));
});
connect(confPageInstallAddin->pairWebClientButton, &QPushButton::clicked, this, [this]() {
PairingDialog d(this);
d.exec();
WebsocketClient::self().quitPairingMode();
});
connect(confPageInstallAddin->minimizeButton, &QPushButton::clicked, this, &QWidget::hide);
connect(confPageInstallAddin->manifestPathCopy, &QPushButton::clicked, this, [this]() {
QGuiApplication::clipboard()->setText(confPageInstallAddin->manifestPath->text());
});
connect(confPageInstallAddin->manifestPathOpenFolder, &QPushButton::clicked, this, [this]() {
auto job = new KIO::OpenFileManagerWindowJob();
job->setHighlightUrls({QUrl::fromUserInput(confPageInstallAddin->manifestPath->text())});
if (!qEnvironmentVariableIsEmpty("XDG_ACTIVATION_TOKEN")) {
job->setStartupId(qgetenv("XDG_ACTIVATION_TOKEN"));
}
job->start();
});
confPageProxyOptions->remoteLabel->setEnabled(!Config::self()->isLocalServer());
confPageProxyOptions->remoteServer->setEnabled(!Config::self()->isLocalServer());
confPageProxyOptions->remoteServer->setText(Config::self()->remoteAddress().toString());
confPageProxyOptions->remoteOption->setChecked(!Config::self()->isLocalServer());
connect(confPageProxyOptions->remoteOption, &QRadioButton::toggled, this, [this](bool checked) {
Config::self()->setIsLocalServer(!checked);
Config::self()->save();
confPageProxyOptions->remoteLabel->setEnabled(!Config::self()->isLocalServer());
confPageProxyOptions->remoteServer->setEnabled(!Config::self()->isLocalServer());
});
connect(confPageProxyOptions->remoteServer, &QLineEdit::textChanged, this, [this]() {
Config::self()->setRemoteAddress(QUrl::fromUserInput(confPageProxyOptions->remoteServer->text()));
Config::self()->save();
});
if (Controller::certificateAlreadyGenerated() || !Config::self()->isLocalServer()) {
ui->stack->setCurrentIndex(ConfPageWelcome);
if (Controller::certificateAlreadyGenerated() && Config::self()->isLocalServer()) {
startLocalServer();
}
startWebsocketClient();
} else {
ui->stack->setCurrentIndex(ConfPageProxyOptions);
}
connect(&m_serverProcess, &QProcess::readyReadStandardError, this, [this]() {
qWarning().noquote() << m_serverProcess.readAllStandardError();
});
connect(&m_serverProcess, &QProcess::readyReadStandardOutput, this, [this]() {
qWarning().noquote() << m_serverProcess.readAllStandardOutput();
});
connect(&m_serverProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError err) {
qWarning() << "Process error" << err;
qWarning().noquote() << m_serverProcess.readAllStandardError();
});
connect(&m_serverProcess, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) {
qWarning() << "Process finished" << exitCode << status;
if (status == QProcess::NormalExit) {
qWarning() << "Status code" << m_serverProcess.exitCode();
}
qWarning().noquote() << m_serverProcess.readAllStandardError();
});
#ifdef Q_OS_WIN
// We intentionally don't use our own config for this: If users disable autostart via
// the Windows settings menu, we want to respect that, too.
{
QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
confPageWelcome->autostartBox->setChecked(!winreg.value(QCoreApplication::applicationName()).toString().isEmpty());
}
connect(confPageWelcome->autostartBox, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state) {
QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
if (state) {
winreg.setValue(QCoreApplication::applicationName(),
QDir::toNativeSeparators(QCoreApplication::applicationFilePath()));
} else {
winreg.remove(QCoreApplication::applicationName());
}
});
#else
confPageWelcome->autostartBox->setVisible(false);
#endif
}
FirstTimeDialog::~FirstTimeDialog() = default;
void FirstTimeDialog::slotStateChanged(const QString &stateDisplay)
{
m_status->setText(stateDisplay);
m_systemTrayIcon.stateChanged(stateDisplay, WebsocketClient::self().state());
}
void FirstTimeDialog::closeEvent(QCloseEvent *e)
{
e->ignore();
hide();
}
void FirstTimeDialog::slotSetup()
{
if (confPageProxyOptions->localOption->isChecked()) {
if (!Controller::certificateAlreadyGenerated()) {
delete m_controller;
m_controller = new Controller(this);
confPageTLSCertificate->plainTextEdit->clear();
connect(m_controller, &Controller::generationDone, this, [this]() {
confPageTLSCertificate->installButton->setVisible(true);
confPageTLSCertificate->installButton->setEnabled(true);
confPageTLSCertificate->label->setText(
i18nc("@info", "About to install certificate with fingerprint: %1 ", m_controller->rootFingerprint()));
confPageTLSCertificate->label->setVisible(true);
confPageTLSCertificate->continueButton->setVisible(false);
});
ui->stack->setCurrentIndex(ConfPageTLSCertificate);
connect(m_controller, &Controller::result, this, [this](KJob *) {
if (m_controller->error()) {
confPageTLSCertificate->plainTextEdit->appendPlainText(m_controller->errorText());
return;
}
confPageTLSCertificate->installButton->setVisible(false);
confPageTLSCertificate->continueButton->setVisible(true);
confPageTLSCertificate->label->setText(i18nc("@info", "Installed certificate with fingerprint: %1", m_controller->rootFingerprint()));
confPageTLSCertificate->continueButton->setEnabled(true);
});
connect(m_controller, &Controller::debutOutput, this, &FirstTimeDialog::slotTlsDebutOutput);
m_controller->start();
} else {
startLocalServer();
startWebsocketClient();
generateManifest();
}
} else {
generateManifest();
}
}
void FirstTimeDialog::startLocalServer()
{
if (m_serverProcess.state() != QProcess::NotRunning) {
return;
}
m_serverProcess.start(u"gpgol-server"_s);
}
void FirstTimeDialog::startWebsocketClient()
{
const auto clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
auto &websocketClient = WebsocketClient::self(QUrl(u"wss://"_s + serverDomain() + u"/websocket"_s), clientId);
connect(&websocketClient, &WebsocketClient::stateChanged, this, &FirstTimeDialog::slotStateChanged);
slotStateChanged(websocketClient.stateDisplay());
}
void FirstTimeDialog::slotTlsDebutOutput(const QString &output)
{
confPageTLSCertificate->plainTextEdit->appendPlainText(output);
}
void FirstTimeDialog::generateManifest()
{
QFile file(u":/gpgol-client/manifest.xml.in"_s);
if (!file.open(QIODeviceBase::ReadOnly)) {
Q_ASSERT(false);
return;
}
ui->stack->setCurrentIndex(ManifestPage);
QByteArray manifest = file.readAll();
manifest.replace("%HOST%", serverDomain().toUtf8());
manifest.replace("%VERSION%", GPGOLWEB_VERSION_STRING);
const auto saveFilePath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/gpgol-web-manifest.xml"_s;
QSaveFile saveFile(saveFilePath);
if (!saveFile.open(QIODeviceBase::WriteOnly)) {
Q_ASSERT(false);
return;
}
saveFile.write(manifest);
saveFile.commit();
confPageInstallAddin->manifestPath->setText(QDir::toNativeSeparators(saveFilePath));
}
QString FirstTimeDialog::serverDomain() const
{
return confPageProxyOptions->localOption->isChecked() ? u"localhost:5656"_s : confPageProxyOptions->remoteServer->text();
}
#ifdef Q_OS_WIN
#include <windows.h>
#endif
void FirstTimeDialog::strongActivateWindow(QWidget* window)
{
#ifdef Q_OS_WIN
// HACK: Simulate Alt-keyPress while bringing the window to the front.
// This helps when our app does not currently have focus - and
// frequently it does not, because we have just clicked in browser/outlook.
// https://stackoverflow.com/questions/72620538/whats-the-correct-way-to-bring-a-window-to-the-front
keybd_event(VK_RMENU, 0, 0, 0);
#endif
window->show();
window->activateWindow();
window->raise();
#ifdef Q_OS_WIN
keybd_event(VK_RMENU, 0, KEYEVENTF_KEYUP, 0);
#endif
}
diff --git a/client/main.cpp b/client/main.cpp
index fa9cbdf..cd61945 100644
--- a/client/main.cpp
+++ b/client/main.cpp
@@ -1,136 +1,149 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QApplication>
#include <QCommandLineParser>
#include <QElapsedTimer>
#include <QFile>
#include <QHttpServer>
#include <QHttpServerResponse>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QPointer>
#include <QTimer>
#include <QUuid>
#include <Libkleo/FileSystemWatcher>
#include <Libkleo/GnuPG>
#include <Libkleo/KeyCache>
#include <KAboutData>
#include <KJob>
#include <KLocalizedString>
#include "../common/log.h"
#include "config.h"
+#include "connectioncontroller.h"
+#include "controller.h"
#include "firsttimedialog.h"
#include "gpgol_client_debug.h"
#include "gpgolweb_version.h"
+#include "setupdialogs.h"
+#include "statusdialog.h"
#include "utils/kuniqueservice.h"
using namespace Qt::Literals::StringLiterals;
using namespace std::chrono;
#ifdef Q_OS_WINDOWS
#include <windows.h>
#endif
#define STARTUP_TIMING qCDebug(GPGOL_CLIENT_LOG) << "Startup timing:" << startupTimer.elapsed() << "ms:"
#define STARTUP_TRACE qCDebug(GPGOL_CLIENT_LOG) << "Startup timing:" << startupTimer.elapsed() << "ms:" << SRCNAME << __func__ << __LINE__;
static void setupLogging(std::shared_ptr<Kleo::Log> log)
{
const QByteArray dirNative = qgetenv("GPGOL_CLIENT_LOGDIR");
if (dirNative.isEmpty()) {
return;
}
log->setOutputDirectory(QFile::decodeName(dirNative));
qInstallMessageHandler(Kleo::Log::messageHandler);
}
int main(int argc, char *argv[])
{
#ifdef Q_OS_WINDOWS
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
}
#endif
QApplication app(argc, argv);
app.setQuitOnLastWindowClosed(false);
// On Windows, broken heuristics appear to be in place WRT whether our app should exit
// when the last QEventLocker goes out of scope, resulting in sudden quits, after handling a
// websocket request while minimized to systray (the specific conditions appear to include that no
// other window besides the FirstTimeDialog had been visible before minimzing).
// We prefer to quit on our own terms, thank you!
app.setQuitLockEnabled(false);
KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol-js-native"));
KAboutData about(QStringLiteral("gpgol-client"),
i18nc("@title:window", "GnuPG Outlook Add-in"),
QStringLiteral(GPGOLWEB_VERSION_STRING),
i18nc("@info", "GPG Outlook add-in"),
KAboutLicense::GPL,
i18nc("@info:credit", "© 2023-2025 g10 Code GmbH"));
about.setDesktopFileName(u"com.gnupg.gpgolweb"_s);
about.setProgramLogo(QIcon::fromTheme(u"com.gnupg.gpgolweb"_s));
about.addAuthor(i18nc("@info:credit", "Carl Schwan"), i18nc("@info:credit", "Maintainer"), u"carl.schwan@gnupg.com"_s, u"https://carlschwan.eu"_s);
about.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails"));
about.setOrganizationDomain("gnupg.com");
about.setBugAddress("https://dev.gnupg.org/maniphest/task/edit/form/1/");
QCommandLineParser parser;
+ parser.addOption({u"setup"_s, u"Run first time setup assistant."_s});
KAboutData::setApplicationData(about);
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
QElapsedTimer startupTimer;
startupTimer.start();
STARTUP_TIMING << "Application created";
/* Create the unique service ASAP to prevent double starts if
* the application is started twice very quickly. */
KUniqueService service;
QObject::connect(&service, &KUniqueService::activateRequested, &service, [&service](const QStringList &arguments, const QString &workingDirectory) {
Q_UNUSED(arguments);
Q_UNUSED(workingDirectory);
service.setExitValue(0);
});
auto log = Kleo::Log::mutableInstance();
setupLogging(log);
STARTUP_TIMING << "Service created";
- QPointer<FirstTimeDialog> launcher = new FirstTimeDialog;
-
+ bool setup = parser.isSet(u"setup"_s) || (Config::self()->isLocalServer() && !Controller::certificateAlreadyGenerated());
+ if (setup) {
+ DialogController::doFirstTimeAssistant();
+ }
+ // TODO: remove me
+ //QPointer<FirstTimeDialog> launcher = new FirstTimeDialog;
+ ConnectionController connection;
+ connection.startLocalServer();
+ connection.startWebsocketClient();
if (Config::self()->showLauncher()) {
- launcher->show();
+ auto dialog = StatusDialog::getOrCreate();
+ dialog->show();
}
STARTUP_TIMING << "KeyCache creation";
auto keyCache = Kleo::KeyCache::mutableInstance();
auto fsWatcher = std::make_shared<Kleo::FileSystemWatcher>();
fsWatcher->whitelistFiles(Kleo::gnupgFileWhitelist());
fsWatcher->addPaths(Kleo::gnupgFolderWhitelist());
fsWatcher->setDelay(1000);
keyCache->addFileSystemWatcher(fsWatcher);
keyCache->startKeyListing();
return app.exec();
}
diff --git a/client/rootcagenerator/controller.cpp b/client/rootcagenerator/controller.cpp
index b134e28..5e90f5a 100644
--- a/client/rootcagenerator/controller.cpp
+++ b/client/rootcagenerator/controller.cpp
@@ -1,410 +1,404 @@
// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "controller.h"
#include "truststore.h"
#include "../utils/formatter.h"
#include <QDate>
#include <QDir>
#include <QProcess>
#include <QSaveFile>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QGpgME/ExportJob>
#include <QGpgME/ImportJob>
#include <QGpgME/KeyGenerationJob>
#include <QGpgME/KeyListJob>
#include <QGpgME/Protocol>
#include <gpgme++/context.h>
#include <Libkleo/Formatting>
#include <Libkleo/KeyParameters>
#include <Libkleo/KeyUsage>
#include <KLocalizedString>
using namespace Qt::StringLiterals;
using namespace Kleo;
using namespace GpgME;
static KeyParameters createRootCaParms()
{
KeyParameters keyParameters(KeyParameters::CMS);
keyParameters.setKeyType(GpgME::Subkey::PubkeyAlgo::AlgoRSA);
keyParameters.setKeyUsage(KeyUsage{KeyUsage::Sign | KeyUsage::Certify});
keyParameters.setDN(u"CN=GnuPG Outlook Add-in Local Root CA"_s);
keyParameters.setEmail(u"localroot@gpgoljs.local"_s);
keyParameters.setKeyLength(3072);
keyParameters.setExpirationDate(QDate(2060, 10, 10));
keyParameters.setUseRandomSerial();
keyParameters.setControlStatements({u"%no-protection"_s});
return keyParameters;
}
static KeyParameters createTlsCertParms(QLatin1StringView keyGrip)
{
KeyParameters keyParameters(KeyParameters::CMS);
keyParameters.setKeyType(GpgME::Subkey::PubkeyAlgo::AlgoRSA);
keyParameters.setKeyUsage(KeyUsage{KeyUsage::Sign | KeyUsage::Encrypt});
keyParameters.setDN(u"CN=GnuPG Outlook Add-in Local Server Certificate"_s);
keyParameters.setEmail(u"local@gpgoljs.local"_s);
keyParameters.setKeyLength(3072);
keyParameters.setExpirationDate(QDate(2060, 10, 10));
keyParameters.setIssuerDN(u"CN=GnuPG Outlook Add-in Local Root CA"_s);
keyParameters.setSigningKey(keyGrip);
keyParameters.setUseRandomSerial();
keyParameters.addDomainName(u"localhost"_s);
keyParameters.setControlStatements({u"%no-protection"_s});
return keyParameters;
}
Controller::Controller(QObject *parent)
: KJob(parent)
{
}
Controller::~Controller()
{
if (m_tmpdir) {
QProcess p;
p.start(u"gpgconf"_s, {u"--homedir"_s, m_tmpdir->path(), u"--kill"_s, u"all"_s});
p.waitForFinished();
}
}
QByteArray Controller::caCert() const
{
return m_publicCA;
}
bool Controller::certificateAlreadyGenerated()
{
auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem"));
-
return !certPath.isEmpty();
}
void Controller::setHomeDirForJob(QGpgME::Job *job)
{
auto context = QGpgME::Job::context(job);
context->setEngineHomeDirectory(m_tmpdir->path().toUtf8().constData());
}
void Controller::start()
{
- if (certificateAlreadyGenerated()) {
- emitResult();
- return;
- }
-
m_tmpdir = std::make_unique<QTemporaryDir>();
auto keyGenerationJob = QGpgME::smime()->keyGenerationJob();
setHomeDirForJob(keyGenerationJob);
connect(keyGenerationJob, &QGpgME::KeyGenerationJob::result, this, &Controller::slotRootCaCreatedSlot);
keyGenerationJob->start(createRootCaParms().toString());
}
void Controller::slotRootCaCreatedSlot(const GpgME::KeyGenerationResult &result, const QByteArray &pubKeyData, const QString &auditLog)
{
Q_UNUSED(auditLog)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.")
: i18n("Could not create key pair: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
auto importJob = QGpgME::smime()->importJob();
setHomeDirForJob(importJob);
connect(importJob, &QGpgME::ImportJob::result, this, &Controller::slotRootCaImportedSlot);
importJob->start(pubKeyData);
}
void Controller::slotRootCaImportedSlot(const GpgME::ImportResult &result, const QString &auditLogAsHtml, const GpgME::Error &auditLogError)
{
Q_UNUSED(auditLogAsHtml)
Q_UNUSED(auditLogError)
Q_EMIT debutOutput(i18nc("Debug message", "Imported root CA"));
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.")
: i18n("Could not create key pair: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
// Get the keygrip
auto keyListJob = QGpgME::smime()->keyListJob();
setHomeDirForJob(keyListJob);
connect(keyListJob, &QGpgME::KeyListJob::result, this, &Controller::slotKeyGripOptained);
keyListJob->start({u"GnuPG Outlook Add-in Local Root CA"_s}, true);
// Export public key
auto exportJob = QGpgME::smime()->publicKeyExportJob(true);
setHomeDirForJob(exportJob);
const auto imports = result.imports();
const auto fingerprint = imports[0].fingerprint();
m_fingerPrint = Formatter::formatX509Fingerprint(QByteArray(fingerprint));
Q_EMIT debutOutput(i18nc("Debug message, %1 is fingerprint", "Root CA created: %1", m_fingerPrint));
exportJob->start({QString::fromLatin1(fingerprint)});
connect(exportJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export public key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_publicCA = keyData;
checkFinished();
});
// Export private key
auto exportSecretJob = QGpgME::smime()->secretKeyExportJob(true);
setHomeDirForJob(exportSecretJob);
exportSecretJob->start({QString::fromLatin1(fingerprint)});
connect(exportSecretJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export secret key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_secretCA = keyData;
checkFinished();
});
}
void Controller::slotKeyGripOptained(const GpgME::KeyListResult &result,
const std::vector<GpgME::Key> &keys,
const QString &auditLogAsHtml,
const GpgME::Error &auditLogError)
{
Q_EMIT debutOutput(i18nc("Debug message", "Got the key grip of Root CA"));
Q_UNUSED(auditLogAsHtml)
Q_UNUSED(auditLogError)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.") : i18n("Could not get keygrip : %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
if (keys.size() != 1) {
setErrorText(i18n("More than one root certificate found"));
setError(UserDefinedError);
emitResult();
return;
}
m_ca = keys[0];
auto keyGenerationJob = QGpgME::smime()->keyGenerationJob();
setHomeDirForJob(keyGenerationJob);
connect(keyGenerationJob, &QGpgME::KeyGenerationJob::result, this, &Controller::slotCertCreatedSlot);
keyGenerationJob->start(createTlsCertParms(QLatin1StringView(keys[0].subkey(0).keyGrip())).toString());
}
void Controller::slotCertCreatedSlot(const GpgME::KeyGenerationResult &result, const QByteArray &pubKeyData, const QString &auditLog)
{
Q_EMIT debutOutput(i18nc("Debug message", "TLS certificate created"));
Q_UNUSED(auditLog)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.")
: i18n("Could not create key pair for cert: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
auto importJob = QGpgME::smime()->importJob();
setHomeDirForJob(importJob);
connect(importJob, &QGpgME::ImportJob::result, this, &Controller::slotCertImportedSlot);
importJob->start(pubKeyData);
}
void Controller::slotCertImportedSlot(const GpgME::ImportResult &result, const QString &auditLogAsHtml, const GpgME::Error &auditLogError)
{
Q_UNUSED(auditLogAsHtml)
Q_UNUSED(auditLogError)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.") : i18n("Could not import cert: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
auto keyListJob = QGpgME::smime()->keyListJob();
setHomeDirForJob(keyListJob);
connect(keyListJob,
&QGpgME::KeyListJob::result,
this,
[this](const GpgME::KeyListResult &result, const std::vector<GpgME::Key> &keys, const QString &auditLogAsHtml, const GpgME::Error &auditLogError) {
Q_UNUSED(result);
Q_UNUSED(auditLogAsHtml);
Q_UNUSED(auditLogError);
m_tls = keys[0];
checkFinished();
});
keyListJob->start({u"GnuPG Outlook Add-in Local Root CA"_s}, true);
// Export public key
auto exportJob = QGpgME::smime()->publicKeyExportJob(true);
setHomeDirForJob(exportJob);
const auto imports = result.imports();
const auto fingerprint = imports[0].fingerprint();
exportJob->start({QString::fromLatin1(fingerprint)});
connect(exportJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export public key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_publicTLS = keyData;
checkFinished();
});
// Export private key
auto exportSecretJob = QGpgME::smime()->secretKeyExportJob(true);
setHomeDirForJob(exportSecretJob);
exportSecretJob->start({QString::fromLatin1(fingerprint)});
connect(exportSecretJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export secret key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_secretTLS = keyData;
checkFinished();
});
}
void Controller::checkFinished()
{
if (!m_secretCA.isEmpty() && !m_publicCA.isEmpty() && !m_publicTLS.isEmpty() && !m_secretTLS.isEmpty() && !m_ca.isNull() && !m_tls.isNull()) {
Q_EMIT generationDone();
}
}
void Controller::install()
{
auto keyPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate-key.pem"));
// Install for gpgol-client
{
auto certPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
Q_EMIT debutOutput(i18nc("Debug message, %1 is a path", "Installing certificate for gpgol-client in %1", certPath));
QDir dir;
if (!dir.mkpath(certPath)) {
Q_EMIT debutOutput(i18nc("Debug message, %1 is a path", "Unable to create the following path: ", certPath));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile localhostPub(certPath + u"/certificate.pem"_s);
if (localhostPub.open(QIODeviceBase::WriteOnly)) {
localhostPub.write(m_publicTLS);
localhostPub.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message. %1 is a filename. %2 is a path", "No permission to write: %1 in %2", localhostPub.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile rootCaPub(certPath + u"/root-ca.pem"_s);
if (rootCaPub.open(QIODeviceBase::WriteOnly)) {
rootCaPub.write(m_publicCA);
rootCaPub.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message. %1 is a filename. %2 is a path", "No permission to write: %1 in %2", rootCaPub.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
}
// Install for gpgol-server
{
auto certPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation).chopped(QStringLiteral(u"gpgol-client").length()).append(u"gpgol-server");
Q_EMIT debutOutput(i18nc("Debug message. %1 is a path", "Installing certificate for gpgol-server in %1", certPath));
QDir dir;
if (!dir.mkpath(certPath)) {
Q_EMIT debutOutput(i18nc("Debug message. %1 is a path", "Unable to create the following path: %1", certPath));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile localhostPub(certPath + u"/certificate.pem"_s);
if (localhostPub.open(QIODeviceBase::WriteOnly)) {
localhostPub.write(m_publicTLS);
localhostPub.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message, %1 is a filename. %2 is a path", "No permission to write: %1 in %2", localhostPub.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile localhostKey(certPath + u"/certificate-key.pem"_s);
if (localhostKey.open(QIODeviceBase::WriteOnly)) {
localhostKey.write(m_secretTLS);
localhostKey.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message. %1 is a filename. %2 is a path.", "No permission to write: %1 in %2", localhostKey.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile rootCaPub(certPath + u"/root-ca.pem"_s);
if (rootCaPub.open(QIODeviceBase::WriteOnly)) {
rootCaPub.write(m_publicCA);
rootCaPub.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message. %1 is a filename. %2 is a path", "No permission to write: %1 in %2", rootCaPub.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
}
auto trustStore = TrustStore();
if (!trustStore.install(*this)) {
Q_EMIT debutOutput(i18nc("Debug message", "Installing certificate to browser failed"));
}
emitResult();
}
QString Controller::rootFingerprint() const
{
return m_fingerPrint;
}
diff --git a/client/setupdialogs.cpp b/client/setupdialogs.cpp
new file mode 100644
index 0000000..c16ee58
--- /dev/null
+++ b/client/setupdialogs.cpp
@@ -0,0 +1,384 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "setupdialogs.h"
+
+#include "config.h"
+#include "connectioncontroller.h"
+#include "gpgolweb_version.h"
+#include "rootcagenerator/controller.h"
+#include "websocketclient.h"
+
+#include <KAssistantDialog>
+#include <KLocalizedString>
+#include <KTitleWidget>
+
+#include <QApplication>
+#include <QButtonGroup>
+#include <QClipboard>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QDesktopServices>
+#include <QFile>
+#include <QGroupBox>
+#include <QLabel>
+#include <QPushButton>
+#include <QRadioButton>
+#include <QSaveFile>
+#include <QToolTip>
+#include <QVBoxLayout>
+
+#include "ui_confpageproxyoptions.h"
+#include "ui_confpagetlscertificate.h"
+
+using namespace Qt::StringLiterals;
+
+PairingDialog::PairingDialog(QWidget *parent)
+ : QDialog(parent)
+{
+ setWindowTitle(i18n("Pairing mode active"));
+ auto l = new QVBoxLayout(this);
+ auto lab = new QLabel(i18n("<p>Copy and paste the code shown below into the input field shown at the top of the Add-In:</p>"));
+ lab->setWordWrap(true);
+ l->addWidget(lab);
+ m_pairingTokenLabel = new QLabel(i18nc("status message", "<p align='center'><b>Obtaining token...</b></p>"));
+ l->addWidget(m_pairingTokenLabel);
+ auto bb = new QDialogButtonBox;
+ auto endButton = bb->addButton(i18nc("@button", "End pairing mode"), QDialogButtonBox::RejectRole);
+ connect(endButton, &QAbstractButton::clicked, this, &QDialog::reject);
+ m_copyButton = bb->addButton(i18nc("@button", "Copy code to clipboard"), QDialogButtonBox::ActionRole);
+ m_copyButton->setEnabled(false);
+ l->addWidget(bb);
+ setFixedSize(sizeHint());
+
+ connect(&WebsocketClient::self(), &WebsocketClient::pairingStatusChanged, this, &PairingDialog::pairingStatusChanged);
+ WebsocketClient::self().enterPairingMode();
+}
+
+void PairingDialog::pairingStatusChanged(const QString& token, bool pairingActive) {
+ if (!pairingActive) {
+ reject();
+ return;
+ }
+ m_pairingTokenLabel->setText(QString(u"<p align='center'><b>%1</b></p>").arg(token));
+ m_copyButton->setEnabled(true);
+ connect(m_copyButton, &QAbstractButton::clicked, [this, token]() {
+ qApp->clipboard()->setText(token);
+ });
+}
+
+
+class GenerateCertificateWidget : public QWidget {
+ Q_OBJECT
+public:
+ GenerateCertificateWidget(bool assistant, QWidget *parent)
+ : QWidget(parent)
+ , m_controller(nullptr)
+ , m_installed(false)
+ , m_dialog(nullptr)
+ {
+ auto hbox = new QHBoxLayout(this);
+ hbox->setContentsMargins(0, 0, 0, 0);
+ m_label = new QLabel();
+ hbox->addWidget(m_label);
+ m_generateButton = new QPushButton();
+ m_generateButton->setVisible(!assistant);
+ if (!assistant) {
+ connect(m_generateButton, &QPushButton::clicked, this, [this]() {
+ KAssistantDialog dialog;
+ addAssistantPages(&dialog);
+ startGenerate();
+ dialog.exec();
+ });
+ }
+ hbox->addWidget(m_generateButton);
+
+ updateStatus();
+ }
+
+ void addAssistantPages(KAssistantDialog *dialog) {
+ m_dialog = dialog;
+
+ auto genPage = new QWidget();
+ auto vbox = new QVBoxLayout(genPage);
+ m_genProgress = new QPlainTextEdit();
+ vbox->addWidget(m_genProgress);
+ m_genDoneLabel = new QLabel();
+ vbox->addWidget(m_genDoneLabel);
+ m_genPageItem = new KPageWidgetItem(genPage);
+ m_genPageItem->setHeader(i18n("Generating TLS certificate"));
+ m_dialog->addPage(m_genPageItem);
+
+ auto installPage = new QWidget();
+ vbox = new QVBoxLayout(installPage);
+ m_installProgress = new QPlainTextEdit();
+ vbox->addWidget(m_installProgress);
+ m_installDoneLabel = new QLabel();
+ vbox->addWidget(m_installDoneLabel);
+ m_installPageItem = new KPageWidgetItem(installPage);
+ m_installPageItem->setHeader(i18n("Installing certificate"));
+ m_dialog->addPage(m_installPageItem);
+
+ connect(dialog, &KPageDialog::currentPageChanged, this,
+ [this](KPageWidgetItem *current, KPageWidgetItem *) {
+ if (current == m_genPageItem) {
+ startGenerate();
+ } else if (current == m_installPageItem) {
+ if (!m_installed) {
+ m_controller->install();
+ }
+ }
+ });
+ }
+
+ void updateStatus() {
+ if (Config::self()->isLocalServer()) {
+ setVisible(true);
+ if (Controller::certificateAlreadyGenerated()) {
+ m_label->setText(i18n("A TLS certificate has already been generated."));
+ m_generateButton->setText(i18n("Re-generate certificate"));
+ m_generateButton->setFixedSize(m_generateButton->minimumSizeHint());
+ //m_generateButton->setToolTip(i18n("Re-generates and installs a TLS certificate for a secure connection to the local proxy server. It is not usually necessary to repeat this step."));
+ } else {
+ m_label->setText(i18n("A TLS certificate is needed for the secure connection to the proxy."));
+ m_generateButton->setText(i18n("Generate and install certificate"));
+ if (m_dialog) {
+ m_label->setText(i18n("A TLS certificate is needed for the secure connection to the proxy. This will be generated in the next step."));
+ }
+ }
+ } else {
+ setVisible(false);
+ }
+
+ if (m_dialog) {
+ m_dialog->setAppropriate(m_genPageItem, Config::self()->isLocalServer() && !Controller::certificateAlreadyGenerated());
+ m_dialog->setAppropriate(m_installPageItem, Config::self()->isLocalServer() && !Controller::certificateAlreadyGenerated());
+ }
+ }
+private:
+ QLabel *m_label;
+ QPushButton *m_generateButton;
+ QPlainTextEdit *m_genProgress, *m_installProgress;
+ QLabel *m_genDoneLabel, *m_installDoneLabel;
+ KPageWidgetItem *m_genPageItem, *m_installPageItem;
+ KAssistantDialog *m_dialog;
+ QPointer<Controller> m_controller; // it's a KJob and will auto-delete
+ bool m_installed;
+
+ void startGenerate() {
+ if (m_controller || m_installed) {
+ return;
+ }
+
+ m_dialog->setValid(m_genPageItem, false);
+ m_dialog->setValid(m_installPageItem, false);
+ m_controller = new Controller(this);
+ m_dialog->setValid(m_genPageItem, false);
+ connect(m_controller, &Controller::generationDone, this, [this]() {
+ m_genDoneLabel->setText(i18nc("@info", "Successfully generated certificate with fingerprint %1", m_controller->rootFingerprint()));
+ m_dialog->setValid(m_genPageItem, true);
+ });
+ connect(m_controller, &Controller::debutOutput, this, [this](const QString &output) {
+ m_genProgress->appendPlainText(output);
+ m_installProgress->appendPlainText(output);
+ });
+ connect(m_controller, &Controller::result, this, [this](KJob *) {
+ if (m_controller->error()) {
+ m_genProgress->appendPlainText(m_controller->errorText());
+ m_installProgress->appendPlainText(m_controller->errorText());
+ return;
+ }
+ m_label->setText(i18nc("@info", "Installed certificate with fingerprint: %1", m_controller->rootFingerprint()));
+ m_dialog->setValid(m_installPageItem, true);
+ m_installed = true;
+ });
+ m_controller->start();
+ }
+};
+
+void DialogController::doDialog(const QList<PageID> &pageIds, const bool assistant) {
+ KPageDialog *dialog = assistant ? new KAssistantDialog() : new KPageDialog();
+
+ if (pageIds.contains(PageProxy)) {
+ auto widget = new QWidget();
+ auto vbox = new QVBoxLayout(widget);
+ auto item = new KPageWidgetItem(widget, i18n("Proxy"));
+ item->setHeader(i18nc("@title", "Configure Proxy"));
+ dialog->addPage(item);
+
+ auto label = new QLabel(i18n("Choose your configuration for the proxy component:"));
+ vbox->addWidget(label);
+ label = new QLabel(i18n("Note: Changes to this setting only take effect after uploading the adjusted manifest file to Outlook on the next page!"));
+ auto font = label->font();
+ font.setItalic(true);
+ label->setFont(font);
+ label->setWordWrap(true);
+ vbox->addWidget(label);
+
+ auto certcontrol = new GenerateCertificateWidget(assistant, nullptr);
+ if (assistant) {
+ certcontrol->addAssistantPages(static_cast<KAssistantDialog*>(dialog));
+ }
+
+ auto grp = new QButtonGroup(widget);
+ auto localOption = new QRadioButton(i18n("Run a local proxy on this machine"));
+ localOption->setChecked(Config::self()->isLocalServer());
+ auto remoteOption = new QRadioButton(i18n("Use proxy from a remote server (EXPERIMENTAL)"));
+ remoteOption->setChecked(!Config::self()->isLocalServer());
+ grp->addButton(localOption);
+ vbox->addWidget(localOption);
+ grp->addButton(remoteOption);
+ vbox->addWidget(remoteOption);
+
+ auto hbox = new QHBoxLayout();
+ auto remoteLabel = new QLabel(i18n("Remote proxy server"));
+ hbox->addWidget(remoteLabel);
+ auto remoteServer = new QLineEdit();
+ remoteServer->setPlaceholderText(u"internal.company.com"_s);
+ remoteServer->setClearButtonEnabled(true);
+ hbox->addWidget(remoteServer);
+ vbox->addLayout(hbox);
+
+ QObject::connect(remoteOption, &QRadioButton::toggled, dialog, [remoteLabel, remoteServer, certcontrol](bool checked) {
+ Config::self()->setIsLocalServer(!checked);
+ Config::self()->save();
+
+ remoteLabel->setEnabled(!Config::self()->isLocalServer());
+ remoteServer->setEnabled(!Config::self()->isLocalServer());
+ certcontrol->updateStatus();
+ });
+ QObject::connect(remoteServer, &QLineEdit::textChanged, dialog, [remoteServer]() {
+ Config::self()->setRemoteAddress(QUrl::fromUserInput(remoteServer->text()));
+ Config::self()->save();
+ });
+ vbox->addWidget(certcontrol);
+ vbox->addStretch();
+ }
+
+ if (pageIds.contains(PageInstallAddin)) {
+ auto widget = new QWidget();
+ auto vbox = new QVBoxLayout(widget);
+ auto item = new KPageWidgetItem(widget);
+ item->setHeader(i18nc("@title", "Install Outlook Add-In"));
+ dialog->addPage(item);
+
+ auto label = new QLabel(i18n("<p>Before the first use, the add-in has to be activated in Outlook:</p>"
+ "<ol><li>Go to the <a href=\"%1\">Outlook Extension Manager</a> (you may be prompted to log in).<li>"
+ "<li><a href=\"%2\">Click here</a> to copy the manifest file name to your clipboard.</li>"
+ "<li>In Outlook, register this via <tt>My Add-Ins -> Custom Add-Ins -> Add a custom Add-In</tt></li>"
+ "<li>Click on any E-Mail, and activate the add-in by clicking on the GnuPG icon shown about the email header.</li>"
+ "<li>When prompted for a pairing code, <a href=\"%3\">click here to enter pairing mode</a>.</li></ol></p>",
+ u"https://outlook.office.com/mail/jsmvvmdeeplink/?path=/options/manageapps&amp;bO=4"_s,
+ u"copy"_s, u"pairing"_s));
+ label->setWordWrap(true);
+ QObject::connect(label, &QLabel::linkActivated, dialog, [label, dialog](const QString &url) {
+ if (url == u"copy"_s) {
+ QFile file(u":/gpgol-client/manifest.xml.in"_s);
+ if (!file.open(QIODeviceBase::ReadOnly)) {
+ Q_ASSERT(false);
+ return;
+ }
+ QByteArray manifest = file.readAll();
+ manifest.replace("%HOST%", ConnectionController::serverDomain().toUtf8());
+ manifest.replace("%VERSION%", GPGOLWEB_VERSION_STRING);
+
+ const auto saveFilePath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/gpgol-web-manifest.xml"_s;
+
+ QSaveFile saveFile(saveFilePath);
+ if (!saveFile.open(QIODeviceBase::WriteOnly)) {
+ Q_ASSERT(false);
+ return;
+ }
+ saveFile.write(manifest);
+ saveFile.commit();
+
+ QGuiApplication::clipboard()->setText(saveFilePath);
+ QToolTip::showText(label->mapToGlobal(QPoint(10, 10)), i18n("Copied to clipboard."), label, QRect(), 2000);
+ } else if (url == u"pairing"_s) {
+ PairingDialog d(dialog);
+ d.exec();
+ WebsocketClient::self().quitPairingMode();
+ } else {
+ QDesktopServices::openUrl(QUrl(url));
+ }
+ });
+ vbox->addWidget(label);
+
+ auto title = new KTitleWidget();
+ title->setText(i18n("Troubleshooting"));
+ vbox->addWidget(title);
+
+ label = new QLabel(i18n("<p>If the extension is not connected:</p><ul>"
+ "<li>Test for problems with the TLS-certificate installation, by opening this <a href=\"%1\">test page</a>.</li>"
+ "<li>Sometimes the add-in icon is not immediately visible in the task-bar. Make sure to click on an existing message in Outlook. "
+ "You may also have to click on the \"Apps\" icon. From there, you should be allowed to \"pin\" the icon for easier access.</li>"
+ "<li>If you have just added the manifest, it may be necessary to reload.</li>"
+ "<li>If your account is organization managed, your administrator may have to allow usage of the GPGOL/Web add-in, manually.</li>"
+ "</ul></p>", u"https://"_s + ConnectionController::serverDomain() + u"/test"_s));
+ label->setWordWrap(true);
+ label->setOpenExternalLinks(true);
+ vbox->addWidget(label);
+ }
+
+ if (pageIds.contains(PageSettings)) {
+ auto widget = new QWidget();
+ auto vbox = new QVBoxLayout(widget);
+ auto featuresbox = new QGroupBox(i18n("Optional features"));
+ auto boxlayout = new QVBoxLayout(featuresbox);
+ auto reencrypt = new QCheckBox(i18n("Reencrypt email folders with new keys"), featuresbox);
+ reencrypt->setChecked(Config::self()->reencrypt());
+ QObject::connect(reencrypt, &QCheckBox::checkStateChanged, dialog, [](Qt::CheckState state) {
+ Config::self()->setReencrypt(state == Qt::Checked);
+ Config::self()->save();
+ });
+ boxlayout->addWidget(reencrypt);
+ vbox->addWidget(featuresbox);
+
+ auto startupbox = new QGroupBox(i18n("Startup behavior"));
+ boxlayout = new QVBoxLayout(startupbox);
+ #ifdef Q_OS_WIN
+ auto autoStartBox = new QCheckBox(i18n("Start GPGOL/Web automatically"));
+ // We intentionally don't use our own config for this: If users disable autostart via
+ // the Windows settings menu, we want to respect that, too.
+ {
+ QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
+ autostartBox->setChecked(!winreg.value(QCoreApplication::applicationName()).toString().isEmpty());
+ }
+ connect(autostartBox, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state) {
+ QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
+ if (state) {
+ winreg.setValue(QCoreApplication::applicationName(),
+ QDir::toNativeSeparators(QCoreApplication::applicationFilePath()));
+ } else {
+ winreg.remove(QCoreApplication::applicationName());
+ }
+ });
+ boxlayout->addWidget(autoStartBox);
+#endif
+
+ auto showOnStartup = new QCheckBox(i18n("Show status dialog when starting"));
+ showOnStartup->setChecked(Config::self()->showLauncher());
+ QObject::connect(showOnStartup, &QCheckBox::checkStateChanged, dialog, [](Qt::CheckState state) {
+ Config::self()->setShowLauncher(state == Qt::Checked);
+ Config::self()->save();
+ });
+ boxlayout->addWidget(showOnStartup);
+ vbox->addWidget(startupbox);
+
+ vbox->addStretch();
+ auto item = new KPageWidgetItem(widget);
+ item->setHeader(i18nc("@title", "Options"));
+ dialog->addPage(item);
+ }
+
+ dialog->exec();
+}
+
+void DialogController::doFirstTimeAssistant()
+{
+ doDialog(QList{PageProxy, PageInstallAddin, PageSettings}, true);
+}
+
+#include "setupdialogs.moc"
diff --git a/client/setupdialogs.h b/client/setupdialogs.h
new file mode 100644
index 0000000..50472fa
--- /dev/null
+++ b/client/setupdialogs.h
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QDialog>
+
+class QLabel;
+class QAbstractButton;
+
+class DialogController {
+public:
+ enum PageID {
+ PageProxy,
+ PageInstallAddin,
+ PageSettings,
+ };
+ static void doDialog(const QList<PageID> &pageIds, const bool assistant=false);
+ static void doFirstTimeAssistant();
+ static void checkDoFirstTimeDialog();
+ static void showStatusDialog();
+};
+
+class PairingDialog : public QDialog {
+public:
+ PairingDialog(QWidget *parent);
+ void pairingStatusChanged(const QString& token, bool pairingActive);
+private:
+ QLabel* m_pairingTokenLabel;
+ QAbstractButton* m_copyButton;
+};
diff --git a/client/statusdialog.cpp b/client/statusdialog.cpp
new file mode 100644
index 0000000..42bce84
--- /dev/null
+++ b/client/statusdialog.cpp
@@ -0,0 +1,147 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "statusdialog.h"
+
+#include "config.h"
+#include "gpgolweb_version.h"
+#include "setupdialogs.h"
+#include "websocketclient.h"
+
+#include <KColorScheme>
+#include <KLocalizedString>
+#include <KTitleWidget>
+
+#include <Libkleo/Compliance>
+
+#include <QApplication>
+#include <QFrame>
+#include <QGroupBox>
+#include <QIcon>
+#include <QLabel>
+#include <QPushButton>
+#include <QStatusBar>
+#include <QVBoxLayout>
+
+using namespace Qt::StringLiterals;
+
+QPointer<StatusDialog> StatusDialog::instance;
+
+StatusDialog *StatusDialog::getOrCreate(QWidget *parent)
+{
+ if (!instance) {
+ instance = new StatusDialog(parent);
+ }
+ return instance;
+}
+
+StatusDialog::StatusDialog(QWidget *parent)
+ : QMainWindow(parent)
+{
+ auto central = new QWidget();
+ setCentralWidget(central);
+
+ auto vbox = new QVBoxLayout(central);
+ auto appicon = new QLabel();
+ appicon->setPixmap(QIcon::fromTheme(u"com.gnupg.gpgolweb"_s).pixmap(64, 64));
+ vbox->addWidget(appicon);
+ vbox->setAlignment(appicon, Qt::AlignCenter);
+ auto title = new KTitleWidget();
+ title->setText(i18nc("@info", "GpgOL/Web %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
+ vbox->addWidget(title);
+ vbox->setAlignment(title, Qt::AlignCenter);
+ auto label = new QLabel(i18n("The GnuPG Add-in for Outlook"));
+ vbox->addWidget(label);
+ vbox->setAlignment(label, Qt::AlignCenter);
+
+ auto statusGroup = new QGroupBox(i18n("Connection status:"));
+ auto grid = new QGridLayout(statusGroup);
+ m_proxyProcessLabel = new QLabel();
+ grid->addWidget(m_proxyProcessLabel, 0, 0);
+ m_proxyConnectionLabel = new QLabel();
+ grid->addWidget(m_proxyConnectionLabel, 1, 0);
+ auto connectionButton = new QPushButton(i18n("Setup connection"));
+ grid->addWidget(connectionButton, 1, 1);
+ m_clientConnectionLabel = new QLabel();
+ grid->addWidget(m_clientConnectionLabel, 2, 0);
+ auto extensionButton = new QPushButton(i18n("Setup extension"));
+ grid->addWidget(extensionButton, 2, 1);
+
+ vbox->addStretch();
+ vbox->addWidget(statusGroup);
+ vbox->addStretch();
+
+ auto hbox = new QHBoxLayout();
+ auto settingsButton = new QPushButton(i18n("General Settings"));
+ hbox->addWidget(settingsButton);
+ hbox->addStretch();
+ auto minimizeButton = new QPushButton(i18n("Minimize to tray"));
+ hbox->addWidget(minimizeButton);
+ vbox->addLayout(hbox);
+
+ auto statusBar = new QStatusBar(this);
+ m_status = new QLabel;
+ statusBar->addPermanentWidget(m_status);
+
+ auto version = new QLabel(i18nc("@info", "Version: %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
+ statusBar->addPermanentWidget(version);
+ if (Kleo::DeVSCompliance::isActive()) {
+ auto statusLbl = std::make_unique<QLabel>(Kleo::DeVSCompliance::name());
+ {
+ auto statusPalette = qApp->palette();
+ KColorScheme::adjustForeground(statusPalette,
+ Kleo::DeVSCompliance::isCompliant() ? KColorScheme::NormalText : KColorScheme::NegativeText,
+ statusLbl->foregroundRole(),
+ KColorScheme::View);
+ statusLbl->setAutoFillBackground(true);
+ KColorScheme::adjustBackground(statusPalette,
+ Kleo::DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground,
+ QPalette::Window,
+ KColorScheme::View);
+ statusLbl->setPalette(statusPalette);
+ }
+ statusBar->addPermanentWidget(statusLbl.release());
+ }
+ setStatusBar(statusBar);
+
+ vbox->addLayout(grid);
+
+ connect(&WebsocketClient::self(), &WebsocketClient::stateChanged, this, &StatusDialog::stateChanged);
+ stateChanged(i18n("Initializing..."));
+
+ connect(connectionButton, &QPushButton::clicked, this, [this]() {
+ DialogController::doDialog(QList{ DialogController::PageProxy });
+ });
+ connect(extensionButton, &QPushButton::clicked, this, [this]() {
+ DialogController::doDialog(QList{ DialogController::PageInstallAddin });
+ });
+ connect(settingsButton, &QPushButton::clicked, this, [this]() {
+ DialogController::doDialog(QList{ DialogController::PageSettings });
+ });
+}
+
+#define ICON_OK u"<span style=\"color:green\">&#10004;</span> "_s
+#define ICON_WARN u"<span style=\"color:orange\">&#9888;</span> "_s
+#define ICON_ERROR u"<span style=\"color:red\">&#10060;</span> "_s
+void StatusDialog::stateChanged(const QString &)
+{
+ if (Config::self()->isLocalServer()) {
+ /* if (m_serverProcess.status() == QProcess::Running) {
+ m_proxyProcessLabel->setText(ICON_OK + i18n("Proxy server process is running"));
+ } else {
+ m_proxyProcessLabel->setText(ICON_ERROR + i18n("Proxy server process is not running"));
+ } */
+ m_proxyProcessLabel->setText(ICON_WARN + i18n("TODO Add check, whether proxy process is running"));
+ }
+
+ auto state = WebsocketClient::self().state();
+ // TODO
+ m_proxyConnectionLabel->setText(ICON_WARN + i18n("TODO Merge check for proxy connection status"));
+ if (state == WebsocketClient::Connected) {
+ m_clientConnectionLabel->setText(ICON_OK + i18n("Web extension is connected, and opened"));
+ } else {
+ m_clientConnectionLabel->setText(ICON_WARN + i18n("Web extension is not connected, or not opened"));
+ }
+}
diff --git a/client/statusdialog.h b/client/statusdialog.h
new file mode 100644
index 0000000..61603ec
--- /dev/null
+++ b/client/statusdialog.h
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QMainWindow>
+
+class QLabel;
+
+class StatusDialog : public QMainWindow {
+public:
+ static StatusDialog *getOrCreate(QWidget *parent = nullptr);
+private:
+ StatusDialog(QWidget *parent = nullptr);
+ void stateChanged(const QString &);
+ QLabel* m_proxyProcessLabel;
+ QLabel* m_proxyConnectionLabel;
+ QLabel* m_clientConnectionLabel;
+ QLabel* m_status;
+ static QPointer<StatusDialog> instance;
+};

File Metadata

Mime Type
text/x-diff
Expires
Thu, Feb 26, 6:56 PM (1 d, 19 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d8/57/ab7a1c660bc6f4a68d6ddcdf76c9

Event Timeline