File indexing completed on 2024-06-23 05:14:06

0001 /*  dialogs/updatenotification.cpp
0002 
0003     This file is part of Kleopatra, the KDE keymanager
0004     SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik
0005     SPDX-FileContributor: Intevation GmbH
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 #include "updatenotification.h"
0010 
0011 #include <view/htmllabel.h>
0012 
0013 #include <Libkleo/Compat>
0014 #include <Libkleo/Formatting>
0015 #include <Libkleo/GnuPG>
0016 
0017 #include "kleopatra_debug.h"
0018 
0019 #include <QCheckBox>
0020 #include <QDateTime>
0021 #include <QDesktopServices>
0022 #include <QDialogButtonBox>
0023 #include <QGridLayout>
0024 #include <QIcon>
0025 #include <QLabel>
0026 #include <QProcess>
0027 #include <QProgressDialog>
0028 #include <QPushButton>
0029 #include <QRegularExpression>
0030 #include <QUrl>
0031 
0032 #include <KAboutData>
0033 #include <KConfigGroup>
0034 #include <KIconLoader>
0035 #include <KLocalizedString>
0036 #include <KMessageBox>
0037 #include <KSharedConfig>
0038 
0039 #include <QGpgME/CryptoConfig>
0040 #include <QGpgME/Protocol>
0041 
0042 #include <gpgme++/error.h>
0043 #include <gpgme++/gpgmefw.h>
0044 #include <gpgme++/swdbresult.h>
0045 
0046 using namespace Kleo;
0047 
0048 namespace
0049 {
0050 static void gpgconf_set_update_check(bool value)
0051 {
0052     auto conf = QGpgME::cryptoConfig();
0053     auto entry = getCryptoConfigEntry(conf, "dirmngr", "allow-version-check");
0054     if (!entry) {
0055         qCDebug(KLEOPATRA_LOG) << "allow-version-check entry not found";
0056         return;
0057     }
0058     if (entry->boolValue() != value) {
0059         entry->setBoolValue(value);
0060         conf->sync(true);
0061     }
0062 }
0063 } // namespace
0064 
0065 void UpdateNotification::forceUpdateCheck(QWidget *parent)
0066 {
0067     auto proc = new QProcess;
0068 
0069     proc->setProgram(gnupgInstallPath() + QStringLiteral("/gpg-connect-agent.exe"));
0070     proc->setArguments({
0071         QStringLiteral("--dirmngr"),
0072         QStringLiteral("loadswdb --force"),
0073         QStringLiteral("/bye"),
0074     });
0075 
0076     auto progress = new QProgressDialog(i18n("Searching for updates..."), i18n("Cancel"), 0, 0, parent);
0077     progress->setMinimumDuration(0);
0078     progress->show();
0079 
0080     connect(progress, &QProgressDialog::canceled, proc, [proc]() {
0081         proc->kill();
0082         qCDebug(KLEOPATRA_LOG) << "Update force canceled. Output:" << QString::fromLocal8Bit(proc->readAllStandardOutput())
0083                                << "stderr:" << QString::fromLocal8Bit(proc->readAllStandardError());
0084     });
0085 
0086     connect(proc, &QProcess::finished, progress, [parent, progress, proc](int exitCode, QProcess::ExitStatus exitStatus) {
0087         qCDebug(KLEOPATRA_LOG) << "Update force exited with status:" << exitStatus << "code:" << exitCode;
0088         delete progress;
0089         proc->deleteLater();
0090         UpdateNotification::checkUpdate(parent, exitStatus == QProcess::NormalExit);
0091     });
0092 
0093     qCDebug(KLEOPATRA_LOG) << "Starting:" << proc->program() << "args" << proc->arguments();
0094 
0095     proc->start();
0096 }
0097 #ifdef Q_OS_WIN
0098 /* Extract the actual version number (conforming to the semanticversioning spec)
0099  * from the Version strings which might be used for Gpg4win / GnuPG VS-Desktop
0100  * which are optionally prefixed with some text followed by a dash
0101  * e.g. "Gpg4win-3.1.15-beta15"; see https://dev.gnupg.org/T5663 */
0102 static const QByteArray extractVersionNumber(const QString &versionString)
0103 {
0104     static const QRegularExpression catchSemVerRegExp{QLatin1StringView{R"(-([0-9]+(?:\.[0-9]+)*(?:-[.0-9A-Za-z-]+)?(?:\+[.0-9a-zA-Z-]+)?)$)"}};
0105 
0106     const auto match = catchSemVerRegExp.match(versionString);
0107     const auto current = match.hasMatch() ? match.captured(1) : versionString;
0108     return current.toUtf8();
0109 }
0110 #endif
0111 
0112 void UpdateNotification::checkUpdate(QWidget *parent, bool force)
0113 {
0114 #ifdef Q_OS_WIN
0115     KConfigGroup updatecfg(KSharedConfig::openConfig(), QStringLiteral("UpdateNotification"));
0116 
0117     if (updatecfg.readEntry("NeverShow", false) && !force) {
0118         return;
0119     }
0120 
0121     // Gpg defaults to no update check. For Gpg4win we want this
0122     // enabled if the user does not explicitly disable update
0123     // checks neverShow would be true in that case or
0124     // we would have set AllowVersionCheck once and the user
0125     // explicitly removed that.
0126     if (force || updatecfg.readEntry("AllowVersionCheckSetOnce", false)) {
0127         gpgconf_set_update_check(true);
0128         updatecfg.writeEntry("AllowVersionCheckSetOnce", true);
0129     }
0130 
0131     GpgME::Error err;
0132     const auto lastshown = updatecfg.readEntry("LastShown", QDateTime());
0133 
0134     if (!force && lastshown.isValid() && lastshown.addSecs(20 * 60 * 60) > QDateTime::currentDateTime()) {
0135         qDebug() << QDateTime::currentDateTime().addSecs(20 * 60 * 60);
0136         return;
0137     }
0138 
0139     const auto current = extractVersionNumber(KAboutData::applicationData().version());
0140 
0141     const auto results = GpgME::SwdbResult::query("gpg4win", current.constData(), &err);
0142     if (err) {
0143         qCDebug(KLEOPATRA_LOG) << "update check failed: " << Formatting::errorAsString(err);
0144         return;
0145     }
0146 
0147     if (results.size() != 1) {
0148         /* Should not happen */
0149         qCDebug(KLEOPATRA_LOG) << "more then one result";
0150         return;
0151     }
0152 
0153     const auto result = results[0];
0154 
0155     if (result.update()) {
0156         const QString newVersion = QStringLiteral("%1.%2.%3").arg(result.version().major).arg(result.version().minor).arg(result.version().patch);
0157         qCDebug(KLEOPATRA_LOG) << "Have update to version:" << newVersion;
0158         UpdateNotification notifier(parent, newVersion);
0159         notifier.exec();
0160         updatecfg.writeEntry("LastShown", QDateTime::currentDateTime());
0161         updatecfg.sync();
0162     } else {
0163         qCDebug(KLEOPATRA_LOG) << "No update for:" << current;
0164         if (force) {
0165             KMessageBox::information(parent, i18nc("@info", "No update found in the available version database."), i18nc("@title", "Up to date"));
0166         }
0167     }
0168 #else
0169     Q_UNUSED(parent)
0170     Q_UNUSED(force)
0171 #endif
0172 }
0173 
0174 UpdateNotification::UpdateNotification(QWidget *parent, const QString &version)
0175     : QDialog(parent)
0176 {
0177     resize(400, 200);
0178     auto lay = new QGridLayout(this);
0179     auto logo = new QLabel;
0180     logo->setMaximumWidth(110);
0181 
0182     setAttribute(Qt::WA_QuitOnClose, false);
0183 
0184     KIconLoader *const il = KIconLoader::global();
0185     const QString iconPath = il->iconPath(QStringLiteral("gpg4win"), KIconLoader::User);
0186     logo->setPixmap(QIcon(iconPath).pixmap(100, 100));
0187 
0188     auto label = new HtmlLabel;
0189     const QString boldVersion = QStringLiteral("<b>%1</b>").arg(version);
0190     label->setHtml(i18nc("%1 is the version number", "Version %1 is available.", boldVersion) + QStringLiteral("<br><br>")
0191                    + i18nc("Link to NEWS style changelog", "See the <a href=\"https://www.gpg4win.org/change-history.html\">new features</a>."));
0192     label->setOpenExternalLinks(true);
0193     label->setTextInteractionFlags(Qt::TextBrowserInteraction);
0194     label->setWordWrap(true);
0195     setWindowTitle(i18nc("@title:window", "Update Available"));
0196     setWindowIcon(QIcon(QLatin1StringView("gpg4win")));
0197 
0198     lay->addWidget(logo, 0, 0);
0199     lay->addWidget(label, 0, 1);
0200     const auto chk = new QCheckBox(i18n("Show this notification for future updates."));
0201     lay->addWidget(chk, 1, 0, 1, -1);
0202 
0203     KConfigGroup updatecfg(KSharedConfig::openConfig(), QStringLiteral("UpdateNotification"));
0204     chk->setChecked(!updatecfg.readEntry("NeverShow", false));
0205 
0206     const auto bb = new QDialogButtonBox();
0207     const auto b = bb->addButton(i18n("&Get update"), QDialogButtonBox::AcceptRole);
0208     b->setDefault(true);
0209     b->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down")));
0210     bb->addButton(QDialogButtonBox::Cancel);
0211     lay->addWidget(bb, 2, 0, 1, -1);
0212     connect(bb, &QDialogButtonBox::accepted, this, [this, chk]() {
0213         QDesktopServices::openUrl(QUrl(QStringLiteral("https://www.gpg4win.org/download.html")));
0214         KConfigGroup updatecfg(KSharedConfig::openConfig(), QStringLiteral("UpdateNotification"));
0215         updatecfg.writeEntry("NeverShow", !chk->isChecked());
0216         gpgconf_set_update_check(chk->isChecked());
0217         QDialog::accept();
0218     });
0219     connect(bb, &QDialogButtonBox::rejected, this, [this, chk]() {
0220         KConfigGroup updatecfg(KSharedConfig::openConfig(), QStringLiteral("UpdateNotification"));
0221         updatecfg.writeEntry("NeverShow", !chk->isChecked());
0222         gpgconf_set_update_check(chk->isChecked());
0223         QDialog::reject();
0224     });
0225 }