File indexing completed on 2024-04-28 05:26:46

0001 /*
0002  *   SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "DiscoverNotifier.h"
0008 #include "BackendNotifierFactory.h"
0009 #include "UnattendedUpdates.h"
0010 #include <KLocalizedString>
0011 #include <KNotificationJobUiDelegate>
0012 #include <KPluginFactory>
0013 #include <QDBusConnection>
0014 #include <QDBusConnectionInterface>
0015 #include <QDBusMessage>
0016 #include <QDBusPendingCall>
0017 #include <QDebug>
0018 #include <QNetworkInformation>
0019 #include <QProcess>
0020 
0021 #include <KIO/ApplicationLauncherJob>
0022 #include <KIO/CommandLauncherJob>
0023 
0024 #include "../libdiscover/utils.h"
0025 #include "updatessettings.h"
0026 #include <chrono>
0027 
0028 using namespace std::chrono_literals;
0029 
0030 namespace
0031 {
0032 bool isOnline()
0033 {
0034     return QNetworkInformation::instance() && QNetworkInformation::instance()->reachability() == QNetworkInformation::Reachability::Online;
0035 }
0036 } // namespace
0037 
0038 DiscoverNotifier::DiscoverNotifier(QObject *parent)
0039     : QObject(parent)
0040 {
0041     m_settings = new UpdatesSettings(this);
0042     m_settingsWatcher = KConfigWatcher::create(m_settings->sharedConfig());
0043     QNetworkInformation::loadBackendByFeatures(QNetworkInformation::Feature::Reachability | QNetworkInformation::Feature::TransportMedium);
0044     if (auto info = QNetworkInformation::instance()) {
0045         connect(info, &QNetworkInformation::reachabilityChanged, this, &DiscoverNotifier::stateChanged);
0046         connect(info, &QNetworkInformation::transportMediumChanged, this, &DiscoverNotifier::stateChanged);
0047     } else {
0048         qWarning() << "QNetworkInformation has no backend. Is NetworkManager.service running?";
0049     }
0050 
0051     refreshUnattended();
0052     connect(m_settingsWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) {
0053         if (group.config()->name() != m_settings->config()->name() || group.name() != QLatin1String("Global")) {
0054             return;
0055         }
0056         if (names.contains("UseUnattendedUpdates") || names.contains("RequiredNotificationInterval")) {
0057             refreshUnattended();
0058             Q_EMIT stateChanged();
0059         }
0060     });
0061 
0062     m_backends = BackendNotifierFactory().allBackends();
0063     for (BackendNotifierModule *module : std::as_const(m_backends)) {
0064         connect(module, &BackendNotifierModule::foundUpdates, this, &DiscoverNotifier::updateStatusNotifier);
0065         connect(module, &BackendNotifierModule::needsRebootChanged, this, [this]() {
0066             // If we are using offline updates, there is no need to badger the user to
0067             // reboot since it is safe to continue using the system in its current state
0068             if (!m_needsReboot && !m_settings->useUnattendedUpdates()) {
0069                 m_needsReboot = true;
0070                 showRebootNotification();
0071                 Q_EMIT stateChanged();
0072                 Q_EMIT needsRebootChanged(true);
0073             }
0074         });
0075 
0076         connect(module, &BackendNotifierModule::foundUpgradeAction, this, &DiscoverNotifier::foundUpgradeAction);
0077     }
0078     connect(&m_timer, &QTimer::timeout, this, &DiscoverNotifier::showUpdatesNotification);
0079     m_timer.setSingleShot(true);
0080     m_timer.setInterval(1s);
0081     updateStatusNotifier();
0082 
0083     // Only fetch updates after the system is comfortably booted
0084     QTimer::singleShot(0s, this, &DiscoverNotifier::recheckSystemUpdateNeeded);
0085 }
0086 
0087 DiscoverNotifier::~DiscoverNotifier() = default;
0088 
0089 void DiscoverNotifier::showDiscover(const QString &xdgActivationToken)
0090 {
0091     auto *job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(QStringLiteral("org.kde.discover")));
0092     job->setStartupId(xdgActivationToken.toUtf8());
0093     job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
0094     job->start();
0095 
0096     if (m_updatesAvailableNotification) {
0097         m_updatesAvailableNotification->close();
0098     }
0099 }
0100 
0101 void DiscoverNotifier::showDiscoverUpdates(const QString &xdgActivationToken)
0102 {
0103     auto *job = new KIO::CommandLauncherJob(QStringLiteral("plasma-discover"), {QStringLiteral("--mode"), QStringLiteral("update")});
0104     job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
0105     job->setDesktopName(QStringLiteral("org.kde.discover"));
0106     job->setStartupId(xdgActivationToken.toUtf8());
0107     job->start();
0108 
0109     if (m_updatesAvailableNotification) {
0110         m_updatesAvailableNotification->close();
0111     }
0112 }
0113 
0114 bool DiscoverNotifier::notifyAboutUpdates() const
0115 {
0116     if (state() != NormalUpdates && state() != SecurityUpdates) {
0117         // it's not very helpful to notify that everything is in order
0118         return false;
0119     }
0120 
0121     if (m_settings->requiredNotificationInterval() < 0) {
0122         return false;
0123     }
0124 
0125     // To configure to a random value, execute:
0126     // kwriteconfig5 --file PlasmaDiscoverUpdates --group Global --key RequiredNotificationInterval 3600
0127     const QDateTime earliestNextNotificationTime = m_settings->lastNotificationTime().addSecs(m_settings->requiredNotificationInterval());
0128     if (earliestNextNotificationTime.isValid() && earliestNextNotificationTime > QDateTime::currentDateTimeUtc()) {
0129         return false;
0130     }
0131 
0132     m_settings->setLastNotificationTime(QDateTime::currentDateTimeUtc());
0133     m_settings->save();
0134 
0135     if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.discover"))) {
0136         return false;
0137     }
0138     return true;
0139 }
0140 
0141 void DiscoverNotifier::showUpdatesNotification()
0142 {
0143     if (m_updatesAvailableNotification) {
0144         m_updatesAvailableNotification->close();
0145     }
0146 
0147     if (!notifyAboutUpdates()) {
0148         return;
0149     }
0150 
0151     m_updatesAvailableNotification =
0152         KNotification::event(QStringLiteral("Update"), message(), {}, iconName(), KNotification::CloseOnTimeout, QStringLiteral("discoverabstractnotifier"));
0153     m_updatesAvailableNotification->setHint(QStringLiteral("resident"), true);
0154     const QString name = i18n("View Updates");
0155 
0156     auto showUpdates = [this] {
0157         showDiscoverUpdates(m_updatesAvailableNotification->xdgActivationToken());
0158     };
0159 
0160     auto defaultAction = m_updatesAvailableNotification->addDefaultAction(name);
0161     connect(defaultAction, &KNotificationAction::activated, this, showUpdates);
0162 
0163     auto showUpdatesAction = m_updatesAvailableNotification->addAction(name);
0164     connect(showUpdatesAction, &KNotificationAction::activated, this, showUpdates);
0165 }
0166 
0167 void DiscoverNotifier::updateStatusNotifier()
0168 {
0169     const bool hasSecurityUpdates = kContains(m_backends, [](BackendNotifierModule *module) {
0170         return module->hasSecurityUpdates();
0171     });
0172     const bool hasUpdates = hasSecurityUpdates || kContains(m_backends, [](BackendNotifierModule *module) {
0173                                 return module->hasUpdates();
0174                             });
0175 
0176     if (m_hasUpdates == hasUpdates && m_hasSecurityUpdates == hasSecurityUpdates)
0177         return;
0178 
0179     m_hasSecurityUpdates = hasSecurityUpdates;
0180     m_hasUpdates = hasUpdates;
0181 
0182     if (state() != NoUpdates) {
0183         m_timer.start();
0184     }
0185 
0186     Q_EMIT stateChanged();
0187 }
0188 
0189 // we only want to do unattended updates when on an ethernet or wlan network
0190 static bool isConnectionAdequate()
0191 {
0192     const auto info = QNetworkInformation::instance();
0193     if (!info) {
0194         return false; // no backend available (e.g. NetworkManager not running), assume not adequate
0195     }
0196     if (info->supports(QNetworkInformation::Feature::Metered)) {
0197         return !info->isMetered();
0198     } else {
0199         const auto transport = info->transportMedium();
0200         return transport == QNetworkInformation::TransportMedium::Ethernet || transport == QNetworkInformation::TransportMedium::WiFi;
0201     }
0202 }
0203 
0204 void DiscoverNotifier::refreshUnattended()
0205 {
0206     m_settings->read();
0207 
0208     if (!notifyAboutUpdates()) {
0209         return;
0210     }
0211 
0212     const auto enabled = m_settings->useUnattendedUpdates() && isOnline() && isConnectionAdequate();
0213     if (bool(m_unattended) == enabled)
0214         return;
0215 
0216     if (enabled) {
0217         m_unattended = new UnattendedUpdates(this);
0218     } else {
0219         delete m_unattended;
0220         m_unattended = nullptr;
0221     }
0222 }
0223 
0224 DiscoverNotifier::State DiscoverNotifier::state() const
0225 {
0226     if (m_needsReboot)
0227         return RebootRequired;
0228     else if (m_isBusy)
0229         return Busy;
0230     else if (!isOnline())
0231         return Offline;
0232     else if (m_hasSecurityUpdates)
0233         return SecurityUpdates;
0234     else if (m_hasUpdates)
0235         return NormalUpdates;
0236     else
0237         return NoUpdates;
0238 }
0239 
0240 QString DiscoverNotifier::iconName() const
0241 {
0242     switch (state()) {
0243     case SecurityUpdates:
0244         return QStringLiteral("update-high");
0245     case NormalUpdates:
0246         return QStringLiteral("update-low");
0247     case NoUpdates:
0248         return QStringLiteral("update-none");
0249     case RebootRequired:
0250         return QStringLiteral("system-reboot");
0251     case Offline:
0252         return QStringLiteral("offline");
0253     case Busy:
0254         return QStringLiteral("state-download");
0255     }
0256     return QString();
0257 }
0258 
0259 QString DiscoverNotifier::message() const
0260 {
0261     switch (state()) {
0262     case SecurityUpdates:
0263         return i18n("Security updates available");
0264     case NormalUpdates:
0265         return i18n("Updates available");
0266     case NoUpdates:
0267         return i18n("System up to date");
0268     case RebootRequired:
0269         return i18n("Computer needs to restart");
0270     case Offline:
0271         return i18n("Offline");
0272     case Busy:
0273         return i18n("Applying unattended updates…");
0274     }
0275     return QString();
0276 }
0277 
0278 void DiscoverNotifier::recheckSystemUpdateNeeded()
0279 {
0280     for (BackendNotifierModule *module : std::as_const(m_backends))
0281         module->recheckSystemUpdateNeeded();
0282 
0283     refreshUnattended();
0284 }
0285 
0286 QStringList DiscoverNotifier::loadedModules() const
0287 {
0288     QStringList ret;
0289     for (BackendNotifierModule *module : m_backends)
0290         ret += QString::fromLatin1(module->metaObject()->className());
0291     return ret;
0292 }
0293 
0294 void DiscoverNotifier::showRebootNotification()
0295 {
0296     KNotification *notification = KNotification::event(QStringLiteral("UpdateRestart"),
0297                                                        i18n("Restart is required"),
0298                                                        i18n("The system needs to be restarted for the updates to take effect."),
0299                                                        QStringLiteral("system-software-update"),
0300                                                        KNotification::Persistent | KNotification::DefaultEvent,
0301                                                        QStringLiteral("discoverabstractnotifier"));
0302 
0303     auto restartAction = notification->addAction(i18nc("@action:button", "Restart"));
0304     connect(restartAction, &KNotificationAction::activated, this, &DiscoverNotifier::reboot);
0305 
0306     notification->sendEvent();
0307 }
0308 
0309 void DiscoverNotifier::reboot()
0310 {
0311     auto method = QDBusMessage::createMethodCall(QStringLiteral("org.kde.LogoutPrompt"),
0312                                                  QStringLiteral("/LogoutPrompt"),
0313                                                  QStringLiteral("org.kde.LogoutPrompt"),
0314                                                  QStringLiteral("promptReboot"));
0315     QDBusConnection::sessionBus().asyncCall(method);
0316 }
0317 
0318 void DiscoverNotifier::foundUpgradeAction(UpgradeAction *action)
0319 {
0320     updateStatusNotifier();
0321 
0322     if (!notifyAboutUpdates()) {
0323         return;
0324     }
0325 
0326     KNotification *notification = new KNotification(QStringLiteral("DistUpgrade"), KNotification::Persistent);
0327     notification->setIconName(QStringLiteral("system-software-update"));
0328     notification->setTitle(i18n("Upgrade available"));
0329     notification->setText(i18nc("A new distro release (name and version) is available for upgrade", "%1 is now available.", action->description()));
0330     notification->setComponentName(QStringLiteral("discoverabstractnotifier"));
0331 
0332     auto upgradeAction = notification->addAction(i18nc("@action:button", "Upgrade"));
0333     connect(upgradeAction, &KNotificationAction::activated, this, [action] {
0334         action->trigger();
0335     });
0336 
0337     connect(action, &UpgradeAction::showDiscoverUpdates, this, [this, notification]() {
0338         showDiscoverUpdates(notification->xdgActivationToken());
0339     });
0340 
0341     notification->sendEvent();
0342 }
0343 
0344 void DiscoverNotifier::setBusy(bool isBusy)
0345 {
0346     if (isBusy == m_isBusy)
0347         return;
0348 
0349     m_isBusy = isBusy;
0350     Q_EMIT busyChanged();
0351     Q_EMIT stateChanged();
0352 }