File indexing completed on 2024-12-01 03:41:14

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 1998-2009 David Faure <faure@kde.org>
0004     SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0007 */
0008 
0009 #include "kfileitemactions.h"
0010 #include "kfileitemactions_p.h"
0011 #include <KAbstractFileItemActionPlugin>
0012 #include <KApplicationTrader>
0013 #include <KAuthorized>
0014 #include <KConfigGroup>
0015 #include <KDesktopFile>
0016 #include <KDesktopFileAction>
0017 #include <KFileUtils>
0018 #include <KIO/ApplicationLauncherJob>
0019 #include <KIO/JobUiDelegate>
0020 #include <KLocalizedString>
0021 #include <KPluginFactory>
0022 #include <KPluginMetaData>
0023 #include <KSandbox>
0024 #include <jobuidelegatefactory.h>
0025 #include <kapplicationtrader.h>
0026 #include <kdirnotify.h>
0027 #include <kurlauthorized.h>
0028 
0029 #include <QFile>
0030 #include <QMenu>
0031 #include <QMimeDatabase>
0032 #include <QtAlgorithms>
0033 
0034 #ifndef KIO_ANDROID_STUB
0035 #include <QDBusConnection>
0036 #include <QDBusConnectionInterface>
0037 #include <QDBusInterface>
0038 #include <QDBusMessage>
0039 #endif
0040 #include <algorithm>
0041 #include <kio_widgets_debug.h>
0042 #include <set>
0043 
0044 static bool KIOSKAuthorizedAction(const KConfigGroup &cfg)
0045 {
0046     const QStringList list = cfg.readEntry("X-KDE-AuthorizeAction", QStringList());
0047     return std::all_of(list.constBegin(), list.constEnd(), [](const QString &action) {
0048         return KAuthorized::authorize(action.trimmed());
0049     });
0050 }
0051 
0052 static bool mimeTypeListContains(const QStringList &list, const KFileItem &item)
0053 {
0054     const QString itemMimeType = item.mimetype();
0055     return std::any_of(list.cbegin(), list.cend(), [&](const QString &mt) {
0056         if (mt == itemMimeType || mt == QLatin1String("all/all")) {
0057             return true;
0058         }
0059 
0060         if (item.isFile() //
0061             && (mt == QLatin1String("allfiles") || mt == QLatin1String("all/allfiles") || mt == QLatin1String("application/octet-stream"))) {
0062             return true;
0063         }
0064 
0065         if (item.currentMimeType().inherits(mt)) {
0066             return true;
0067         }
0068 
0069         if (mt.endsWith(QLatin1String("/*"))) {
0070             const int slashPos = mt.indexOf(QLatin1Char('/'));
0071             const auto topLevelType = QStringView(mt).mid(0, slashPos);
0072             return itemMimeType.startsWith(topLevelType);
0073         }
0074         return false;
0075     });
0076 }
0077 
0078 // This helper class stores the .desktop-file actions and the servicemenus
0079 // in order to support X-KDE-Priority and X-KDE-Submenu.
0080 namespace KIO
0081 {
0082 class PopupServices
0083 {
0084 public:
0085     ServiceList &selectList(const QString &priority, const QString &submenuName);
0086 
0087     ServiceList user;
0088     ServiceList userToplevel;
0089     ServiceList userPriority;
0090 
0091     QMap<QString, ServiceList> userSubmenus;
0092     QMap<QString, ServiceList> userToplevelSubmenus;
0093     QMap<QString, ServiceList> userPrioritySubmenus;
0094 };
0095 
0096 ServiceList &PopupServices::selectList(const QString &priority, const QString &submenuName)
0097 {
0098     // we use the categories .desktop entry to define submenus
0099     // if none is defined, we just pop it in the main menu
0100     if (submenuName.isEmpty()) {
0101         if (priority == QLatin1String("TopLevel")) {
0102             return userToplevel;
0103         } else if (priority == QLatin1String("Important")) {
0104             return userPriority;
0105         }
0106     } else if (priority == QLatin1String("TopLevel")) {
0107         return userToplevelSubmenus[submenuName];
0108     } else if (priority == QLatin1String("Important")) {
0109         return userPrioritySubmenus[submenuName];
0110     } else {
0111         return userSubmenus[submenuName];
0112     }
0113     return user;
0114 }
0115 } // namespace
0116 
0117 ////
0118 
0119 KFileItemActionsPrivate::KFileItemActionsPrivate(KFileItemActions *qq)
0120     : QObject()
0121     , q(qq)
0122     , m_executeServiceActionGroup(static_cast<QWidget *>(nullptr))
0123     , m_runApplicationActionGroup(static_cast<QWidget *>(nullptr))
0124     , m_parentWidget(nullptr)
0125     , m_config(QStringLiteral("kservicemenurc"), KConfig::NoGlobals)
0126 {
0127     QObject::connect(&m_executeServiceActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotExecuteService);
0128     QObject::connect(&m_runApplicationActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotRunApplication);
0129 }
0130 
0131 KFileItemActionsPrivate::~KFileItemActionsPrivate()
0132 {
0133 }
0134 
0135 int KFileItemActionsPrivate::insertServicesSubmenus(const QMap<QString, ServiceList> &submenus, QMenu *menu)
0136 {
0137     int count = 0;
0138     QMap<QString, ServiceList>::ConstIterator it;
0139     for (it = submenus.begin(); it != submenus.end(); ++it) {
0140         if (it.value().isEmpty()) {
0141             // avoid empty sub-menus
0142             continue;
0143         }
0144 
0145         QMenu *actionSubmenu = new QMenu(menu);
0146         const int servicesAddedCount = insertServices(it.value(), actionSubmenu);
0147 
0148         if (servicesAddedCount > 0) {
0149             count += servicesAddedCount;
0150             actionSubmenu->setTitle(it.key());
0151             actionSubmenu->setIcon(QIcon::fromTheme(it.value().first().icon()));
0152             actionSubmenu->menuAction()->setObjectName(QStringLiteral("services_submenu")); // for the unittest
0153             menu->addMenu(actionSubmenu);
0154         } else {
0155             // avoid empty sub-menus
0156             delete actionSubmenu;
0157         }
0158     }
0159 
0160     return count;
0161 }
0162 
0163 int KFileItemActionsPrivate::insertServices(const ServiceList &list, QMenu *menu)
0164 {
0165     ServiceList sortedList = list;
0166     std::sort(sortedList.begin(), sortedList.end(), [](const KDesktopFileAction &a1, const KDesktopFileAction &a2) {
0167         return a1.name() < a2.name();
0168     });
0169     int count = 0;
0170     for (const KDesktopFileAction &serviceAction : std::as_const(sortedList)) {
0171         if (serviceAction.isSeparator()) {
0172             const QList<QAction *> actions = menu->actions();
0173             if (!actions.isEmpty() && !actions.last()->isSeparator()) {
0174                 menu->addSeparator();
0175             }
0176             continue;
0177         }
0178 
0179         QAction *act = new QAction(q);
0180         act->setObjectName(QStringLiteral("menuaction")); // for the unittest
0181         QString text = serviceAction.name();
0182         text.replace(QLatin1Char('&'), QLatin1String("&&"));
0183         act->setText(text);
0184         if (!serviceAction.icon().isEmpty()) {
0185             act->setIcon(QIcon::fromTheme(serviceAction.icon()));
0186         }
0187         act->setData(QVariant::fromValue(serviceAction));
0188         m_executeServiceActionGroup.addAction(act);
0189 
0190         menu->addAction(act); // Add to toplevel menu
0191         ++count;
0192     }
0193 
0194     return count;
0195 }
0196 
0197 void KFileItemActionsPrivate::slotExecuteService(QAction *act)
0198 {
0199     const KDesktopFileAction serviceAction = act->data().value<KDesktopFileAction>();
0200     if (KAuthorized::authorizeAction(serviceAction.name())) {
0201         auto *job = new KIO::ApplicationLauncherJob(serviceAction);
0202         job->setUrls(m_props.urlList());
0203         job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0204         job->start();
0205     }
0206 }
0207 
0208 KFileItemActions::KFileItemActions(QObject *parent)
0209     : QObject(parent)
0210     , d(new KFileItemActionsPrivate(this))
0211 {
0212 }
0213 
0214 KFileItemActions::~KFileItemActions() = default;
0215 
0216 void KFileItemActions::setItemListProperties(const KFileItemListProperties &itemListProperties)
0217 {
0218     d->m_props = itemListProperties;
0219 
0220     d->m_mimeTypeList.clear();
0221     const KFileItemList items = d->m_props.items();
0222     for (const KFileItem &item : items) {
0223         if (!d->m_mimeTypeList.contains(item.mimetype())) {
0224             d->m_mimeTypeList << item.mimetype();
0225         }
0226     }
0227 }
0228 
0229 void KFileItemActions::addActionsTo(QMenu *menu, MenuActionSources sources, const QList<QAction *> &additionalActions, const QStringList &excludeList)
0230 {
0231     QMenu *actionsMenu = menu;
0232     if (sources & MenuActionSource::Services) {
0233         actionsMenu = d->addServiceActionsTo(menu, additionalActions, excludeList).menu;
0234     } else {
0235         // Since we didn't call addServiceActionsTo(), we have to add additional actions manually
0236         for (QAction *action : additionalActions) {
0237             actionsMenu->addAction(action);
0238         }
0239     }
0240     if (sources & MenuActionSource::Plugins) {
0241         d->addPluginActionsTo(menu, actionsMenu, excludeList);
0242     }
0243 }
0244 
0245 // static
0246 KService::List KFileItemActions::associatedApplications(const QStringList &mimeTypeList)
0247 {
0248     return KFileItemActionsPrivate::associatedApplications(mimeTypeList, QString(), QStringList{});
0249 }
0250 
0251 static KService::Ptr preferredService(const QString &mimeType, const QStringList &excludedDesktopEntryNames, const QString &constraint)
0252 {
0253     KService::List services;
0254     if (constraint.isEmpty()) {
0255         services = KApplicationTrader::queryByMimeType(mimeType, [&](const KService::Ptr &serv) {
0256             return !excludedDesktopEntryNames.contains(serv->desktopEntryName());
0257         });
0258     }
0259     return services.isEmpty() ? KService::Ptr() : services.first();
0260 }
0261 
0262 void KFileItemActions::insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QStringList &excludedDesktopEntryNames)
0263 {
0264     d->insertOpenWithActionsTo(before, topMenu, excludedDesktopEntryNames, QString());
0265 }
0266 
0267 void KFileItemActionsPrivate::slotRunPreferredApplications()
0268 {
0269     const KFileItemList fileItems = m_fileOpenList;
0270     const QStringList mimeTypeList = listMimeTypes(fileItems);
0271     const QStringList serviceIdList = listPreferredServiceIds(mimeTypeList, QStringList(), m_traderConstraint);
0272 
0273     for (const QString &serviceId : serviceIdList) {
0274         KFileItemList serviceItems;
0275         for (const KFileItem &item : fileItems) {
0276             const KService::Ptr serv = preferredService(item.mimetype(), QStringList(), m_traderConstraint);
0277             const QString preferredServiceId = serv ? serv->storageId() : QString();
0278             if (preferredServiceId == serviceId) {
0279                 serviceItems << item;
0280             }
0281         }
0282 
0283         if (serviceId.isEmpty()) { // empty means: no associated app for this MIME type
0284             openWithByMime(serviceItems);
0285             continue;
0286         }
0287 
0288         const KService::Ptr servicePtr = KService::serviceByStorageId(serviceId); // can be nullptr
0289         auto *job = new KIO::ApplicationLauncherJob(servicePtr);
0290         job->setUrls(serviceItems.urlList());
0291         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0292         job->start();
0293     }
0294 }
0295 
0296 void KFileItemActions::runPreferredApplications(const KFileItemList &fileOpenList)
0297 {
0298     d->m_fileOpenList = fileOpenList;
0299     d->m_traderConstraint = QString();
0300     d->slotRunPreferredApplications();
0301 }
0302 
0303 void KFileItemActionsPrivate::openWithByMime(const KFileItemList &fileItems)
0304 {
0305     const QStringList mimeTypeList = listMimeTypes(fileItems);
0306     for (const QString &mimeType : mimeTypeList) {
0307         KFileItemList mimeItems;
0308         for (const KFileItem &item : fileItems) {
0309             if (item.mimetype() == mimeType) {
0310                 mimeItems << item;
0311             }
0312         }
0313         // Show Open With dialog
0314         auto *job = new KIO::ApplicationLauncherJob();
0315         job->setUrls(mimeItems.urlList());
0316         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0317         job->start();
0318     }
0319 }
0320 
0321 void KFileItemActionsPrivate::slotRunApplication(QAction *act)
0322 {
0323     // Is it an application, from one of the "Open With" actions?
0324     KService::Ptr app = act->data().value<KService::Ptr>();
0325     Q_ASSERT(app);
0326     auto *job = new KIO::ApplicationLauncherJob(app);
0327     job->setUrls(m_props.urlList());
0328     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0329     job->start();
0330 }
0331 
0332 void KFileItemActionsPrivate::slotOpenWithDialog()
0333 {
0334     // The item 'Other...' or 'Open With...' has been selected
0335     Q_EMIT q->openWithDialogAboutToBeShown();
0336     auto *job = new KIO::ApplicationLauncherJob();
0337     job->setUrls(m_props.urlList());
0338     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0339     job->start();
0340 }
0341 
0342 QStringList KFileItemActionsPrivate::listMimeTypes(const KFileItemList &items)
0343 {
0344     QStringList mimeTypeList;
0345     for (const KFileItem &item : items) {
0346         if (!mimeTypeList.contains(item.mimetype())) {
0347             mimeTypeList << item.mimetype();
0348         }
0349     }
0350     return mimeTypeList;
0351 }
0352 
0353 QStringList
0354 KFileItemActionsPrivate::listPreferredServiceIds(const QStringList &mimeTypeList, const QStringList &excludedDesktopEntryNames, const QString &traderConstraint)
0355 {
0356     QStringList serviceIdList;
0357     serviceIdList.reserve(mimeTypeList.size());
0358     for (const QString &mimeType : mimeTypeList) {
0359         const KService::Ptr serv = preferredService(mimeType, excludedDesktopEntryNames, traderConstraint);
0360         serviceIdList << (serv ? serv->storageId() : QString()); // empty string means mimetype has no associated apps
0361     }
0362     serviceIdList.removeDuplicates();
0363     return serviceIdList;
0364 }
0365 
0366 QAction *KFileItemActionsPrivate::createAppAction(const KService::Ptr &service, bool singleOffer)
0367 {
0368     QString actionName(service->name().replace(QLatin1Char('&'), QLatin1String("&&")));
0369     if (singleOffer) {
0370         actionName = i18n("Open &with %1", actionName);
0371     } else {
0372         actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName);
0373     }
0374 
0375     QAction *act = new QAction(q);
0376     act->setObjectName(QStringLiteral("openwith")); // for the unittest
0377     act->setIcon(QIcon::fromTheme(service->icon()));
0378     act->setText(actionName);
0379     act->setData(QVariant::fromValue(service));
0380     m_runApplicationActionGroup.addAction(act);
0381     return act;
0382 }
0383 
0384 bool KFileItemActionsPrivate::shouldDisplayServiceMenu(const KConfigGroup &cfg, const QString &protocol) const
0385 {
0386     const QList<QUrl> urlList = m_props.urlList();
0387     if (!KIOSKAuthorizedAction(cfg)) {
0388         return false;
0389     }
0390     if (cfg.hasKey("X-KDE-Protocol")) {
0391         const QString theProtocol = cfg.readEntry("X-KDE-Protocol");
0392         if (theProtocol.startsWith(QLatin1Char('!'))) { // Is it excluded?
0393             if (QStringView(theProtocol).mid(1) == protocol) {
0394                 return false;
0395             }
0396         } else if (protocol != theProtocol) {
0397             return false;
0398         }
0399     } else if (cfg.hasKey("X-KDE-Protocols")) {
0400         const QStringList protocols = cfg.readEntry("X-KDE-Protocols", QStringList());
0401         if (!protocols.contains(protocol)) {
0402             return false;
0403         }
0404     } else if (protocol == QLatin1String("trash")) {
0405         // Require servicemenus for the trash to ask for protocol=trash explicitly.
0406         // Trashed files aren't supposed to be available for actions.
0407         // One might want a servicemenu for trash.desktop itself though.
0408         return false;
0409     }
0410 
0411     const auto requiredNumbers = cfg.readEntry("X-KDE-RequiredNumberOfUrls", QList<int>());
0412     if (!requiredNumbers.isEmpty() && !requiredNumbers.contains(urlList.count())) {
0413         return false;
0414     }
0415     if (cfg.hasKey("X-KDE-MinNumberOfUrls")) {
0416         const int minNumber = cfg.readEntry("X-KDE-MinNumberOfUrls").toInt();
0417         if (urlList.count() < minNumber) {
0418             return false;
0419         }
0420     }
0421     if (cfg.hasKey("X-KDE-MaxNumberOfUrls")) {
0422         const int maxNumber = cfg.readEntry("X-KDE-MaxNumberOfUrls").toInt();
0423         if (urlList.count() > maxNumber) {
0424             return false;
0425         }
0426     }
0427     return true;
0428 }
0429 
0430 bool KFileItemActionsPrivate::checkTypesMatch(const KConfigGroup &cfg) const
0431 {
0432     const QStringList types = cfg.readXdgListEntry("MimeType");
0433     if (types.isEmpty()) {
0434         return false;
0435     }
0436 
0437     const QStringList excludeTypes = cfg.readEntry("ExcludeServiceTypes", QStringList());
0438     const KFileItemList items = m_props.items();
0439     return std::all_of(items.constBegin(), items.constEnd(), [&types, &excludeTypes](const KFileItem &i) {
0440         return mimeTypeListContains(types, i) && !mimeTypeListContains(excludeTypes, i);
0441     });
0442 }
0443 
0444 KFileItemActionsPrivate::ServiceActionInfo
0445 KFileItemActionsPrivate::addServiceActionsTo(QMenu *mainMenu, const QList<QAction *> &additionalActions, const QStringList &excludeList)
0446 {
0447     const KFileItemList items = m_props.items();
0448     const KFileItem &firstItem = items.first();
0449     const QString protocol = firstItem.url().scheme(); // assumed to be the same for all items
0450     const bool isLocal = !firstItem.localPath().isEmpty();
0451 
0452     KIO::PopupServices s;
0453 
0454     // 2 - Look for "servicemenus" bindings (user-defined services)
0455 
0456     // first check the .directory if this is a directory
0457     const bool isSingleLocal = items.count() == 1 && isLocal;
0458     if (m_props.isDirectory() && isSingleLocal) {
0459         const QString dotDirectoryFile = QUrl::fromLocalFile(firstItem.localPath()).path().append(QLatin1String("/.directory"));
0460         if (QFile::exists(dotDirectoryFile)) {
0461             const KDesktopFile desktopFile(dotDirectoryFile);
0462             const KConfigGroup cfg = desktopFile.desktopGroup();
0463 
0464             if (KIOSKAuthorizedAction(cfg)) {
0465                 const QString priority = cfg.readEntry("X-KDE-Priority");
0466                 const QString submenuName = cfg.readEntry("X-KDE-Submenu");
0467                 ServiceList &list = s.selectList(priority, submenuName);
0468                 list += desktopFile.actions();
0469             }
0470         }
0471     }
0472 
0473     const KConfigGroup showGroup = m_config.group(QStringLiteral("Show"));
0474 
0475     const QMimeDatabase db;
0476     const QStringList files = serviceMenuFilePaths();
0477     for (const QString &file : files) {
0478         const KDesktopFile desktopFile(file);
0479         const KConfigGroup cfg = desktopFile.desktopGroup();
0480         if (!shouldDisplayServiceMenu(cfg, protocol)) {
0481             continue;
0482         }
0483 
0484         const QList<KDesktopFileAction> actions = desktopFile.actions();
0485         if (!actions.isEmpty()) {
0486             if (!checkTypesMatch(cfg)) {
0487                 continue;
0488             }
0489 
0490             const QString priority = cfg.readEntry("X-KDE-Priority");
0491             const QString submenuName = cfg.readEntry("X-KDE-Submenu");
0492 
0493             ServiceList &list = s.selectList(priority, submenuName);
0494             std::copy_if(actions.cbegin(), actions.cend(), std::back_inserter(list), [&excludeList, &showGroup](const KDesktopFileAction &srvAction) {
0495                 return showGroup.readEntry(srvAction.name(), true) && !excludeList.contains(srvAction.name());
0496             });
0497         }
0498     }
0499 
0500     QMenu *actionMenu = mainMenu;
0501     int userItemCount = 0;
0502     if (s.user.count() + s.userSubmenus.count() + s.userPriority.count() + s.userPrioritySubmenus.count() + additionalActions.count() > 3) {
0503         // we have more than three items, so let's make a submenu
0504         actionMenu = new QMenu(i18nc("@title:menu", "&Actions"), mainMenu);
0505         actionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic")));
0506         actionMenu->menuAction()->setObjectName(QStringLiteral("actions_submenu")); // for the unittest
0507         mainMenu->addMenu(actionMenu);
0508     }
0509 
0510     userItemCount += additionalActions.count();
0511     for (QAction *action : additionalActions) {
0512         actionMenu->addAction(action);
0513     }
0514     userItemCount += insertServicesSubmenus(s.userPrioritySubmenus, actionMenu);
0515     userItemCount += insertServices(s.userPriority, actionMenu);
0516     userItemCount += insertServicesSubmenus(s.userSubmenus, actionMenu);
0517     userItemCount += insertServices(s.user, actionMenu);
0518 
0519     userItemCount += insertServicesSubmenus(s.userToplevelSubmenus, mainMenu);
0520     userItemCount += insertServices(s.userToplevel, mainMenu);
0521 
0522     return {userItemCount, actionMenu};
0523 }
0524 
0525 int KFileItemActionsPrivate::addPluginActionsTo(QMenu *mainMenu, QMenu *actionsMenu, const QStringList &excludeList)
0526 {
0527     QString commonMimeType = m_props.mimeType();
0528     if (commonMimeType.isEmpty() && m_props.isFile()) {
0529         commonMimeType = QStringLiteral("application/octet-stream");
0530     }
0531 
0532     int itemCount = 0;
0533 
0534     const KConfigGroup showGroup = m_config.group(QStringLiteral("Show"));
0535 
0536     const QMimeDatabase db;
0537     const auto jsonPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/kfileitemaction"), [&db, commonMimeType](const KPluginMetaData &metaData) {
0538         auto mimeType = db.mimeTypeForName(commonMimeType);
0539         const QStringList list = metaData.mimeTypes();
0540         return std::any_of(list.constBegin(), list.constEnd(), [mimeType](const QString &supportedMimeType) {
0541             return mimeType.inherits(supportedMimeType);
0542         });
0543     });
0544 
0545     for (const auto &jsonMetadata : jsonPlugins) {
0546         // The plugin has been disabled
0547         const QString pluginId = jsonMetadata.pluginId();
0548         if (!showGroup.readEntry(pluginId, true) || excludeList.contains(pluginId)) {
0549             continue;
0550         }
0551 
0552         KAbstractFileItemActionPlugin *abstractPlugin = m_loadedPlugins.value(pluginId);
0553         if (!abstractPlugin) {
0554             abstractPlugin = KPluginFactory::instantiatePlugin<KAbstractFileItemActionPlugin>(jsonMetadata, this).plugin;
0555             m_loadedPlugins.insert(pluginId, abstractPlugin);
0556         }
0557         if (abstractPlugin) {
0558             connect(abstractPlugin, &KAbstractFileItemActionPlugin::error, q, &KFileItemActions::error);
0559             const QList<QAction *> actions = abstractPlugin->actions(m_props, m_parentWidget);
0560             itemCount += actions.count();
0561             const QString showInSubmenu = jsonMetadata.value(QStringLiteral("X-KDE-Show-In-Submenu"));
0562             if (showInSubmenu == QLatin1String("true")) {
0563                 actionsMenu->addActions(actions);
0564             } else {
0565                 mainMenu->addActions(actions);
0566             }
0567         }
0568     }
0569 
0570     return itemCount;
0571 }
0572 
0573 KService::List
0574 KFileItemActionsPrivate::associatedApplications(const QStringList &mimeTypeList, const QString &traderConstraint, const QStringList &excludedDesktopEntryNames)
0575 {
0576     if (!KAuthorized::authorizeAction(QStringLiteral("openwith")) || mimeTypeList.isEmpty()) {
0577         return KService::List();
0578     }
0579 
0580     KService::List firstOffers;
0581     if (traderConstraint.isEmpty()) {
0582         firstOffers = KApplicationTrader::queryByMimeType(mimeTypeList.first(), [excludedDesktopEntryNames](const KService::Ptr &service) {
0583             return !excludedDesktopEntryNames.contains(service->desktopEntryName());
0584         });
0585     }
0586 
0587     QList<KFileItemActionsPrivate::ServiceRank> rankings;
0588     QStringList serviceList;
0589 
0590     // This section does two things.  First, it determines which services are common to all the given MIME types.
0591     // Second, it ranks them based on their preference level in the associated applications list.
0592     // The more often a service appear near the front of the list, the LOWER its score.
0593 
0594     rankings.reserve(firstOffers.count());
0595     serviceList.reserve(firstOffers.count());
0596     for (int i = 0; i < firstOffers.count(); ++i) {
0597         KFileItemActionsPrivate::ServiceRank tempRank;
0598         tempRank.service = firstOffers[i];
0599         tempRank.score = i;
0600         rankings << tempRank;
0601         serviceList << tempRank.service->storageId();
0602     }
0603 
0604     for (int j = 1; j < mimeTypeList.count(); ++j) {
0605         KService::List offers;
0606         QStringList subservice; // list of services that support this MIME type
0607         if (traderConstraint.isEmpty()) {
0608             offers = KApplicationTrader::queryByMimeType(mimeTypeList[j], [excludedDesktopEntryNames](const KService::Ptr &service) {
0609                 return !excludedDesktopEntryNames.contains(service->desktopEntryName());
0610             });
0611         }
0612 
0613         subservice.reserve(offers.count());
0614         for (int i = 0; i != offers.count(); ++i) {
0615             const QString serviceId = offers[i]->storageId();
0616             subservice << serviceId;
0617             const int idPos = serviceList.indexOf(serviceId);
0618             if (idPos != -1) {
0619                 rankings[idPos].score += i;
0620             } // else: we ignore the services that didn't support the previous MIME types
0621         }
0622 
0623         // Remove services which supported the previous MIME types but don't support this one
0624         for (int i = 0; i < serviceList.count(); ++i) {
0625             if (!subservice.contains(serviceList[i])) {
0626                 serviceList.removeAt(i);
0627                 rankings.removeAt(i);
0628                 --i;
0629             }
0630         }
0631         // Nothing left -> there is no common application for these MIME types
0632         if (rankings.isEmpty()) {
0633             return KService::List();
0634         }
0635     }
0636 
0637     std::sort(rankings.begin(), rankings.end(), KFileItemActionsPrivate::lessRank);
0638 
0639     KService::List result;
0640     result.reserve(rankings.size());
0641     for (const KFileItemActionsPrivate::ServiceRank &tempRank : std::as_const(rankings)) {
0642         result << tempRank.service;
0643     }
0644 
0645     return result;
0646 }
0647 
0648 void KFileItemActionsPrivate::insertOpenWithActionsTo(QAction *before,
0649                                                       QMenu *topMenu,
0650                                                       const QStringList &excludedDesktopEntryNames,
0651                                                       const QString &traderConstraint)
0652 {
0653     if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
0654         return;
0655     }
0656 
0657     m_traderConstraint = traderConstraint;
0658     // TODO Overload with excludedDesktopEntryNames, but this method in public API and will be handled in a new MR
0659     KService::List offers = associatedApplications(m_mimeTypeList, traderConstraint, excludedDesktopEntryNames);
0660 
0661     //// Ok, we have everything, now insert
0662 
0663     const KFileItemList items = m_props.items();
0664     const KFileItem &firstItem = items.first();
0665     const bool isLocal = firstItem.url().isLocalFile();
0666     const bool isDir = m_props.isDirectory();
0667     // "Open With..." for folders is really not very useful, especially for remote folders.
0668     // (media:/something, or trash:/, or ftp://...).
0669     // Don't show "open with" actions for remote dirs only
0670     if (isDir && !isLocal) {
0671         return;
0672     }
0673 
0674     const auto makeOpenWithAction = [this, isDir] {
0675         auto action = new QAction(this);
0676         action->setText(isDir ? i18nc("@title:menu", "&Open Folder With...") : i18nc("@title:menu", "&Open With..."));
0677         action->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
0678         action->setObjectName(QStringLiteral("openwith_browse")); // For the unittest
0679         return action;
0680     };
0681 
0682 #ifndef KIO_ANDROID_STUB
0683     if (KSandbox::isInside() && !m_fileOpenList.isEmpty()) {
0684         auto openWithAction = makeOpenWithAction();
0685         QObject::connect(openWithAction, &QAction::triggered, this, [this] {
0686             const auto &items = m_fileOpenList;
0687             for (const auto &fileItem : items) {
0688                 QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"),
0689                                                                       QLatin1String("/org/freedesktop/portal/desktop"),
0690                                                                       QLatin1String("org.freedesktop.portal.OpenURI"),
0691                                                                       QLatin1String("OpenURI"));
0692                 message << QString() << fileItem.url() << QVariantMap{};
0693                 QDBusConnection::sessionBus().asyncCall(message);
0694             }
0695         });
0696         topMenu->insertAction(before, openWithAction);
0697         return;
0698     }
0699     if (KSandbox::isInside()) {
0700         return;
0701     }
0702 #endif
0703 
0704     QStringList serviceIdList = listPreferredServiceIds(m_mimeTypeList, excludedDesktopEntryNames, traderConstraint);
0705 
0706     // When selecting files with multiple MIME types, offer either "open with <app for all>"
0707     // or a generic <open> (if there are any apps associated).
0708     if (m_mimeTypeList.count() > 1 && !serviceIdList.isEmpty()
0709         && !(serviceIdList.count() == 1 && serviceIdList.first().isEmpty())) { // empty means "no apps associated"
0710 
0711         QAction *runAct = new QAction(this);
0712         if (serviceIdList.count() == 1) {
0713             const KService::Ptr app = preferredService(m_mimeTypeList.first(), excludedDesktopEntryNames, traderConstraint);
0714             runAct->setText(isDir ? i18n("&Open folder with %1", app->name()) : i18n("&Open with %1", app->name()));
0715             runAct->setIcon(QIcon::fromTheme(app->icon()));
0716 
0717             // Remove that app from the offers list (#242731)
0718             for (int i = 0; i < offers.count(); ++i) {
0719                 if (offers[i]->storageId() == app->storageId()) {
0720                     offers.removeAt(i);
0721                     break;
0722                 }
0723             }
0724         } else {
0725             runAct->setText(i18n("&Open"));
0726         }
0727 
0728         QObject::connect(runAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotRunPreferredApplications);
0729         topMenu->insertAction(before, runAct);
0730 
0731         m_traderConstraint = traderConstraint;
0732         m_fileOpenList = m_props.items();
0733     }
0734 
0735     auto openWithAct = makeOpenWithAction();
0736     QObject::connect(openWithAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotOpenWithDialog);
0737 
0738     if (!offers.isEmpty()) {
0739         // Show the top app inline for files, but not folders
0740         if (!isDir) {
0741             QAction *act = createAppAction(offers.takeFirst(), true);
0742             topMenu->insertAction(before, act);
0743         }
0744 
0745         // If there are still more apps, show them in a sub-menu
0746         if (!offers.isEmpty()) { // submenu 'open with'
0747             QMenu *subMenu = new QMenu(isDir ? i18nc("@title:menu", "&Open Folder With") : i18nc("@title:menu", "&Open With"), topMenu);
0748             subMenu->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
0749             subMenu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // For the unittest
0750             // Add other apps to the sub-menu
0751             for (const KServicePtr &service : std::as_const(offers)) {
0752                 QAction *act = createAppAction(service, false);
0753                 subMenu->addAction(act);
0754             }
0755 
0756             subMenu->addSeparator();
0757 
0758             openWithAct->setText(i18nc("@action:inmenu Open With", "&Other Application..."));
0759             subMenu->addAction(openWithAct);
0760 
0761             topMenu->insertMenu(before, subMenu);
0762         } else { // No other apps
0763             topMenu->insertAction(before, openWithAct);
0764         }
0765     } else { // no app offers -> Open With...
0766         openWithAct->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
0767         openWithAct->setObjectName(QStringLiteral("openwith")); // For the unittest
0768         topMenu->insertAction(before, openWithAct);
0769     }
0770 
0771     if (m_props.mimeType() == QLatin1String("application/x-desktop")) {
0772         const QString path = firstItem.localPath();
0773         const ServiceList services = KDesktopFile(path).actions();
0774         for (const KDesktopFileAction &serviceAction : services) {
0775             QAction *action = new QAction(this);
0776             action->setText(serviceAction.name());
0777             action->setIcon(QIcon::fromTheme(serviceAction.icon()));
0778 
0779             connect(action, &QAction::triggered, this, [serviceAction] {
0780                 if (KAuthorized::authorizeAction(serviceAction.name())) {
0781                     auto *job = new KIO::ApplicationLauncherJob(serviceAction);
0782                     job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
0783                     job->start();
0784                 }
0785             });
0786 
0787             topMenu->addAction(action);
0788         }
0789     }
0790 
0791     topMenu->insertSeparator(before);
0792 }
0793 
0794 QStringList KFileItemActionsPrivate::serviceMenuFilePaths()
0795 {
0796     QStringList filePaths;
0797 
0798     std::set<QString> uniqueFileNames;
0799 
0800     // Load servicemenus from new install location
0801     const QStringList paths =
0802         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kio/servicemenus"), QStandardPaths::LocateDirectory);
0803     const QStringList fromDisk = KFileUtils::findAllUniqueFiles(paths, QStringList(QStringLiteral("*.desktop")));
0804     for (const QString &fileFromDisk : fromDisk) {
0805         if (auto [_, inserted] = uniqueFileNames.insert(fileFromDisk.split(QLatin1Char('/')).last()); inserted) {
0806             filePaths << fileFromDisk;
0807         }
0808     }
0809     return filePaths;
0810 }
0811 
0812 void KFileItemActions::setParentWidget(QWidget *widget)
0813 {
0814     d->m_parentWidget = widget;
0815 }
0816 
0817 #include "moc_kfileitemactions.cpp"
0818 #include "moc_kfileitemactions_p.cpp"