File indexing completed on 2024-04-28 16:44:24

0001 /*
0002   SPDX-FileCopyrightText: 1999 Matthias Hoelzer-Kluepfel <hoelzer@kde.org>
0003   SPDX-FileCopyrightText: 2000 Matthias Elter <elter@kde.org>
0004   SPDX-FileCopyrightText: 2004 Frans Englich <frans.englich@telia.com>
0005 
0006   SPDX-License-Identifier: GPL-2.0-or-later
0007 
0008 */
0009 
0010 #include "main.h"
0011 
0012 #include <config-kde-cli-tools.h>
0013 
0014 #include <QCommandLineOption>
0015 #include <QCommandLineParser>
0016 #include <QDBusConnection>
0017 #include <QDBusConnectionInterface>
0018 #include <QDBusInterface>
0019 #include <QDBusServiceWatcher>
0020 #include <QDebug>
0021 #include <QIcon>
0022 #include <QRegularExpression>
0023 #include <QStandardPaths>
0024 
0025 #include <KAboutData>
0026 #include <KActivities/ResourceInstance>
0027 #include <KAuthorized>
0028 #include <KCModuleProxy>
0029 #include <KLocalizedString>
0030 #include <KPluginMetaData>
0031 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 92)
0032 #include <KServiceTypeTrader>
0033 #endif
0034 #include <KStartupInfo>
0035 #include <kworkspace.h>
0036 
0037 #include <algorithm>
0038 #include <iostream>
0039 
0040 inline QVector<KPluginMetaData> findKCMsMetaData()
0041 {
0042     QVector<KPluginMetaData> metaDataList = KPluginMetaData::findPlugins(QStringLiteral("plasma/kcms"));
0043     metaDataList << KPluginMetaData::findPlugins(QStringLiteral("plasma/kcms/systemsettings"));
0044     metaDataList << KPluginMetaData::findPlugins(QStringLiteral("plasma/kcms/systemsettings_qwidgets"));
0045     metaDataList << KPluginMetaData::findPlugins(QStringLiteral("plasma/kcms/kinfocenter"));
0046     return metaDataList;
0047 }
0048 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 92)
0049 static KService::List listModules()
0050 {
0051     // First condition is what systemsettings does, second what kinfocenter does, make sure this is kept in sync
0052     // We need the exist calls because otherwise the trader language aborts if the property doesn't exist and the second part of the or is not evaluated
0053     KService::List services =
0054         KServiceTypeTrader::self()->query(QStringLiteral("KCModule"),
0055                                           QStringLiteral("(exist [X-KDE-System-Settings-Parent-Category] and [X-KDE-System-Settings-Parent-Category] != '') or "
0056                                                          "(exist [X-KDE-ParentApp] and [X-KDE-ParentApp] == 'kinfocenter')"));
0057 
0058     auto it = std::remove_if(services.begin(), services.end(), [](const KService::Ptr &service) {
0059         return !KAuthorized::authorizeControlModule(service->menuId());
0060     });
0061     services.erase(it, services.end());
0062 
0063     std::stable_sort(services.begin(), services.end(), [](const KService::Ptr s1, const KService::Ptr s2) {
0064         return QString::compare(s1->desktopEntryName(), s2->desktopEntryName(), Qt::CaseInsensitive) < 0;
0065     });
0066 
0067     return services;
0068 }
0069 
0070 static KService::Ptr locateModule(const QString &module)
0071 {
0072     QString path = module;
0073 
0074     if (!path.endsWith(QLatin1String(".desktop"))) {
0075         path += QStringLiteral(".desktop");
0076     }
0077 
0078     KService::Ptr service = KService::serviceByStorageId(path);
0079     if (!service) {
0080         return KService::Ptr();
0081     }
0082 
0083     if (!service->hasServiceType(QStringLiteral("KCModule"))) {
0084         // Not a KCModule. E.g. "kcmshell5 akonadi" finds services/kresources/kabc/akonadi.desktop, unrelated.
0085         return KService::Ptr();
0086     }
0087 
0088     if (service->noDisplay()) {
0089         qDebug() << module << "should not be loaded.";
0090         return KService::Ptr();
0091     }
0092 
0093     return service;
0094 }
0095 #endif
0096 
0097 bool KCMShell::isRunning()
0098 {
0099     const QString owner = QDBusConnection::sessionBus().interface()->serviceOwner(m_serviceName);
0100     if (owner == QDBusConnection::sessionBus().baseService()) {
0101         return false; // We are the one and only.
0102     }
0103 
0104     qDebug() << "kcmshell5 with modules" << m_serviceName << "is already running.";
0105 
0106     QDBusInterface iface(m_serviceName, QStringLiteral("/KCModule/dialog"), QStringLiteral("org.kde.KCMShellMultiDialog"));
0107     QDBusReply<void> reply = iface.call(QStringLiteral("activate"), KStartupInfo::startupId());
0108     if (!reply.isValid()) {
0109         qDebug() << "Calling D-Bus function dialog::activate() failed.";
0110         return false; // Error, we have to do it ourselves.
0111     }
0112 
0113     return true;
0114 }
0115 
0116 KCMShellMultiDialog::KCMShellMultiDialog(KPageDialog::FaceType dialogFace, QWidget *parent)
0117     : KCMultiDialog(parent)
0118 {
0119     setFaceType(dialogFace);
0120     setModal(false);
0121 
0122     QDBusConnection::sessionBus().registerObject(QStringLiteral("/KCModule/dialog"), this, QDBusConnection::ExportScriptableSlots);
0123 
0124     connect(this, &KCMShellMultiDialog::currentPageChanged, this, [](KPageWidgetItem *newPage, KPageWidgetItem *oldPage) {
0125         Q_UNUSED(oldPage);
0126         KCModuleProxy *activeModule = newPage->widget()->findChild<KCModuleProxy *>();
0127         if (activeModule) {
0128             KActivities::ResourceInstance::notifyAccessed(QUrl(QLatin1String("kcm:") + activeModule->metaData().pluginId()),
0129                                                           QStringLiteral("org.kde.systemsettings"));
0130         }
0131     });
0132 }
0133 
0134 void KCMShellMultiDialog::activate(const QByteArray &asn_id)
0135 {
0136 #ifdef HAVE_X11
0137     setAttribute(Qt::WA_NativeWindow, true);
0138     KStartupInfo::setNewStartupId(windowHandle(), asn_id);
0139 #endif
0140 }
0141 
0142 void KCMShell::setServiceName(const QString &dbusName)
0143 {
0144     m_serviceName = QLatin1String("org.kde.kcmshell_") + dbusName;
0145     QDBusConnection::sessionBus().registerService(m_serviceName);
0146 }
0147 
0148 void KCMShell::waitForExit()
0149 {
0150     QDBusServiceWatcher *watcher = new QDBusServiceWatcher(this);
0151     watcher->setConnection(QDBusConnection::sessionBus());
0152     watcher->setWatchMode(QDBusServiceWatcher::WatchForOwnerChange);
0153     watcher->addWatchedService(m_serviceName);
0154     connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &KCMShell::appExit);
0155     exec();
0156 }
0157 
0158 void KCMShell::appExit(const QString &appId, const QString &oldName, const QString &newName)
0159 {
0160     Q_UNUSED(appId);
0161     Q_UNUSED(newName);
0162 
0163     if (!oldName.isEmpty()) {
0164         qDebug() << appId << "closed, quitting.";
0165         qApp->quit();
0166     }
0167 }
0168 
0169 int main(int _argc, char *_argv[])
0170 {
0171     const bool qpaVariable = qEnvironmentVariableIsSet("QT_QPA_PLATFORM");
0172     KWorkSpace::detectPlatform(_argc, _argv);
0173     KCMShell app(_argc, _argv);
0174     if (!qpaVariable) {
0175         // don't leak the env variable to processes we start
0176         qunsetenv("QT_QPA_PLATFORM");
0177     }
0178     KLocalizedString::setApplicationDomain("kcmshell5");
0179 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0180     app.setAttribute(Qt::AA_UseHighDpiPixmaps, true);
0181 #endif
0182     KAboutData aboutData(QStringLiteral("kcmshell5"), //
0183                          i18n("System Settings Module"),
0184                          QLatin1String(PROJECT_VERSION),
0185                          i18n("A tool to start single system settings modules"),
0186                          KAboutLicense::GPL,
0187                          i18n("(c) 1999-2016, The KDE Developers"));
0188 
0189     aboutData.addAuthor(i18n("Frans Englich"), i18n("Maintainer"), QStringLiteral("frans.englich@kde.org"));
0190     aboutData.addAuthor(i18n("Daniel Molkentin"), QString(), QStringLiteral("molkentin@kde.org"));
0191     aboutData.addAuthor(i18n("Matthias Hoelzer-Kluepfel"), QString(), QStringLiteral("hoelzer@kde.org"));
0192     aboutData.addAuthor(i18n("Matthias Elter"), QString(), QStringLiteral("elter@kde.org"));
0193     aboutData.addAuthor(i18n("Matthias Ettrich"), QString(), QStringLiteral("ettrich@kde.org"));
0194     aboutData.addAuthor(i18n("Waldo Bastian"), QString(), QStringLiteral("bastian@kde.org"));
0195     KAboutData::setApplicationData(aboutData);
0196 
0197     QCommandLineParser parser;
0198     aboutData.setupCommandLine(&parser);
0199 
0200     parser.addOption(QCommandLineOption(QStringLiteral("list"), i18n("List all possible modules")));
0201     parser.addPositionalArgument(QStringLiteral("module"), i18n("Configuration module to open"));
0202     parser.addOption(QCommandLineOption(QStringLiteral("lang"), i18n("Specify a particular language"), QLatin1String("language")));
0203     parser.addOption(QCommandLineOption(QStringLiteral("silent"), i18n("Do not display main window")));
0204     parser.addOption(QCommandLineOption(QStringLiteral("args"), i18n("Arguments for the module"), QLatin1String("arguments")));
0205     parser.addOption(QCommandLineOption(QStringLiteral("icon"), i18n("Use a specific icon for the window"), QLatin1String("icon")));
0206     parser.addOption(QCommandLineOption(QStringLiteral("caption"), i18n("Use a specific caption for the window"), QLatin1String("caption")));
0207 
0208     parser.parse(app.arguments());
0209     aboutData.processCommandLine(&parser);
0210 
0211     parser.process(app);
0212 
0213     const QString lang = parser.value(QStringLiteral("lang"));
0214     if (!lang.isEmpty()) {
0215         std::cout << i18n("--lang is deprecated. Please set the LANGUAGE environment variable instead").toLocal8Bit().constData() << std::endl;
0216     }
0217 
0218     if (parser.isSet(QStringLiteral("list"))) {
0219         std::cout << i18n("The following modules are available:").toLocal8Bit().constData() << '\n';
0220 
0221         QVector<KPluginMetaData> plugins = findKCMsMetaData();
0222 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 92)
0223         const KService::List services = listModules();
0224         for (const auto &service : services) {
0225             const QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("/kservices5/") + service->entryPath());
0226             plugins << KPluginMetaData::fromDesktopFile(file);
0227         }
0228 #endif
0229         int maxLen = 0;
0230 
0231         for (const auto &plugin : plugins) {
0232             const int len = plugin.pluginId().size();
0233             maxLen = std::max(maxLen, len);
0234         }
0235 
0236         for (const auto &plugin : plugins) {
0237             QString comment = plugin.description();
0238             if (comment.isEmpty()) {
0239                 comment = i18n("No description available");
0240             }
0241 
0242             const QString entry = QStringLiteral("%1 - %2").arg(plugin.pluginId().leftJustified(maxLen, QLatin1Char(' ')), comment);
0243 
0244             std::cout << entry.toLocal8Bit().constData() << '\n';
0245         }
0246 
0247         std::cout << std::endl;
0248 
0249         return 0;
0250     }
0251 
0252     if (parser.positionalArguments().isEmpty()) {
0253         parser.showHelp();
0254         return -1;
0255     }
0256 
0257     QString serviceName;
0258     QList<KPluginMetaData> metaDataList;
0259 
0260     QStringList args = parser.positionalArguments();
0261     args.removeDuplicates();
0262     for (const QString &arg : args) {
0263         KPluginMetaData data(arg);
0264         if (data.isValid()) {
0265             metaDataList << data;
0266         } else {
0267             // Look in the namespaces for systemsettings/kinfocenter
0268             const static auto knownKCMs = findKCMsMetaData();
0269             const QStringList possibleIds{arg, QStringLiteral("kcm_") + arg, QStringLiteral("kcm") + arg};
0270             bool foundKCM = std::any_of(knownKCMs.begin(), knownKCMs.end(), [&possibleIds, &metaDataList, &arg, &serviceName](const KPluginMetaData &data) {
0271                 bool idMatches = possibleIds.contains(data.pluginId());
0272                 if (idMatches) {
0273                     metaDataList << data;
0274                     if (!serviceName.isEmpty()) {
0275                         serviceName += QLatin1Char('_');
0276                     }
0277                     serviceName += arg;
0278                 }
0279                 return idMatches;
0280             });
0281             if (foundKCM) {
0282                 continue;
0283             }
0284 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 92)
0285             KService::Ptr service = locateModule(arg);
0286             if (!service) {
0287                 service = locateModule(QStringLiteral("kcm_") + arg);
0288             }
0289             if (!service) {
0290                 service = locateModule(QStringLiteral("kcm") + arg);
0291             }
0292 
0293             if (service) {
0294                 if (!serviceName.isEmpty()) {
0295                     serviceName += QLatin1Char('_');
0296                 }
0297                 serviceName += arg;
0298 
0299                 const QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("/kservices5/") + service->entryPath());
0300                 auto data = KPluginMetaData::fromDesktopFile(file);
0301                 metaDataList << data;
0302             } else {
0303                 std::cerr << i18n("Could not find module '%1'. See kcmshell5 --list for the full list of modules.", arg).toLocal8Bit().constData() << std::endl;
0304             }
0305 #endif
0306         }
0307     }
0308 
0309     /* Check if this particular module combination is already running */
0310     app.setServiceName(serviceName);
0311     if (app.isRunning()) {
0312         app.waitForExit();
0313         return 0;
0314     }
0315 
0316     KPageDialog::FaceType ftype = KPageDialog::Plain;
0317 
0318     const int modCount = metaDataList.count();
0319     if (modCount == 0) {
0320         return -1;
0321     }
0322 
0323     if (modCount > 1) {
0324         ftype = KPageDialog::List;
0325     }
0326 
0327     KCMShellMultiDialog *dlg = new KCMShellMultiDialog(ftype);
0328     dlg->setAttribute(Qt::WA_DeleteOnClose);
0329 
0330     if (parser.isSet(QStringLiteral("caption"))) {
0331         dlg->setWindowTitle(parser.value(QStringLiteral("caption")));
0332     } else if (modCount == 1) {
0333         dlg->setWindowTitle(metaDataList.constFirst().name());
0334     }
0335 
0336     const QStringList moduleArgs = parser.value(QStringLiteral("args")).split(QRegularExpression(QStringLiteral(" +")));
0337     if (metaDataList.size() == 1) {
0338         KPageWidgetItem *item = dlg->addModule(*metaDataList.cbegin(), moduleArgs);
0339         // This makes sure the content area is focused by default
0340         item->widget()->setFocus(Qt::MouseFocusReason);
0341     } else {
0342         for (const KPluginMetaData &m : std::as_const(metaDataList)) {
0343             dlg->addModule(m, moduleArgs);
0344         }
0345     }
0346 
0347     if (parser.isSet(QStringLiteral("icon"))) {
0348         dlg->setWindowIcon(QIcon::fromTheme(parser.value(QStringLiteral("icon"))));
0349     } else {
0350         dlg->setWindowIcon(QIcon::fromTheme(metaDataList.constFirst().iconName()));
0351     }
0352 
0353     if (app.desktopFileName() == QLatin1String("org.kde.kcmshell5")) {
0354         const QString path = metaDataList.constFirst().metaDataFileName();
0355 
0356         if (path.endsWith(QLatin1String(".desktop"))) {
0357             app.setDesktopFileName(path);
0358         } else {
0359             app.setDesktopFileName(metaDataList.constFirst().pluginId());
0360         }
0361     }
0362 
0363     dlg->show();
0364 
0365     app.exec();
0366 
0367     return 0;
0368 }
0369 // vim: sw=4 et sts=4