/* SPDX-FileCopyrightText: 2016 Eike Hein SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL */ #include "tasktools.h" #include #include #include #include #include #include #include #include #include #include #if KF5_VERSION_MINOR >= 62 #include #else #include #include #endif #include #include #include #include #include #if HAVE_X11 #include #endif namespace Latte { namespace WindowSystem { AppData appDataFromUrl(const QUrl &url, const QIcon &fallbackIcon) { AppData data; data.url = url; if (url.hasQuery()) { QUrlQuery uQuery(url); if (uQuery.hasQueryItem(QLatin1String("iconData"))) { QString iconData(uQuery.queryItemValue(QLatin1String("iconData"))); QPixmap pixmap; QByteArray bytes = QByteArray::fromBase64(iconData.toLocal8Bit(), QByteArray::Base64UrlEncoding); pixmap.loadFromData(bytes); data.icon.addPixmap(pixmap); } if (uQuery.hasQueryItem(QLatin1String("skipTaskbar"))) { QString skipTaskbar(uQuery.queryItemValue(QLatin1String("skipTaskbar"))); data.skipTaskbar = (skipTaskbar == QStringLiteral("true")); } } // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (url.scheme() == QStringLiteral("applications")) { const KService::Ptr service = KService::serviceByMenuId(url.path()); if (service && url.path() == service->menuId()) { data.name = service->name(); data.genericName = service->genericName(); data.id = service->storageId(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(service->icon()); } } } if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) { const KService::Ptr service = KService::serviceByStorageId(url.fileName()); // Resolve to non-absolute menuId-based URL if possible. if (service) { const QString &menuId = service->menuId(); if (!menuId.isEmpty()) { data.url = QUrl(QStringLiteral("applications:") + menuId); } } if (service && QUrl::fromLocalFile(service->entryPath()) == url) { data.name = service->name(); data.genericName = service->genericName(); data.id = service->storageId(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(service->icon()); } } else { KDesktopFile f(url.toLocalFile()); if (f.tryExec()) { data.name = f.readName(); data.genericName = f.readGenericName(); data.id = QUrl::fromLocalFile(f.fileName()).fileName(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(f.readIcon()); } } } if (data.id.endsWith(".desktop")) { data.id = data.id.left(data.id.length() - 8); } } else if (url.scheme() == QLatin1String("preferred")) { data.id = defaultApplication(url); const KService::Ptr service = KService::serviceByStorageId(data.id); if (service) { const QString &menuId = service->menuId(); const QString &desktopFile = service->entryPath(); data.name = service->name(); data.genericName = service->genericName(); data.id = service->storageId(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(service->icon()); } // Update with resolved URL. if (!menuId.isEmpty()) { data.url = QUrl(QStringLiteral("applications:") + menuId); } else { data.url = QUrl::fromLocalFile(desktopFile); } } } if (data.name.isEmpty()) { data.name = url.fileName(); } if (data.icon.isNull()) { data.icon = fallbackIcon; } return data; } AppData appDataFromAppId(const QString &appId) { AppData data; KService::Ptr service = KService::serviceByStorageId(appId); if (service) { data.id = service->storageId(); data.name = service->name(); data.genericName = service->genericName(); const QString &menuId = service->menuId(); // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (!menuId.isEmpty()) { data.url = QUrl(QStringLiteral("applications:") + menuId); } else { data.url = QUrl::fromLocalFile(service->entryPath()); } return data; } QString desktopFile = appId; if (!desktopFile.endsWith(QLatin1String(".desktop"))) { desktopFile.append(QLatin1String(".desktop")); } if (KDesktopFile::isDesktopFile(desktopFile) && QFile::exists(desktopFile)) { KDesktopFile f(desktopFile); data.id = QUrl::fromLocalFile(f.fileName()).fileName(); if (data.id.endsWith(QLatin1String(".desktop"))) { data.id = data.id.left(data.id.length() - 8); } data.name = f.readName(); data.genericName = f.readGenericName(); data.url = QUrl::fromLocalFile(desktopFile); } return data; } QUrl windowUrlFromMetadata(const QString &appId, quint32 pid, KSharedConfig::Ptr rulesConfig, const QString &xWindowsWMClassName) { if (!rulesConfig) { return QUrl(); } QUrl url; KService::List services; bool triedPid = false; // The code below this function goes on a hunt for services based on the metadata // that has been passed in. Occasionally, it will find more than one matching // service. In some scenarios (e.g. multiple identically-named .desktop files) // there's a need to pick the most useful one. The function below promises to "sort" // a list of services by how closely their KService::menuId() relates to the key that // has been passed in. The current naive implementation simply looks for a menuId // that starts with the key, prepends it to the list and returns it. In practice, // that means a KService with a menuId matching the appId will win over one with a // menuId that encodes a subfolder hierarchy. // A concrete example: Valve's Steam client is sometimes installed two times, once // natively as a Linux application, once via Wine. Both have .desktop files named // (S|)steam.desktop. The Linux native version is located in the menu by means of // categorization ("Games") and just has a menuId() matching the .desktop file name, // but the Wine version is placed in a folder hierarchy by Wine and gets a menuId() // of wine-Programs-Steam-Steam.desktop. The weighing done by this function makes // sure the Linux native version gets mapped to the former, while other heuristics // map the Wine version reliably to the latter. // In lieu of this weighing we just used whatever KServiceTypeTrader returned first, // so what we do here can be no worse. auto sortServicesByMenuId = [](KService::List &services, const QString &key) { if (services.count() == 1) { return; } for (const auto service : services) { if (service->menuId().startsWith(key, Qt::CaseInsensitive)) { services.prepend(service); return; } } }; if (!(appId.isEmpty() && xWindowsWMClassName.isEmpty())) { // Check to see if this wmClass matched a saved one ... KConfigGroup grp(rulesConfig, "Mapping"); KConfigGroup set(rulesConfig, "Settings"); // Evaluate MatchCommandLineFirst directives from config first. // Some apps have different launchers depending upon command line ... QStringList matchCommandLineFirst = set.readEntry("MatchCommandLineFirst", QStringList()); if (!appId.isEmpty() && matchCommandLineFirst.contains(appId)) { triedPid = true; services = servicesFromPid(pid, rulesConfig); } // Try to match using xWindowsWMClassName also. if (!xWindowsWMClassName.isEmpty() && matchCommandLineFirst.contains("::"+xWindowsWMClassName)) { triedPid = true; services = servicesFromPid(pid, rulesConfig); } if (!appId.isEmpty()) { // Evaluate any mapping rules that map to a specific .desktop file. QString mapped(grp.readEntry(appId + "::" + xWindowsWMClassName, QString())); if (mapped.endsWith(QLatin1String(".desktop"))) { url = QUrl(mapped); return url; } if (mapped.isEmpty()) { mapped = grp.readEntry(appId, QString()); if (mapped.endsWith(QLatin1String(".desktop"))) { url = QUrl(mapped); return url; } } // Some apps, such as Wine, cannot use xWindowsWMClassName to map to launcher name - as Wine itself is not a GUI app // So, Settings/ManualOnly lists window classes where the user will always have to manualy set the launcher ... QStringList manualOnly = set.readEntry("ManualOnly", QStringList()); if (!appId.isEmpty() && manualOnly.contains(appId)) { return url; } // Try matching both appId and xWindowsWMClassName against StartupWMClass. // We do this before evaluating the mapping rules further, because StartupWMClass // is essentially a mapping rule, and we expect it to be set deliberately and // sensibly to instruct us what to do. Also, mapping rules // // StartupWMClass=STRING // // If true, it is KNOWN that the application will map at least one // window with the given string as its WM class or WM name hint. // // Source: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-0.1.txt if (services.isEmpty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(appId)); sortServicesByMenuId(services, appId); } if (services.isEmpty() && !xWindowsWMClassName.isEmpty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(xWindowsWMClassName)); sortServicesByMenuId(services, xWindowsWMClassName); } // Evaluate rewrite rules from config. if (services.isEmpty()) { KConfigGroup rewriteRulesGroup(rulesConfig, QStringLiteral("Rewrite Rules")); if (rewriteRulesGroup.hasGroup(appId)) { KConfigGroup rewriteGroup(&rewriteRulesGroup, appId); const QStringList &rules = rewriteGroup.groupList(); for (const QString &rule : rules) { KConfigGroup ruleGroup(&rewriteGroup, rule); const QString propertyConfig = ruleGroup.readEntry(QStringLiteral("Property"), QString()); QString matchProperty; if (propertyConfig == QLatin1String("ClassClass")) { matchProperty = appId; } else if (propertyConfig == QLatin1String("ClassName")) { matchProperty = xWindowsWMClassName; } if (matchProperty.isEmpty()) { continue; } const QString serviceSearchIdentifier = ruleGroup.readEntry(QStringLiteral("Identifier"), QString()); if (serviceSearchIdentifier.isEmpty()) { continue; } QRegularExpression regExp(ruleGroup.readEntry(QStringLiteral("Match"))); const auto match = regExp.match(matchProperty); if (match.hasMatch()) { const QString actualMatch = match.captured(QStringLiteral("match")); if (actualMatch.isEmpty()) { continue; } QString rewrittenString = ruleGroup.readEntry(QStringLiteral("Target")).arg(actualMatch); // If no "Target" is provided, instead assume the matched property (appId/xWindowsWMClassName). if (rewrittenString.isEmpty()) { rewrittenString = matchProperty; } services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ %2)").arg(rewrittenString, serviceSearchIdentifier)); sortServicesByMenuId(services, serviceSearchIdentifier); if (!services.isEmpty()) { break; } } } } } // The appId looks like a path. if (services.isEmpty() && appId.startsWith(QStringLiteral("/"))) { // Check if it's a path to a .desktop file. if (KDesktopFile::isDesktopFile(appId) && QFile::exists(appId)) { return QUrl::fromLocalFile(appId); } // Check if the appId passes as a .desktop file path if we add the extension. const QString appIdPlusExtension(appId + QStringLiteral(".desktop")); if (KDesktopFile::isDesktopFile(appIdPlusExtension) && QFile::exists(appIdPlusExtension)) { return QUrl::fromLocalFile(appIdPlusExtension); } } // Try matching mapped name against DesktopEntryName. if (!mapped.isEmpty() && services.isEmpty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName) and (not exist NoDisplay or not NoDisplay)").arg(mapped)); sortServicesByMenuId(services, mapped); } // Try matching mapped name against 'Name'. if (!mapped.isEmpty() && services.isEmpty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name) and (not exist NoDisplay or not NoDisplay)").arg(mapped)); sortServicesByMenuId(services, mapped); } // Try matching appId against DesktopEntryName. if (services.isEmpty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName) and (not exist NoDisplay or not NoDisplay)").arg(appId)); sortServicesByMenuId(services, appId); } // Try matching appId against 'Name'. // This has a shaky chance of success as appId is untranslated, but 'Name' may be localized. if (services.isEmpty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name) and (not exist NoDisplay or not NoDisplay)").arg(appId)); sortServicesByMenuId(services, appId); } // Check rules configuration for whether we want to hide this task. // Some window tasks update from bogus to useful metadata early during startup. // This config key allows listing the bogus metadata, and the matching window // tasks are hidden until they perform a metadate update that stops them from // matching. QStringList skipTaskbar = set.readEntry("SkipTaskbar", QStringList()); if (skipTaskbar.contains(appId)) { QUrlQuery query(url); query.addQueryItem(QStringLiteral("skipTaskbar"), QStringLiteral("true")); url.setQuery(query); } else if (skipTaskbar.contains(mapped)) { QUrlQuery query(url); query.addQueryItem(QStringLiteral("skipTaskbar"), QStringLiteral("true")); url.setQuery(query); } } // Ok, absolute *last* chance, try matching via pid (but only if we have not already tried this!) ... if (services.isEmpty() && !triedPid) { services = servicesFromPid(pid, rulesConfig); } } // Try to improve on a possible from-binary fallback. // If no services were found or we got a fake-service back from getServicesViaPid() // we attempt to improve on this by adding a loosely matched reverse-domain-name // DesktopEntryName. Namely anything that is '*.appId.desktop' would qualify here. // // Illustrative example of a case where the above heuristics would fail to produce // a reasonable result: // - org.kde.dragonplayer.desktop // - binary is 'dragon' // - qapp appname and thus appId is 'dragonplayer' // - appId cannot directly match the desktop file because of RDN // - appId also cannot match the binary because of name mismatch // - in the following code *.appId can match org.kde.dragonplayer though if (services.isEmpty() || services.at(0)->desktopEntryName().isEmpty()) { auto matchingServices = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' ~~ DesktopEntryName)").arg(appId)); QMutableListIterator it(matchingServices); while (it.hasNext()) { auto service = it.next(); if (!service->desktopEntryName().endsWith("." + appId)) { it.remove(); } } // Exactly one match is expected, otherwise we discard the results as to reduce // the likelihood of false-positive mappings. Since we essentially eliminate the // uniqueness that RDN is meant to bring to the table we could potentially end // up with more than one match here. if (matchingServices.length() == 1) { services = matchingServices; } } if (!services.isEmpty()) { const QString &menuId = services.at(0)->menuId(); // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (!menuId.isEmpty()) { url.setUrl(QStringLiteral("applications:") + menuId); return url; } QString path = services.at(0)->entryPath(); if (path.isEmpty()) { path = services.at(0)->exec(); } if (!path.isEmpty()) { QString query = url.query(); url = QUrl::fromLocalFile(path); url.setQuery(query); return url; } } return url; } KService::List servicesFromPid(quint32 pid, KSharedConfig::Ptr rulesConfig) { if (pid == 0) { return KService::List(); } if (!rulesConfig) { return KService::List(); } #if KF5_VERSION_MINOR >= 62 auto proc = KProcessList::processInfo(pid); if (!proc.isValid()) { return KService::List(); } const QString cmdLine = proc.command(); #else KSysGuard::Processes procs; procs.updateOrAddProcess(pid); KSysGuard::Process *proc = procs.getProcess(pid); const QString &cmdLine = proc ? proc->command().simplified() : QString(); // proc->command has a trailing space??? #endif if (cmdLine.isEmpty()) { return KService::List(); } #if KF5_VERSION_MINOR >= 62 return servicesFromCmdLine(cmdLine, proc.name(), rulesConfig); #else return servicesFromCmdLine(cmdLine, proc->name(), rulesConfig); #endif } KService::List servicesFromCmdLine(const QString &_cmdLine, const QString &processName, KSharedConfig::Ptr rulesConfig) { QString cmdLine = _cmdLine; KService::List services; if (!rulesConfig) { return services; } const int firstSpace = cmdLine.indexOf(' '); int slash = 0; services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine)); if (services.isEmpty()) { // Could not find with complete command line, so strip out the path part ... slash = cmdLine.lastIndexOf('/', firstSpace); if (slash > 0) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine.mid(slash + 1))); } } if (services.isEmpty() && firstSpace > 0) { // Could not find with arguments, so try without ... cmdLine = cmdLine.left(firstSpace); services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine)); if (services.isEmpty()) { slash = cmdLine.lastIndexOf('/'); if (slash > 0) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine.mid(slash + 1))); } } } if (services.isEmpty()) { KConfigGroup set(rulesConfig, "Settings"); const QStringList &runtimes = set.readEntry("TryIgnoreRuntimes", QStringList()); bool ignore = runtimes.contains(cmdLine); if (!ignore && slash > 0) { ignore = runtimes.contains(cmdLine.mid(slash + 1)); } if (ignore) { return servicesFromCmdLine(_cmdLine.mid(firstSpace + 1), processName, rulesConfig); } } if (services.isEmpty() && !processName.isEmpty() && !QStandardPaths::findExecutable(cmdLine).isEmpty()) { // cmdLine now exists without arguments if there were any. services << QExplicitlySharedDataPointer(new KService(processName, cmdLine, QString())); } return services; } QString defaultApplication(const QUrl &url) { if (url.scheme() != QLatin1String("preferred")) { return QString(); } const QString &application = url.host(); if (application.isEmpty()) { return QString(); } if (application.compare(QLatin1String("mailer"), Qt::CaseInsensitive) == 0) { KEMailSettings settings; // In KToolInvocation, the default is kmail; but let's be friendlier. QString command = settings.getSetting(KEMailSettings::ClientProgram); if (command.isEmpty()) { if (KService::Ptr kontact = KService::serviceByStorageId(QStringLiteral("kontact"))) { return kontact->storageId(); } else if (KService::Ptr kmail = KService::serviceByStorageId(QStringLiteral("kmail"))) { return kmail->storageId(); } } if (!command.isEmpty()) { if (settings.getSetting(KEMailSettings::ClientTerminal) == QLatin1String("true")) { KConfigGroup confGroup(KSharedConfig::openConfig(), "General"); const QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); command = preferredTerminal + QLatin1String(" -e ") + command; } return command; } } else if (application.compare(QLatin1String("browser"), Qt::CaseInsensitive) == 0) { KConfigGroup config(KSharedConfig::openConfig(), "General"); QString browserApp = config.readPathEntry("BrowserApplication", QString()); if (browserApp.isEmpty()) { const KService::Ptr htmlApp = KMimeTypeTrader::self()->preferredService(QStringLiteral("text/html")); if (htmlApp) { browserApp = htmlApp->storageId(); } } else if (browserApp.startsWith('!')) { browserApp = browserApp.mid(1); } return browserApp; } else if (application.compare(QLatin1String("terminal"), Qt::CaseInsensitive) == 0) { KConfigGroup confGroup(KSharedConfig::openConfig(), "General"); return confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); } else if (application.compare(QLatin1String("filemanager"), Qt::CaseInsensitive) == 0) { KService::Ptr service = KMimeTypeTrader::self()->preferredService(QStringLiteral("inode/directory")); if (service) { return service->storageId(); } } else if (KService::Ptr service = KMimeTypeTrader::self()->preferredService(application)) { return service->storageId(); } else { // Try the files in share/apps/kcm_componentchooser/*.desktop. QStringList directories = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kcm_componentchooser"), QStandardPaths::LocateDirectory); QStringList services; foreach(const QString& directory, directories) { QDir dir(directory); foreach(const QString& f, dir.entryList(QStringList("*.desktop"))) services += dir.absoluteFilePath(f); } foreach (const QString & service, services) { KConfig config(service, KConfig::SimpleConfig); KConfigGroup cg = config.group(QByteArray()); const QString type = cg.readEntry("valueName", QString()); if (type.compare(application, Qt::CaseInsensitive) == 0) { KConfig store(cg.readPathEntry("storeInFile", QStringLiteral("null"))); KConfigGroup storeCg(&store, cg.readEntry("valueSection", QString())); const QString exec = storeCg.readPathEntry(cg.readEntry("valueName", "kcm_componenchooser_null"), cg.readEntry("defaultImplementation", QString())); if (!exec.isEmpty()) { return exec; } break; } } } return QString(""); } } }