File indexing completed on 2024-04-28 16:51:33

0001 /*
0002     SPDX-FileCopyrightText: 2017 Kai Uwe Broulik <kde@privat.broulik.de>
0003     SPDX-FileCopyrightText: 2017 David Edmundson <davidedmundson@kde.org>
0004     SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
0005 
0006     SPDX-License-Identifier: MIT
0007 */
0008 
0009 #include "settings.h"
0010 
0011 #include <unistd.h> // getppid
0012 
0013 #include <QDBusConnection>
0014 #include <QGuiApplication>
0015 #include <QIcon>
0016 #include <QProcess>
0017 
0018 #include <KDesktopFile>
0019 #include <KProcessList>
0020 #include <KService>
0021 
0022 #include <taskmanager/abstracttasksmodel.h>
0023 #include <taskmanager/windowtasksmodel.h>
0024 
0025 #include "pluginmanager.h"
0026 
0027 #include <config-host.h>
0028 
0029 const QMap<Settings::Environment, QString> Settings::environmentNames = {
0030     {Settings::Environment::Chrome, QStringLiteral("chrome")},
0031     {Settings::Environment::Chromium, QStringLiteral("chromium")},
0032     {Settings::Environment::Firefox, QStringLiteral("firefox")},
0033     {Settings::Environment::Opera, QStringLiteral("opera")},
0034     {Settings::Environment::Vivaldi, QStringLiteral("vivaldi")},
0035     {Settings::Environment::Brave, QStringLiteral("brave")},
0036     {Settings::Environment::Edge, QStringLiteral("edge")},
0037 };
0038 
0039 const QMap<Settings::Environment, EnvironmentDescription> Settings::environmentDescriptions = {
0040     {Settings::Environment::Chrome,
0041      {
0042          QStringLiteral("google-chrome"),
0043          QStringLiteral("Google Chrome"),
0044          QStringLiteral("google-chrome"),
0045          QStringLiteral("google.com"),
0046          QStringLiteral("Google"),
0047          QStringLiteral("google-chrome"),
0048      }},
0049     {Settings::Environment::Chromium,
0050      {
0051          QStringLiteral("chromium-browser"),
0052          QStringLiteral("Chromium"),
0053          QStringLiteral("chromium-browser"),
0054          QStringLiteral("google.com"),
0055          QStringLiteral("Google"),
0056          QStringLiteral("chromium-browser"),
0057      }},
0058     {Settings::Environment::Firefox,
0059      {
0060          QStringLiteral("firefox"),
0061          QStringLiteral("Mozilla Firefox"),
0062          QStringLiteral("firefox"),
0063          QStringLiteral("mozilla.org"),
0064          QStringLiteral("Mozilla"),
0065          QStringLiteral("firefox"),
0066      }},
0067     {Settings::Environment::Opera,
0068      {
0069          QStringLiteral("opera"),
0070          QStringLiteral("Opera"),
0071          QStringLiteral("opera"),
0072          QStringLiteral("opera.com"),
0073          QStringLiteral("Opera"),
0074          QStringLiteral("opera"),
0075      }},
0076     {Settings::Environment::Vivaldi,
0077      {
0078          QStringLiteral("vivaldi"),
0079          QStringLiteral("Vivaldi"),
0080          // This is what the official package on their website uses
0081          QStringLiteral("vivaldi-stable"),
0082          QStringLiteral("vivaldi.com"),
0083          QStringLiteral("Vivaldi"),
0084          QStringLiteral("vivaldi"),
0085      }},
0086     {Settings::Environment::Brave,
0087      {
0088          QStringLiteral("Brave"),
0089          QStringLiteral("Brave"),
0090          QStringLiteral("brave-browser"),
0091          QStringLiteral("brave.com"),
0092          QStringLiteral("Brave"),
0093          QStringLiteral("brave"),
0094      }},
0095     {Settings::Environment::Edge,
0096      {
0097          QStringLiteral("Edge"),
0098          QStringLiteral("Microsoft Edge"),
0099          QStringLiteral("microsoft-edge"),
0100          QStringLiteral("microsoft.com"),
0101          QStringLiteral("Microsoft"),
0102          QStringLiteral("microsoft-edge"),
0103      }},
0104 };
0105 
0106 Settings::Settings()
0107     : AbstractBrowserPlugin(QStringLiteral("settings"), 1, nullptr)
0108     // Settings is a singleton and cleaned up only in exit_handlers
0109     // at which point the QPA will already have cleaned up
0110     // leading to a crash on Wayland
0111     , m_tasksModel(new TaskManager::WindowTasksModel(qGuiApp))
0112 {
0113     for (int i = 0; i < m_tasksModel->rowCount(); ++i) {
0114         if (setEnvironmentFromTasksModelIndex(m_tasksModel->index(i, 0))) {
0115             break;
0116         }
0117     }
0118 
0119     // If we didn't find the browser window yet, monitor the model for changes
0120     if (qApp->desktopFileName().isEmpty()) {
0121         connect(m_tasksModel, &TaskManager::WindowTasksModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
0122             if (parent.isValid()) {
0123                 return;
0124             }
0125 
0126             for (int i = first; i <= last; ++i) {
0127                 if (setEnvironmentFromTasksModelIndex(m_tasksModel->index(i, 0))) {
0128                     break;
0129                 }
0130             }
0131         });
0132         connect(m_tasksModel,
0133                 &TaskManager::WindowTasksModel::dataChanged,
0134                 this,
0135                 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) {
0136                     // TODO should we bother checking this, even?
0137                     if (topLeft.parent().isValid() || bottomRight.parent().isValid() || topLeft.column() != 0 || bottomRight.column() != 0) {
0138                         return;
0139                     }
0140 
0141                     if (!roles.isEmpty() && !roles.contains(TaskManager::AbstractTasksModel::LauncherUrlWithoutIcon)
0142                         && !roles.contains(TaskManager::AbstractTasksModel::AppId)) {
0143                         return;
0144                     }
0145 
0146                     for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
0147                         if (setEnvironmentFromTasksModelIndex(m_tasksModel->index(i, 0))) {
0148                             break;
0149                         }
0150                     }
0151                 });
0152     }
0153 }
0154 
0155 Settings &Settings::self()
0156 {
0157     static Settings s_self;
0158     return s_self;
0159 }
0160 
0161 void Settings::handleData(const QString &event, const QJsonObject &data)
0162 {
0163     if (event == QLatin1String("changed")) {
0164         m_settings = data;
0165 
0166         for (auto it = data.begin(), end = data.end(); it != end; ++it) {
0167             const QString &subsystem = it.key();
0168             const QJsonObject &settingsObject = it->toObject();
0169 
0170             const QJsonValue enabledVariant = settingsObject.value(QStringLiteral("enabled"));
0171             // probably protocol overhead, not a plugin setting, skip.
0172             if (enabledVariant.type() == QJsonValue::Undefined) {
0173                 continue;
0174             }
0175 
0176             auto *plugin = PluginManager::self().pluginForSubsystem(subsystem);
0177             if (!plugin) {
0178                 continue;
0179             }
0180 
0181             if (enabledVariant.toBool()) {
0182                 PluginManager::self().loadPlugin(plugin);
0183             } else {
0184                 PluginManager::self().unloadPlugin(plugin);
0185             }
0186 
0187             PluginManager::self().settingsChanged(plugin, settingsObject);
0188         }
0189 
0190         Q_EMIT changed(data);
0191     } else if (event == QLatin1String("openKRunnerSettings")) {
0192         QProcess::startDetached(QStringLiteral("systemsettings5"), {QStringLiteral("kcm_plasmasearch")});
0193     } else if (event == QLatin1String("setEnvironment")) {
0194         setEnvironmentFromExtensionMessage(data);
0195     }
0196 }
0197 
0198 bool Settings::setEnvironmentFromTasksModelIndex(const QModelIndex &idx)
0199 {
0200     bool ok = false;
0201     const auto pid = idx.data(TaskManager::AbstractTasksModel::AppPid).toLongLong(&ok);
0202     if (!ok || pid != getppid()) {
0203         return false;
0204     }
0205 
0206     const QUrl launcherUrl = idx.data(TaskManager::AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
0207     if (!launcherUrl.isValid()) {
0208         return false;
0209     }
0210 
0211     KService::Ptr service;
0212     if (launcherUrl.scheme() == QLatin1String("applications")) {
0213         service = KService::serviceByMenuId(launcherUrl.path());
0214     } else if (launcherUrl.isLocalFile()) {
0215         const QString launcherPath = launcherUrl.toLocalFile();
0216         if (KDesktopFile::isDesktopFile(launcherPath)) {
0217             service = KService::serviceByDesktopPath(launcherUrl.toLocalFile());
0218         }
0219     } else {
0220         qWarning() << "Got unrecognized launcher URL" << launcherUrl;
0221         return false;
0222     }
0223 
0224     if (!service) {
0225         qWarning() << "Failed to get service from launcher URL" << launcherUrl;
0226         return false;
0227     }
0228 
0229     // Ignore any browser-hosted app windows.
0230     if (!service->categories().contains(QLatin1String("WebBrowser"))) {
0231         qInfo() << "Ignoring launcher URL" << launcherUrl << "which doesn't have \"WebBrowser\" category";
0232         return false;
0233     }
0234 
0235     qApp->setApplicationName(service->menuId());
0236     qApp->setApplicationDisplayName(service->name());
0237     qApp->setDesktopFileName(service->desktopEntryName());
0238     qApp->setWindowIcon(QIcon::fromTheme(service->icon()));
0239 
0240     m_tasksModel->disconnect(this); // prevent further signal emission to not deref a nullptr https://bugs.kde.org/show_bug.cgi?id=435811
0241     m_tasksModel->deleteLater();
0242     m_tasksModel = nullptr;
0243 
0244     return true;
0245 }
0246 
0247 void Settings::setEnvironmentFromExtensionMessage(const QJsonObject &data)
0248 {
0249     QString name = data.value(QStringLiteral("browserName")).toString();
0250 
0251     // Most chromium-based browsers just impersonate Chromium nowadays to keep websites from locking them out
0252     // so we'll need to make an educated guess from our parent process
0253     if (name == QLatin1String("chromium") || name == QLatin1String("chrome")) {
0254         const auto processInfo = KProcessList::processInfo(getppid());
0255         if (processInfo.name().contains(QLatin1String("vivaldi"))) {
0256             name = QStringLiteral("vivaldi");
0257         } else if (processInfo.name().contains(QLatin1String("brave"))) {
0258             name = QStringLiteral("brave");
0259         } else if (processInfo.name().contains(QLatin1String("edge"))) {
0260             name = QStringLiteral("edge");
0261         }
0262     }
0263 
0264     m_environment = Settings::environmentNames.key(name, Settings::Environment::Unknown);
0265     m_currentEnvironment = Settings::environmentDescriptions.value(m_environment);
0266 
0267     if (qApp->desktopFileName().isEmpty()) {
0268         qApp->setApplicationName(m_currentEnvironment.applicationName);
0269         qApp->setApplicationDisplayName(m_currentEnvironment.applicationDisplayName);
0270         qApp->setDesktopFileName(m_currentEnvironment.desktopFileName);
0271         qApp->setWindowIcon(QIcon::fromTheme(m_currentEnvironment.iconName));
0272         // TODO remove?
0273         qApp->setOrganizationDomain(m_currentEnvironment.organizationDomain);
0274         qApp->setOrganizationName(m_currentEnvironment.organizationName);
0275     }
0276 }
0277 
0278 QJsonObject Settings::handleData(int serial, const QString &event, const QJsonObject &data)
0279 {
0280     Q_UNUSED(serial)
0281     Q_UNUSED(data)
0282 
0283     QJsonObject ret;
0284 
0285     if (event == QLatin1String("getSubsystemStatus")) {
0286         // should we add a PluginManager::knownSubsystems() that returns a QList<AbstractBrowserPlugin*>?
0287         const QStringList subsystems = PluginManager::self().knownPluginSubsystems();
0288         for (const QString &subsystem : subsystems) {
0289             const AbstractBrowserPlugin *plugin = PluginManager::self().pluginForSubsystem(subsystem);
0290 
0291             QJsonObject details = plugin->status();
0292             details.insert(QStringLiteral("version"), plugin->protocolVersion());
0293             details.insert(QStringLiteral("loaded"), plugin->isLoaded());
0294             ret.insert(subsystem, details);
0295         }
0296     } else if (event == QLatin1String("getVersion")) {
0297         ret.insert(QStringLiteral("host"), QStringLiteral(HOST_VERSION_STRING));
0298     }
0299 
0300     return ret;
0301 }
0302 
0303 Settings::Environment Settings::environment() const
0304 {
0305     return m_environment;
0306 }
0307 
0308 bool Settings::pluginEnabled(const QString &subsystem) const
0309 {
0310     return settingsForPlugin(subsystem).value(QStringLiteral("enabled")).toBool();
0311 }
0312 
0313 QJsonObject Settings::settingsForPlugin(const QString &subsystem) const
0314 {
0315     return m_settings.value(subsystem).toObject();
0316 }