File indexing completed on 2024-05-05 17:45:02

0001 /*
0002     SPDX-FileCopyrightText: 2006 Aaron Seigo <aseigo@kde.org>
0003     SPDX-FileCopyrightText: 2014 Vishesh Handa <vhanda@kde.org>
0004     SPDX-FileCopyrightText: 2016-2020 Harald Sitter <sitter@kde.org>
0005     SPDX-FileCopyrightText: 2022 Alexander Lohnau <alexander.lohnau@gmx.de>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-only
0008 */
0009 
0010 #include "servicerunner.h"
0011 
0012 #include <algorithm>
0013 
0014 #include <QMimeData>
0015 
0016 #include <QDebug>
0017 #include <QDir>
0018 #include <QIcon>
0019 #include <QStandardPaths>
0020 #include <QUrl>
0021 #include <QUrlQuery>
0022 
0023 #include <KActivities/ResourceInstance>
0024 #include <KApplicationTrader>
0025 #include <KLocalizedString>
0026 #include <KNotificationJobUiDelegate>
0027 #include <KServiceAction>
0028 #include <KShell>
0029 #include <KStringHandler>
0030 #include <KSycoca>
0031 
0032 #include <KIO/ApplicationLauncherJob>
0033 #include <KIO/DesktopExecParser>
0034 
0035 #include "debug.h"
0036 
0037 namespace
0038 {
0039 int weightedLength(const QString &query)
0040 {
0041     return KStringHandler::logicalLength(query);
0042 }
0043 
0044 inline bool contains(const QString &result, const QStringList &queryList)
0045 {
0046     return std::all_of(queryList.cbegin(), queryList.cend(), [&result](const QString &query) {
0047         return result.contains(query, Qt::CaseInsensitive);
0048     });
0049 }
0050 
0051 inline bool contains(const QStringList &results, const QStringList &queryList)
0052 {
0053     return std::all_of(queryList.cbegin(), queryList.cend(), [&results](const QString &query) {
0054         return std::any_of(results.cbegin(), results.cend(), [&query](const QString &result) {
0055             return result.contains(query, Qt::CaseInsensitive);
0056         });
0057     });
0058 }
0059 
0060 } // namespace
0061 
0062 /**
0063  * @brief Finds all KServices for a given runner query
0064  */
0065 class ServiceFinder
0066 {
0067 public:
0068     ServiceFinder(ServiceRunner *runner, const QList<KService::Ptr> &list)
0069         : m_runner(runner)
0070         , m_services(list)
0071     {
0072     }
0073 
0074     void match(Plasma::RunnerContext &context)
0075     {
0076         term = context.query();
0077         // Splitting the query term to match using subsequences
0078         queryList = term.split(QLatin1Char(' '));
0079         weightedTermLength = weightedLength(term);
0080 
0081         matchNameKeywordAndGenericName();
0082         matchCategories();
0083         matchJumpListActions();
0084 
0085         context.addMatches(matches);
0086     }
0087 
0088 private:
0089     void seen(const KService::Ptr &service)
0090     {
0091         m_seen.insert(service->storageId());
0092         m_seen.insert(service->exec());
0093     }
0094 
0095     void seen(const KServiceAction &action)
0096     {
0097         m_seen.insert(action.exec());
0098     }
0099 
0100     bool hasSeen(const KService::Ptr &service)
0101     {
0102         return m_seen.contains(service->storageId()) && m_seen.contains(service->exec());
0103     }
0104 
0105     bool hasSeen(const KServiceAction &action)
0106     {
0107         return m_seen.contains(action.exec());
0108     }
0109 
0110     bool disqualify(const KService::Ptr &service)
0111     {
0112         auto ret = hasSeen(service) || service->noDisplay();
0113         qCDebug(RUNNER_SERVICES) << service->name() << "disqualified?" << ret;
0114         seen(service);
0115         return ret;
0116     }
0117 
0118     qreal increaseMatchRelavance(const KService::Ptr &service, const QStringList &strList, const QString &category)
0119     {
0120         // Increment the relevance based on all the words (other than the first) of the query list
0121         qreal relevanceIncrement = 0;
0122 
0123         for (int i = 1; i < strList.size(); ++i) {
0124             const auto &str = strList.at(i);
0125             if (category == QLatin1String("Name")) {
0126                 if (service->name().contains(str, Qt::CaseInsensitive)) {
0127                     relevanceIncrement += 0.01;
0128                 }
0129             } else if (category == QLatin1String("GenericName")) {
0130                 if (service->genericName().contains(str, Qt::CaseInsensitive)) {
0131                     relevanceIncrement += 0.01;
0132                 }
0133             } else if (category == QLatin1String("Exec")) {
0134                 if (service->exec().contains(str, Qt::CaseInsensitive)) {
0135                     relevanceIncrement += 0.01;
0136                 }
0137             } else if (category == QLatin1String("Comment")) {
0138                 if (service->comment().contains(str, Qt::CaseInsensitive)) {
0139                     relevanceIncrement += 0.01;
0140                 }
0141             }
0142         }
0143 
0144         return relevanceIncrement;
0145     }
0146 
0147     void setupMatch(const KService::Ptr &service, Plasma::QueryMatch &match)
0148     {
0149         const QString name = service->name();
0150 
0151         match.setText(name);
0152 
0153         QUrl url(service->storageId());
0154         url.setScheme(QStringLiteral("applications"));
0155         match.setData(url);
0156 
0157         QString path = service->entryPath();
0158         if (!QDir::isAbsolutePath(path)) {
0159             path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kservices5/") + path);
0160         }
0161 
0162         match.setUrls({QUrl::fromLocalFile(path)});
0163 
0164         QString exec = service->exec();
0165 
0166         QStringList resultingArgs = KIO::DesktopExecParser(KService(QString(), exec, QString()), {}).resultingArguments();
0167 
0168         // Remove any environment variables.
0169         if (KIO::DesktopExecParser::executableName(exec) == QLatin1String("env")) {
0170             resultingArgs.removeFirst(); // remove "env".
0171 
0172             while (!resultingArgs.isEmpty() && resultingArgs.first().contains(QLatin1Char('='))) {
0173                 resultingArgs.removeFirst();
0174             }
0175 
0176             // Now parse it again to resolve the path.
0177             resultingArgs = KIO::DesktopExecParser(KService(QString(), KShell::joinArgs(resultingArgs), QString()), {}).resultingArguments();
0178         }
0179 
0180         // Remove special arguments that have no real impact on the application.
0181         static const auto specialArgs = {QStringLiteral("-qwindowtitle"), QStringLiteral("-qwindowicon"), QStringLiteral("--started-from-file")};
0182 
0183         for (const auto &specialArg : specialArgs) {
0184             int index = resultingArgs.indexOf(specialArg);
0185             if (index > -1) {
0186                 if (resultingArgs.count() > index) {
0187                     resultingArgs.removeAt(index);
0188                 }
0189                 if (resultingArgs.count() > index) {
0190                     resultingArgs.removeAt(index); // remove value, too, if any.
0191                 }
0192             }
0193         }
0194 
0195         match.setId(QStringLiteral("exec://") + resultingArgs.join(QLatin1Char(' ')));
0196         if (!service->genericName().isEmpty() && service->genericName() != name) {
0197             match.setSubtext(service->genericName());
0198         } else if (!service->comment().isEmpty()) {
0199             match.setSubtext(service->comment());
0200         }
0201 
0202         if (!service->icon().isEmpty()) {
0203             match.setIconName(service->icon());
0204         }
0205     }
0206 
0207     void matchNameKeywordAndGenericName()
0208     {
0209         const auto nameKeywordAndGenericNameFilter = [this](const KService::Ptr &service) {
0210             // Name
0211             if (contains(service->name(), queryList)) {
0212                 return true;
0213             }
0214             // If the term length is < 3, no real point searching the Keywords and GenericName
0215             if (weightedTermLength < 3) {
0216                 return false;
0217             }
0218             // Keywords
0219             if (contains(service->keywords(), queryList)) {
0220                 return true;
0221             }
0222             // GenericName
0223             if (contains(service->genericName(), queryList) || contains(service->untranslatedGenericName(), queryList)) {
0224                 return true;
0225             }
0226             // Comment
0227             if (contains(service->comment(), queryList)) {
0228                 return true;
0229             }
0230 
0231             return false;
0232         };
0233 
0234         for (const KService::Ptr &service : m_services) {
0235             if (!nameKeywordAndGenericNameFilter(service) || disqualify(service)) {
0236                 continue;
0237             }
0238 
0239             const QString id = service->storageId();
0240             const QString name = service->name();
0241             const QString exec = service->exec();
0242 
0243             Plasma::QueryMatch match(m_runner);
0244             match.setType(Plasma::QueryMatch::PossibleMatch);
0245             setupMatch(service, match);
0246             qreal relevance(0.6);
0247 
0248             // If the term was < 3 chars and NOT at the beginning of the App's name or Exec, then
0249             // chances are the user doesn't want that app.
0250             if (weightedTermLength < 3) {
0251                 if (name.startsWith(term, Qt::CaseInsensitive) || exec.startsWith(term, Qt::CaseInsensitive)) {
0252                     relevance = 0.9;
0253                 } else {
0254                     continue;
0255                 }
0256             } else if (name.compare(term, Qt::CaseInsensitive) == 0) {
0257                 relevance = 1;
0258                 match.setType(Plasma::QueryMatch::ExactMatch);
0259             } else if (name.contains(queryList[0], Qt::CaseInsensitive)) {
0260                 relevance = 0.8;
0261                 relevance += increaseMatchRelavance(service, queryList, QStringLiteral("Name"));
0262 
0263                 if (name.startsWith(queryList[0], Qt::CaseInsensitive)) {
0264                     relevance += 0.1;
0265                 }
0266             } else if (service->genericName().contains(queryList[0], Qt::CaseInsensitive)) {
0267                 relevance = 0.65;
0268                 relevance += increaseMatchRelavance(service, queryList, QStringLiteral("GenericName"));
0269 
0270                 if (service->genericName().startsWith(queryList[0], Qt::CaseInsensitive)) {
0271                     relevance += 0.05;
0272                 }
0273             } else if (service->comment().contains(queryList[0], Qt::CaseInsensitive)) {
0274                 relevance = 0.5;
0275                 relevance += increaseMatchRelavance(service, queryList, QStringLiteral("Comment"));
0276 
0277                 if (service->comment().startsWith(queryList[0], Qt::CaseInsensitive)) {
0278                     relevance += 0.05;
0279                 }
0280             }
0281 
0282             if (service->categories().contains(QLatin1String("KDE"))) {
0283                 qCDebug(RUNNER_SERVICES) << "found a kde thing" << id << match.subtext() << relevance;
0284                 relevance += .09;
0285             }
0286 
0287             qCDebug(RUNNER_SERVICES) << name << "is this relevant:" << relevance;
0288             match.setRelevance(relevance);
0289 
0290             matches << match;
0291         }
0292     }
0293 
0294     void matchCategories()
0295     {
0296         // Do not match categories for short queries, BUG: 469769
0297         if (weightedTermLength < 5) {
0298             return;
0299         }
0300         const auto categoriesFilter = [this](const KService::Ptr &service) {
0301             return contains(service->categories(), queryList);
0302         };
0303 
0304         for (const KService::Ptr &service : m_services) {
0305             if (!categoriesFilter(service) || disqualify(service)) {
0306                 continue;
0307             }
0308             qCDebug(RUNNER_SERVICES) << service->name() << "is an exact match!" << service->storageId() << service->exec();
0309 
0310             Plasma::QueryMatch match(m_runner);
0311             match.setType(Plasma::QueryMatch::PossibleMatch);
0312             setupMatch(service, match);
0313 
0314             qreal relevance = 0.4;
0315             const QStringList categories = service->categories();
0316             if (std::any_of(categories.begin(), categories.end(), [this](const QString &category) {
0317                     return category.compare(term, Qt::CaseInsensitive) == 0;
0318                 })) {
0319                 relevance = 0.6;
0320             } else if (service->categories().contains(QLatin1String("X-KDE-More")) || !service->showInCurrentDesktop()) {
0321                 relevance = 0.5;
0322             }
0323 
0324             if (service->isApplication()) {
0325                 relevance += .04;
0326             }
0327 
0328             match.setRelevance(relevance);
0329             matches << match;
0330         }
0331     }
0332 
0333     void matchJumpListActions()
0334     {
0335         if (weightedTermLength < 3) {
0336             return;
0337         }
0338 
0339         const auto hasActionsFilter = [](const KService::Ptr &service) {
0340             return !service->actions().isEmpty();
0341         };
0342         for (const KService::Ptr &service : m_services) {
0343             if (!hasActionsFilter(service) || service->noDisplay()) {
0344                 continue;
0345             }
0346 
0347             // Skip SystemSettings as we find KCMs already
0348             if (service->storageId() == QLatin1String("systemsettings.desktop")) {
0349                 continue;
0350             }
0351 
0352             const auto actions = service->actions();
0353             for (const KServiceAction &action : actions) {
0354                 if (action.text().isEmpty() || action.exec().isEmpty() || hasSeen(action)) {
0355                     continue;
0356                 }
0357                 seen(action);
0358 
0359                 const int matchIndex = action.text().indexOf(term, 0, Qt::CaseInsensitive);
0360                 if (matchIndex < 0) {
0361                     continue;
0362                 }
0363 
0364                 Plasma::QueryMatch match(m_runner);
0365                 match.setType(Plasma::QueryMatch::PossibleMatch);
0366                 if (!action.icon().isEmpty()) {
0367                     match.setIconName(action.icon());
0368                 } else {
0369                     match.setIconName(service->icon());
0370                 }
0371                 match.setText(i18nc("Jump list search result, %1 is action (eg. open new tab), %2 is application (eg. browser)",
0372                                     "%1 - %2",
0373                                     action.text(),
0374                                     service->name()));
0375 
0376                 QUrl url(service->storageId());
0377                 url.setScheme(QStringLiteral("applications"));
0378 
0379                 QUrlQuery query;
0380                 query.addQueryItem(QStringLiteral("action"), action.name());
0381                 url.setQuery(query);
0382 
0383                 match.setData(url);
0384 
0385                 qreal relevance = 0.5;
0386                 if (action.text().compare(term, Qt::CaseInsensitive) == 0) {
0387                     relevance = 0.65;
0388                     match.setType(Plasma::QueryMatch::HelperMatch); // Give it a higer match type to ensure it is shown, BUG: 455436
0389                 } else if (matchIndex == 0) {
0390                     relevance += 0.05;
0391                 }
0392 
0393                 match.setRelevance(relevance);
0394 
0395                 matches << match;
0396             }
0397         }
0398     }
0399 
0400     ServiceRunner *m_runner;
0401     QSet<QString> m_seen;
0402     const QList<KService::Ptr> m_services;
0403 
0404     QList<Plasma::QueryMatch> matches;
0405     QString query;
0406     QString term;
0407     QStringList queryList;
0408     int weightedTermLength = -1;
0409 };
0410 
0411 ServiceRunner::ServiceRunner(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args)
0412     : Plasma::AbstractRunner(parent, metaData, args)
0413 {
0414     setObjectName(QStringLiteral("Application"));
0415     setPriority(AbstractRunner::HighestPriority);
0416 
0417     addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), i18n("Finds applications whose name or description match :q:")));
0418 }
0419 
0420 ServiceRunner::~ServiceRunner() = default;
0421 
0422 void ServiceRunner::match(Plasma::RunnerContext &context)
0423 {
0424     KSycoca::disableAutoRebuild();
0425     ServiceFinder finder(this, KApplicationTrader::query([](const KService::Ptr &) {
0426                              return true;
0427                          }));
0428     finder.match(context);
0429 }
0430 
0431 void ServiceRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match)
0432 {
0433     Q_UNUSED(context)
0434 
0435     const QUrl dataUrl = match.data().toUrl();
0436 
0437     KService::Ptr service = KService::serviceByStorageId(dataUrl.path());
0438     if (!service) {
0439         return;
0440     }
0441 
0442     KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + service->storageId()), QStringLiteral("org.kde.krunner"));
0443 
0444     KIO::ApplicationLauncherJob *job = nullptr;
0445 
0446     const QString actionName = QUrlQuery(dataUrl).queryItemValue(QStringLiteral("action"));
0447     if (actionName.isEmpty()) {
0448         job = new KIO::ApplicationLauncherJob(service);
0449     } else {
0450         const auto actions = service->actions();
0451         auto it = std::find_if(actions.begin(), actions.end(), [&actionName](const KServiceAction &action) {
0452             return action.name() == actionName;
0453         });
0454         Q_ASSERT(it != actions.end());
0455 
0456         job = new KIO::ApplicationLauncherJob(*it);
0457     }
0458 
0459     auto *delegate = new KNotificationJobUiDelegate;
0460     delegate->setAutoErrorHandlingEnabled(true);
0461     job->setUiDelegate(delegate);
0462     job->start();
0463 }