File indexing completed on 2024-04-28 15:27:22

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 <KFileUtils>
0017 #include <KIO/ApplicationLauncherJob>
0018 #include <KIO/JobUiDelegate>
0019 #include <KLocalizedString>
0020 #include <KMimeTypeTrader>
0021 #include <KPluginFactory>
0022 #include <KPluginMetaData>
0023 #include <KSandbox>
0024 #include <KServiceTypeTrader>
0025 #include <jobuidelegatefactory.h>
0026 #include <kapplicationtrader.h>
0027 #include <kdesktopfileactions.h>
0028 #include <kdirnotify.h>
0029 #include <kurlauthorized.h>
0030 
0031 #include <QFile>
0032 #include <QMenu>
0033 #include <QMimeDatabase>
0034 #include <QtAlgorithms>
0035 
0036 #ifndef KIO_ANDROID_STUB
0037 #include <QDBusConnection>
0038 #include <QDBusConnectionInterface>
0039 #include <QDBusInterface>
0040 #include <QDBusMessage>
0041 #endif
0042 #include <algorithm>
0043 #include <kio_widgets_debug.h>
0044 #include <set>
0045 
0046 static bool KIOSKAuthorizedAction(const KConfigGroup &cfg)
0047 {
0048     const QStringList list = cfg.readEntry("X-KDE-AuthorizeAction", QStringList());
0049     return std::all_of(list.constBegin(), list.constEnd(), [](const QString &action) {
0050         return KAuthorized::authorize(action.trimmed());
0051     });
0052 }
0053 
0054 static bool mimeTypeListContains(const QStringList &list, const KFileItem &item)
0055 {
0056     const QString itemMimeType = item.mimetype();
0057     return std::any_of(list.cbegin(), list.cend(), [&](const QString &mt) {
0058         if (mt == itemMimeType || mt == QLatin1String("all/all")) {
0059             return true;
0060         }
0061 
0062         if (item.isFile() //
0063             && (mt == QLatin1String("allfiles") || mt == QLatin1String("all/allfiles") || mt == QLatin1String("application/octet-stream"))) {
0064             return true;
0065         }
0066 
0067         if (item.currentMimeType().inherits(mt)) {
0068             return true;
0069         }
0070 
0071         if (mt.endsWith(QLatin1String("/*"))) {
0072             const int slashPos = mt.indexOf(QLatin1Char('/'));
0073             const auto topLevelType = QStringView(mt).mid(0, slashPos);
0074             return itemMimeType.startsWith(topLevelType);
0075         }
0076         return false;
0077     });
0078 }
0079 
0080 // This helper class stores the .desktop-file actions and the servicemenus
0081 // in order to support X-KDE-Priority and X-KDE-Submenu.
0082 namespace KIO
0083 {
0084 class PopupServices
0085 {
0086 public:
0087     ServiceList &selectList(const QString &priority, const QString &submenuName);
0088 
0089 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
0090     ServiceList builtin;
0091 #endif
0092 
0093     ServiceList user;
0094     ServiceList userToplevel;
0095     ServiceList userPriority;
0096 
0097     QMap<QString, ServiceList> userSubmenus;
0098     QMap<QString, ServiceList> userToplevelSubmenus;
0099     QMap<QString, ServiceList> userPrioritySubmenus;
0100 };
0101 
0102 ServiceList &PopupServices::selectList(const QString &priority, const QString &submenuName)
0103 {
0104     // we use the categories .desktop entry to define submenus
0105     // if none is defined, we just pop it in the main menu
0106     if (submenuName.isEmpty()) {
0107         if (priority == QLatin1String("TopLevel")) {
0108             return userToplevel;
0109         } else if (priority == QLatin1String("Important")) {
0110             return userPriority;
0111         }
0112     } else if (priority == QLatin1String("TopLevel")) {
0113         return userToplevelSubmenus[submenuName];
0114     } else if (priority == QLatin1String("Important")) {
0115         return userPrioritySubmenus[submenuName];
0116     } else {
0117         return userSubmenus[submenuName];
0118     }
0119     return user;
0120 }
0121 } // namespace
0122 
0123 ////
0124 
0125 KFileItemActionsPrivate::KFileItemActionsPrivate(KFileItemActions *qq)
0126     : QObject()
0127     , q(qq)
0128     , m_executeServiceActionGroup(static_cast<QWidget *>(nullptr))
0129     , m_runApplicationActionGroup(static_cast<QWidget *>(nullptr))
0130     , m_parentWidget(nullptr)
0131     , m_config(QStringLiteral("kservicemenurc"), KConfig::NoGlobals)
0132 {
0133     QObject::connect(&m_executeServiceActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotExecuteService);
0134     QObject::connect(&m_runApplicationActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotRunApplication);
0135 }
0136 
0137 KFileItemActionsPrivate::~KFileItemActionsPrivate()
0138 {
0139 }
0140 
0141 int KFileItemActionsPrivate::insertServicesSubmenus(const QMap<QString, ServiceList> &submenus, QMenu *menu, bool isBuiltin)
0142 {
0143     int count = 0;
0144     QMap<QString, ServiceList>::ConstIterator it;
0145     for (it = submenus.begin(); it != submenus.end(); ++it) {
0146         if (it.value().isEmpty()) {
0147             // avoid empty sub-menus
0148             continue;
0149         }
0150 
0151         QMenu *actionSubmenu = new QMenu(menu);
0152         const int servicesAddedCount = insertServices(it.value(), actionSubmenu, isBuiltin);
0153 
0154         if (servicesAddedCount > 0) {
0155             count += servicesAddedCount;
0156             actionSubmenu->setTitle(it.key());
0157             actionSubmenu->setIcon(QIcon::fromTheme(it.value().first().icon()));
0158             actionSubmenu->menuAction()->setObjectName(QStringLiteral("services_submenu")); // for the unittest
0159             menu->addMenu(actionSubmenu);
0160         } else {
0161             // avoid empty sub-menus
0162             delete actionSubmenu;
0163         }
0164     }
0165 
0166     return count;
0167 }
0168 
0169 int KFileItemActionsPrivate::insertServices(const ServiceList &list, QMenu *menu, bool isBuiltin)
0170 {
0171     ServiceList sortedList = list;
0172     std::sort(sortedList.begin(), sortedList.end(), [](const KServiceAction &a1, const KServiceAction &a2) {
0173         return a1.name() < a2.name();
0174     });
0175     int count = 0;
0176     for (const KServiceAction &serviceAction : std::as_const(sortedList)) {
0177         if (serviceAction.isSeparator()) {
0178             const QList<QAction *> actions = menu->actions();
0179             if (!actions.isEmpty() && !actions.last()->isSeparator()) {
0180                 menu->addSeparator();
0181             }
0182             continue;
0183         }
0184 
0185         if (isBuiltin || !serviceAction.noDisplay()) {
0186             QAction *act = new QAction(q);
0187             act->setObjectName(QStringLiteral("menuaction")); // for the unittest
0188             QString text = serviceAction.text();
0189             text.replace(QLatin1Char('&'), QLatin1String("&&"));
0190             act->setText(text);
0191             if (!serviceAction.icon().isEmpty()) {
0192                 act->setIcon(QIcon::fromTheme(serviceAction.icon()));
0193             }
0194             act->setData(QVariant::fromValue(serviceAction));
0195             m_executeServiceActionGroup.addAction(act);
0196 
0197             menu->addAction(act); // Add to toplevel menu
0198             ++count;
0199         }
0200     }
0201 
0202     return count;
0203 }
0204 
0205 void KFileItemActionsPrivate::slotExecuteService(QAction *act)
0206 {
0207     const KServiceAction serviceAction = act->data().value<KServiceAction>();
0208     if (KAuthorized::authorizeAction(serviceAction.name())) {
0209         auto *job = new KIO::ApplicationLauncherJob(serviceAction);
0210         job->setUrls(m_props.urlList());
0211         job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0212         job->start();
0213     }
0214 }
0215 
0216 KFileItemActions::KFileItemActions(QObject *parent)
0217     : QObject(parent)
0218     , d(new KFileItemActionsPrivate(this))
0219 {
0220 }
0221 
0222 KFileItemActions::~KFileItemActions() = default;
0223 
0224 void KFileItemActions::setItemListProperties(const KFileItemListProperties &itemListProperties)
0225 {
0226     d->m_props = itemListProperties;
0227 
0228     d->m_mimeTypeList.clear();
0229     const KFileItemList items = d->m_props.items();
0230     for (const KFileItem &item : items) {
0231         if (!d->m_mimeTypeList.contains(item.mimetype())) {
0232             d->m_mimeTypeList << item.mimetype();
0233         }
0234     }
0235 }
0236 
0237 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 79)
0238 int KFileItemActions::addServiceActionsTo(QMenu *mainMenu)
0239 {
0240     return d->addServiceActionsTo(mainMenu, {}, {}).userItemCount;
0241 }
0242 #endif
0243 
0244 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 79)
0245 int KFileItemActions::addPluginActionsTo(QMenu *mainMenu)
0246 {
0247     return d->addPluginActionsTo(mainMenu, mainMenu, {});
0248 }
0249 #endif
0250 
0251 void KFileItemActions::addActionsTo(QMenu *menu, MenuActionSources sources, const QList<QAction *> &additionalActions, const QStringList &excludeList)
0252 {
0253     QMenu *actionsMenu = menu;
0254     if (sources & MenuActionSource::Services) {
0255         actionsMenu = d->addServiceActionsTo(menu, additionalActions, excludeList).menu;
0256     } else {
0257         // Since we didn't call addServiceActionsTo(), we have to add additional actions manually
0258         for (QAction *action : additionalActions) {
0259             actionsMenu->addAction(action);
0260         }
0261     }
0262     if (sources & MenuActionSource::Plugins) {
0263         d->addPluginActionsTo(menu, actionsMenu, excludeList);
0264     }
0265 }
0266 
0267 // static
0268 KService::List KFileItemActions::associatedApplications(const QStringList &mimeTypeList)
0269 {
0270     return KFileItemActionsPrivate::associatedApplications(mimeTypeList, QString(), QStringList{});
0271 }
0272 
0273 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 83)
0274 // static
0275 KService::List KFileItemActions::associatedApplications(const QStringList &mimeTypeList, const QString &traderConstraint)
0276 {
0277     return KFileItemActionsPrivate::associatedApplications(mimeTypeList, traderConstraint, QStringList{});
0278 }
0279 
0280 #endif
0281 
0282 static KService::Ptr preferredService(const QString &mimeType, const QStringList &excludedDesktopEntryNames, const QString &constraint)
0283 {
0284     KService::List services;
0285     if (constraint.isEmpty()) {
0286         services = KApplicationTrader::queryByMimeType(mimeType, [&](const KService::Ptr &serv) {
0287             return !excludedDesktopEntryNames.contains(serv->desktopEntryName());
0288         });
0289     }
0290 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
0291     else {
0292         Q_ASSERT(excludedDesktopEntryNames.isEmpty());
0293         // KMimeTypeTrader::preferredService doesn't take a constraint
0294         QT_WARNING_PUSH
0295         QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0296         QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0297         services = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("Application"), constraint);
0298         QT_WARNING_POP
0299     }
0300 #endif
0301     return services.isEmpty() ? KService::Ptr() : services.first();
0302 }
0303 
0304 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
0305 void KFileItemActions::addOpenWithActionsTo(QMenu *topMenu, const QString &traderConstraint)
0306 {
0307     d->insertOpenWithActionsTo(nullptr, topMenu, QStringList(), traderConstraint);
0308 }
0309 #endif
0310 
0311 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
0312 void KFileItemActions::insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QString &traderConstraint)
0313 {
0314     d->insertOpenWithActionsTo(before, topMenu, QStringList(), traderConstraint);
0315 }
0316 #endif
0317 
0318 void KFileItemActions::insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QStringList &excludedDesktopEntryNames)
0319 {
0320     d->insertOpenWithActionsTo(before, topMenu, excludedDesktopEntryNames, QString());
0321 }
0322 
0323 void KFileItemActionsPrivate::slotRunPreferredApplications()
0324 {
0325     const KFileItemList fileItems = m_fileOpenList;
0326     const QStringList mimeTypeList = listMimeTypes(fileItems);
0327     const QStringList serviceIdList = listPreferredServiceIds(mimeTypeList, QStringList(), m_traderConstraint);
0328 
0329     for (const QString &serviceId : serviceIdList) {
0330         KFileItemList serviceItems;
0331         for (const KFileItem &item : fileItems) {
0332             const KService::Ptr serv = preferredService(item.mimetype(), QStringList(), m_traderConstraint);
0333             const QString preferredServiceId = serv ? serv->storageId() : QString();
0334             if (preferredServiceId == serviceId) {
0335                 serviceItems << item;
0336             }
0337         }
0338 
0339         if (serviceId.isEmpty()) { // empty means: no associated app for this MIME type
0340             openWithByMime(serviceItems);
0341             continue;
0342         }
0343 
0344         const KService::Ptr servicePtr = KService::serviceByStorageId(serviceId); // can be nullptr
0345         auto *job = new KIO::ApplicationLauncherJob(servicePtr);
0346         job->setUrls(serviceItems.urlList());
0347         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0348         job->start();
0349     }
0350 }
0351 
0352 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 83)
0353 void KFileItemActions::runPreferredApplications(const KFileItemList &fileOpenList, const QString &traderConstraint)
0354 {
0355     d->m_fileOpenList = fileOpenList;
0356     d->m_traderConstraint = traderConstraint;
0357     d->slotRunPreferredApplications();
0358 }
0359 #endif
0360 
0361 void KFileItemActions::runPreferredApplications(const KFileItemList &fileOpenList)
0362 {
0363     d->m_fileOpenList = fileOpenList;
0364     d->m_traderConstraint = QString();
0365     d->slotRunPreferredApplications();
0366 }
0367 
0368 void KFileItemActionsPrivate::openWithByMime(const KFileItemList &fileItems)
0369 {
0370     const QStringList mimeTypeList = listMimeTypes(fileItems);
0371     for (const QString &mimeType : mimeTypeList) {
0372         KFileItemList mimeItems;
0373         for (const KFileItem &item : fileItems) {
0374             if (item.mimetype() == mimeType) {
0375                 mimeItems << item;
0376             }
0377         }
0378         // Show Open With dialog
0379         auto *job = new KIO::ApplicationLauncherJob();
0380         job->setUrls(mimeItems.urlList());
0381         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0382         job->start();
0383     }
0384 }
0385 
0386 void KFileItemActionsPrivate::slotRunApplication(QAction *act)
0387 {
0388     // Is it an application, from one of the "Open With" actions?
0389     KService::Ptr app = act->data().value<KService::Ptr>();
0390     Q_ASSERT(app);
0391     auto *job = new KIO::ApplicationLauncherJob(app);
0392     job->setUrls(m_props.urlList());
0393     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0394     job->start();
0395 }
0396 
0397 void KFileItemActionsPrivate::slotOpenWithDialog()
0398 {
0399     // The item 'Other...' or 'Open With...' has been selected
0400     Q_EMIT q->openWithDialogAboutToBeShown();
0401     auto *job = new KIO::ApplicationLauncherJob();
0402     job->setUrls(m_props.urlList());
0403     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0404     job->start();
0405 }
0406 
0407 QStringList KFileItemActionsPrivate::listMimeTypes(const KFileItemList &items)
0408 {
0409     QStringList mimeTypeList;
0410     for (const KFileItem &item : items) {
0411         if (!mimeTypeList.contains(item.mimetype())) {
0412             mimeTypeList << item.mimetype();
0413         }
0414     }
0415     return mimeTypeList;
0416 }
0417 
0418 QStringList
0419 KFileItemActionsPrivate::listPreferredServiceIds(const QStringList &mimeTypeList, const QStringList &excludedDesktopEntryNames, const QString &traderConstraint)
0420 {
0421     QStringList serviceIdList;
0422     serviceIdList.reserve(mimeTypeList.size());
0423     for (const QString &mimeType : mimeTypeList) {
0424         const KService::Ptr serv = preferredService(mimeType, excludedDesktopEntryNames, traderConstraint);
0425         serviceIdList << (serv ? serv->storageId() : QString()); // empty string means mimetype has no associated apps
0426     }
0427     serviceIdList.removeDuplicates();
0428     return serviceIdList;
0429 }
0430 
0431 QAction *KFileItemActionsPrivate::createAppAction(const KService::Ptr &service, bool singleOffer)
0432 {
0433     QString actionName(service->name().replace(QLatin1Char('&'), QLatin1String("&&")));
0434     if (singleOffer) {
0435         actionName = i18n("Open &with %1", actionName);
0436     } else {
0437         actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName);
0438     }
0439 
0440     QAction *act = new QAction(q);
0441     act->setObjectName(QStringLiteral("openwith")); // for the unittest
0442     act->setIcon(QIcon::fromTheme(service->icon()));
0443     act->setText(actionName);
0444     act->setData(QVariant::fromValue(service));
0445     m_runApplicationActionGroup.addAction(act);
0446     return act;
0447 }
0448 
0449 bool KFileItemActionsPrivate::shouldDisplayServiceMenu(const KConfigGroup &cfg, const QString &protocol) const
0450 {
0451     const QList<QUrl> urlList = m_props.urlList();
0452     if (!KIOSKAuthorizedAction(cfg)) {
0453         return false;
0454     }
0455 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 76) && !defined(KIO_ANDROID_STUB)
0456     if (cfg.hasKey("X-KDE-ShowIfRunning")) {
0457         qCWarning(KIO_WIDGETS) << "The property X-KDE-ShowIfRunning is deprecated and will be removed in future releases";
0458         const QString app = cfg.readEntry("X-KDE-ShowIfRunning");
0459         if (QDBusConnection::sessionBus().interface()->isServiceRegistered(app)) {
0460             return false;
0461         }
0462     }
0463     if (cfg.hasKey("X-KDE-ShowIfDBusCall")) {
0464         qCWarning(KIO_WIDGETS) << "The property X-KDE-ShowIfDBusCall is deprecated and will be removed in future releases";
0465         QString calldata = cfg.readEntry("X-KDE-ShowIfDBusCall");
0466         const QStringList parts = calldata.split(QLatin1Char(' '));
0467         const QString &app = parts.at(0);
0468         const QString &obj = parts.at(1);
0469         QString interface = parts.at(2);
0470         QString method;
0471         int pos = interface.lastIndexOf(QLatin1Char('.'));
0472         if (pos != -1) {
0473             method = interface.mid(pos + 1);
0474             interface.truncate(pos);
0475         }
0476 
0477         QDBusMessage reply = QDBusInterface(app, obj, interface).call(method, QUrl::toStringList(urlList));
0478         if (reply.arguments().count() < 1 || reply.arguments().at(0).type() != QVariant::Bool || !reply.arguments().at(0).toBool()) {
0479             return false;
0480         }
0481     }
0482 #endif
0483     if (cfg.hasKey("X-KDE-Protocol")) {
0484         const QString theProtocol = cfg.readEntry("X-KDE-Protocol");
0485         if (theProtocol.startsWith(QLatin1Char('!'))) { // Is it excluded?
0486             if (QStringView(theProtocol).mid(1) == protocol) {
0487                 return false;
0488             }
0489         } else if (protocol != theProtocol) {
0490             return false;
0491         }
0492     } else if (cfg.hasKey("X-KDE-Protocols")) {
0493         const QStringList protocols = cfg.readEntry("X-KDE-Protocols", QStringList());
0494         if (!protocols.contains(protocol)) {
0495             return false;
0496         }
0497     } else if (protocol == QLatin1String("trash")) {
0498         // Require servicemenus for the trash to ask for protocol=trash explicitly.
0499         // Trashed files aren't supposed to be available for actions.
0500         // One might want a servicemenu for trash.desktop itself though.
0501         return false;
0502     }
0503 
0504 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 76)
0505     if (cfg.hasKey("X-KDE-Require")) {
0506         qCWarning(KIO_WIDGETS) << "The property X-KDE-Require is deprecated and will be removed in future releases";
0507         const QStringList capabilities = cfg.readEntry("X-KDE-Require", QStringList());
0508         if (capabilities.contains(QLatin1String("Write")) && !m_props.supportsWriting()) {
0509             return false;
0510         }
0511     }
0512 #endif
0513 
0514     const auto requiredNumbers = cfg.readEntry("X-KDE-RequiredNumberOfUrls", QList<int>());
0515     if (!requiredNumbers.isEmpty() && !requiredNumbers.contains(urlList.count())) {
0516         return false;
0517     }
0518     if (cfg.hasKey("X-KDE-MinNumberOfUrls")) {
0519         const int minNumber = cfg.readEntry("X-KDE-MinNumberOfUrls").toInt();
0520         if (urlList.count() < minNumber) {
0521             return false;
0522         }
0523     }
0524     if (cfg.hasKey("X-KDE-MaxNumberOfUrls")) {
0525         const int maxNumber = cfg.readEntry("X-KDE-MaxNumberOfUrls").toInt();
0526         if (urlList.count() > maxNumber) {
0527             return false;
0528         }
0529     }
0530     return true;
0531 }
0532 
0533 bool KFileItemActionsPrivate::checkTypesMatch(const KConfigGroup &cfg) const
0534 {
0535     // Like KService, we support ServiceTypes, X-KDE-ServiceTypes, and MimeType.
0536     const QStringList types = cfg.readEntry("ServiceTypes", QStringList())
0537         << cfg.readEntry("X-KDE-ServiceTypes", QStringList()) << cfg.readXdgListEntry("MimeType");
0538 
0539     if (types.isEmpty()) {
0540         return false;
0541     }
0542 
0543     const QStringList excludeTypes = cfg.readEntry("ExcludeServiceTypes", QStringList());
0544     const KFileItemList items = m_props.items();
0545     return std::all_of(items.constBegin(), items.constEnd(), [&types, &excludeTypes](const KFileItem &i) {
0546         return mimeTypeListContains(types, i) && !mimeTypeListContains(excludeTypes, i);
0547     });
0548 }
0549 
0550 KFileItemActionsPrivate::ServiceActionInfo
0551 KFileItemActionsPrivate::addServiceActionsTo(QMenu *mainMenu, const QList<QAction *> &additionalActions, const QStringList &excludeList)
0552 {
0553     const KFileItemList items = m_props.items();
0554     const KFileItem &firstItem = items.first();
0555     const QString protocol = firstItem.url().scheme(); // assumed to be the same for all items
0556     const bool isLocal = !firstItem.localPath().isEmpty();
0557     const bool isSingleLocal = items.count() == 1 && isLocal;
0558     const QList<QUrl> urlList = m_props.urlList();
0559 
0560     KIO::PopupServices s;
0561 
0562     // TODO KF6 remove mention of "builtin" (deprecated)
0563     // 1 - Look for builtin services
0564     if (isSingleLocal && m_props.mimeType() == QLatin1String("application/x-desktop")) {
0565 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
0566         // Get builtin services, like mount/unmount
0567         s.builtin = KDesktopFileActions::builtinServices(QUrl::fromLocalFile(firstItem.localPath()));
0568 #endif
0569     }
0570 
0571     // 2 - Look for "servicemenus" bindings (user-defined services)
0572 
0573     // first check the .directory if this is a directory
0574     if (m_props.isDirectory() && isSingleLocal) {
0575         const QString dotDirectoryFile = QUrl::fromLocalFile(firstItem.localPath()).path().append(QLatin1String("/.directory"));
0576         if (QFile::exists(dotDirectoryFile)) {
0577             const KDesktopFile desktopFile(dotDirectoryFile);
0578             const KConfigGroup cfg = desktopFile.desktopGroup();
0579 
0580             if (KIOSKAuthorizedAction(cfg)) {
0581                 const QString priority = cfg.readEntry("X-KDE-Priority");
0582                 const QString submenuName = cfg.readEntry("X-KDE-Submenu");
0583                 ServiceList &list = s.selectList(priority, submenuName);
0584                 list += KDesktopFileActions::userDefinedServices(KService(dotDirectoryFile), true);
0585             }
0586         }
0587     }
0588 
0589     const KConfigGroup showGroup = m_config.group("Show");
0590 
0591     const QMimeDatabase db;
0592     const QStringList files = serviceMenuFilePaths();
0593     for (const QString &file : files) {
0594         const KDesktopFile desktopFile(file);
0595         const KConfigGroup cfg = desktopFile.desktopGroup();
0596 
0597         if (!shouldDisplayServiceMenu(cfg, protocol)) {
0598             continue;
0599         }
0600 
0601         if (cfg.hasKey("Actions") || cfg.hasKey("X-KDE-GetActionMenu")) {
0602             if (!checkTypesMatch(cfg)) {
0603                 continue;
0604             }
0605 
0606             const QString priority = cfg.readEntry("X-KDE-Priority");
0607             const QString submenuName = cfg.readEntry("X-KDE-Submenu");
0608 
0609             ServiceList &list = s.selectList(priority, submenuName);
0610             const ServiceList userServices = KDesktopFileActions::userDefinedServices(KService(file), isLocal, urlList);
0611             std::copy_if(userServices.cbegin(), userServices.cend(), std::back_inserter(list), [&excludeList, &showGroup](const KServiceAction &srvAction) {
0612                 return showGroup.readEntry(srvAction.name(), true) && !excludeList.contains(srvAction.name());
0613             });
0614         }
0615     }
0616 
0617     QMenu *actionMenu = mainMenu;
0618     int userItemCount = 0;
0619     if (s.user.count() + s.userSubmenus.count() + s.userPriority.count() + s.userPrioritySubmenus.count() + additionalActions.count() > 3) {
0620         // we have more than three items, so let's make a submenu
0621         actionMenu = new QMenu(i18nc("@title:menu", "&Actions"), mainMenu);
0622         actionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic")));
0623         actionMenu->menuAction()->setObjectName(QStringLiteral("actions_submenu")); // for the unittest
0624         mainMenu->addMenu(actionMenu);
0625     }
0626 
0627     userItemCount += additionalActions.count();
0628     for (QAction *action : additionalActions) {
0629         actionMenu->addAction(action);
0630     }
0631     userItemCount += insertServicesSubmenus(s.userPrioritySubmenus, actionMenu, false);
0632     userItemCount += insertServices(s.userPriority, actionMenu, false);
0633     userItemCount += insertServicesSubmenus(s.userSubmenus, actionMenu, false);
0634     userItemCount += insertServices(s.user, actionMenu, false);
0635 
0636 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
0637     userItemCount += insertServices(s.builtin, mainMenu, true);
0638 #endif
0639 
0640     userItemCount += insertServicesSubmenus(s.userToplevelSubmenus, mainMenu, false);
0641     userItemCount += insertServices(s.userToplevel, mainMenu, false);
0642 
0643     return {userItemCount, actionMenu};
0644 }
0645 
0646 int KFileItemActionsPrivate::addPluginActionsTo(QMenu *mainMenu, QMenu *actionsMenu, const QStringList &excludeList)
0647 {
0648     QString commonMimeType = m_props.mimeType();
0649     if (commonMimeType.isEmpty() && m_props.isFile()) {
0650         commonMimeType = QStringLiteral("application/octet-stream");
0651     }
0652 
0653     QStringList addedPlugins;
0654     int itemCount = 0;
0655 
0656     const KConfigGroup showGroup = m_config.group("Show");
0657 
0658     const QMimeDatabase db;
0659     const auto jsonPlugins =
0660         KPluginMetaData::findPlugins(QStringLiteral("kf" QT_STRINGIFY(QT_VERSION_MAJOR) "/kfileitemaction"),
0661                                      [&db, commonMimeType](const KPluginMetaData &metaData) {
0662                                          auto mimeType = db.mimeTypeForName(commonMimeType);
0663                                          const QStringList list = metaData.mimeTypes();
0664                                          return std::any_of(list.constBegin(), list.constEnd(), [mimeType](const QString &supportedMimeType) {
0665                                              return mimeType.inherits(supportedMimeType);
0666                                          });
0667                                      });
0668 
0669     for (const auto &jsonMetadata : jsonPlugins) {
0670         // The plugin has been disabled
0671         const QString pluginId = jsonMetadata.pluginId();
0672         if (!showGroup.readEntry(pluginId, true) || excludeList.contains(pluginId)) {
0673             continue;
0674         }
0675 
0676         KAbstractFileItemActionPlugin *abstractPlugin = m_loadedPlugins.value(pluginId);
0677         if (!abstractPlugin) {
0678             abstractPlugin = KPluginFactory::instantiatePlugin<KAbstractFileItemActionPlugin>(jsonMetadata, this).plugin;
0679             m_loadedPlugins.insert(pluginId, abstractPlugin);
0680         }
0681         if (abstractPlugin) {
0682             connect(abstractPlugin, &KAbstractFileItemActionPlugin::error, q, &KFileItemActions::error);
0683             const QList<QAction *> actions = abstractPlugin->actions(m_props, m_parentWidget);
0684             itemCount += actions.count();
0685             const QString showInSubmenu = jsonMetadata.value(QStringLiteral("X-KDE-Show-In-Submenu"));
0686             if (showInSubmenu == QLatin1String("true")) {
0687                 actionsMenu->addActions(actions);
0688             } else {
0689                 mainMenu->addActions(actions);
0690             }
0691             addedPlugins.append(jsonMetadata.pluginId());
0692         }
0693     }
0694 
0695 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
0696     QT_WARNING_PUSH
0697     QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0698     QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0699     const KService::List fileItemPlugins =
0700         KMimeTypeTrader::self()->query(commonMimeType, QStringLiteral("KFileItemAction/Plugin"), QStringLiteral("exist Library"));
0701     for (const auto &service : fileItemPlugins) {
0702         if (!showGroup.readEntry(service->desktopEntryName(), true)) {
0703             // The plugin has been disabled
0704             continue;
0705         }
0706 
0707         // The plugin also has a JSON metadata and has already been added.
0708         if (addedPlugins.contains(service->desktopEntryName())) {
0709             continue;
0710         }
0711 
0712         KAbstractFileItemActionPlugin *abstractPlugin = m_loadedPlugins.value(service->desktopEntryName());
0713         if (!abstractPlugin) {
0714             abstractPlugin = service->createInstance<KAbstractFileItemActionPlugin>(this);
0715             if (abstractPlugin) {
0716                 connect(abstractPlugin, &KAbstractFileItemActionPlugin::error, q, &KFileItemActions::error);
0717                 m_loadedPlugins.insert(service->desktopEntryName(), abstractPlugin);
0718                 qCWarning(KIO_WIDGETS) << "The" << service->name()
0719                                        << "plugin still installs the desktop file for plugin loading. Please use JSON metadata instead, see "
0720                                        << "KAbstractFileItemActionPlugin class docs for instructions.";
0721             }
0722         }
0723         if (abstractPlugin) {
0724             auto actions = abstractPlugin->actions(m_props, m_parentWidget);
0725             itemCount += actions.count();
0726             mainMenu->addActions(actions);
0727             addedPlugins.append(service->desktopEntryName());
0728         }
0729     }
0730     QT_WARNING_POP
0731 #endif
0732 
0733     return itemCount;
0734 }
0735 
0736 KService::List
0737 KFileItemActionsPrivate::associatedApplications(const QStringList &mimeTypeList, const QString &traderConstraint, const QStringList &excludedDesktopEntryNames)
0738 {
0739     if (!KAuthorized::authorizeAction(QStringLiteral("openwith")) || mimeTypeList.isEmpty()) {
0740         return KService::List();
0741     }
0742 
0743     KService::List firstOffers;
0744     if (traderConstraint.isEmpty()) {
0745         firstOffers = KApplicationTrader::queryByMimeType(mimeTypeList.first(), [excludedDesktopEntryNames](const KService::Ptr &service) {
0746             return !excludedDesktopEntryNames.contains(service->desktopEntryName());
0747         });
0748     }
0749 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
0750     else {
0751         Q_ASSERT(excludedDesktopEntryNames.isEmpty());
0752         QT_WARNING_PUSH
0753         QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0754         QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0755         firstOffers = KMimeTypeTrader::self()->query(mimeTypeList.first(), QStringLiteral("Application"), traderConstraint);
0756         QT_WARNING_POP
0757     }
0758 #endif
0759 
0760     QList<KFileItemActionsPrivate::ServiceRank> rankings;
0761     QStringList serviceList;
0762 
0763     // This section does two things.  First, it determines which services are common to all the given MIME types.
0764     // Second, it ranks them based on their preference level in the associated applications list.
0765     // The more often a service appear near the front of the list, the LOWER its score.
0766 
0767     rankings.reserve(firstOffers.count());
0768     serviceList.reserve(firstOffers.count());
0769     for (int i = 0; i < firstOffers.count(); ++i) {
0770         KFileItemActionsPrivate::ServiceRank tempRank;
0771         tempRank.service = firstOffers[i];
0772         tempRank.score = i;
0773         rankings << tempRank;
0774         serviceList << tempRank.service->storageId();
0775     }
0776 
0777     for (int j = 1; j < mimeTypeList.count(); ++j) {
0778         KService::List offers;
0779         QStringList subservice; // list of services that support this MIME type
0780         if (traderConstraint.isEmpty()) {
0781             offers = KApplicationTrader::queryByMimeType(mimeTypeList[j], [excludedDesktopEntryNames](const KService::Ptr &service) {
0782                 return !excludedDesktopEntryNames.contains(service->desktopEntryName());
0783             });
0784         }
0785 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
0786         else {
0787             QT_WARNING_PUSH
0788             QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0789             QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0790             Q_ASSERT(excludedDesktopEntryNames.isEmpty());
0791             offers = KMimeTypeTrader::self()->query(mimeTypeList[j], QStringLiteral("Application"), traderConstraint);
0792             QT_WARNING_POP
0793         }
0794 #endif
0795         subservice.reserve(offers.count());
0796         for (int i = 0; i != offers.count(); ++i) {
0797             const QString serviceId = offers[i]->storageId();
0798             subservice << serviceId;
0799             const int idPos = serviceList.indexOf(serviceId);
0800             if (idPos != -1) {
0801                 rankings[idPos].score += i;
0802             } // else: we ignore the services that didn't support the previous MIME types
0803         }
0804 
0805         // Remove services which supported the previous MIME types but don't support this one
0806         for (int i = 0; i < serviceList.count(); ++i) {
0807             if (!subservice.contains(serviceList[i])) {
0808                 serviceList.removeAt(i);
0809                 rankings.removeAt(i);
0810                 --i;
0811             }
0812         }
0813         // Nothing left -> there is no common application for these MIME types
0814         if (rankings.isEmpty()) {
0815             return KService::List();
0816         }
0817     }
0818 
0819     std::sort(rankings.begin(), rankings.end(), KFileItemActionsPrivate::lessRank);
0820 
0821     KService::List result;
0822     result.reserve(rankings.size());
0823     for (const KFileItemActionsPrivate::ServiceRank &tempRank : std::as_const(rankings)) {
0824         result << tempRank.service;
0825     }
0826 
0827     return result;
0828 }
0829 
0830 void KFileItemActionsPrivate::insertOpenWithActionsTo(QAction *before,
0831                                                       QMenu *topMenu,
0832                                                       const QStringList &excludedDesktopEntryNames,
0833                                                       const QString &traderConstraint)
0834 {
0835     if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
0836         return;
0837     }
0838 
0839     m_traderConstraint = traderConstraint;
0840     // TODO Overload with excludedDesktopEntryNames, but this method in public API and will be handled in a new MR
0841     KService::List offers = associatedApplications(m_mimeTypeList, traderConstraint, excludedDesktopEntryNames);
0842 
0843     //// Ok, we have everything, now insert
0844 
0845     const KFileItemList items = m_props.items();
0846     const KFileItem &firstItem = items.first();
0847     const bool isLocal = firstItem.url().isLocalFile();
0848     const bool isDir = m_props.isDirectory();
0849     // "Open With..." for folders is really not very useful, especially for remote folders.
0850     // (media:/something, or trash:/, or ftp://...).
0851     // Don't show "open with" actions for remote dirs only
0852     if (isDir && !isLocal) {
0853         return;
0854     }
0855 
0856     const auto makeOpenWithAction = [this, isDir] {
0857         auto action = new QAction(this);
0858         action->setText(isDir ? i18nc("@title:menu", "&Open Folder With...") : i18nc("@title:menu", "&Open With..."));
0859         action->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
0860         action->setObjectName(QStringLiteral("openwith_browse")); // For the unittest
0861         return action;
0862     };
0863 
0864 #ifndef KIO_ANDROID_STUB
0865     if (KSandbox::isInside() && !m_fileOpenList.isEmpty()) {
0866         auto openWithAction = makeOpenWithAction();
0867         QObject::connect(openWithAction, &QAction::triggered, this, [this] {
0868             const auto &items = m_fileOpenList;
0869             for (const auto &fileItem : items) {
0870                 QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"),
0871                                                                       QLatin1String("/org/freedesktop/portal/desktop"),
0872                                                                       QLatin1String("org.freedesktop.portal.OpenURI"),
0873                                                                       QLatin1String("OpenURI"));
0874                 message << QString() << fileItem.url() << QVariantMap{};
0875                 QDBusConnection::sessionBus().asyncCall(message);
0876             }
0877         });
0878         topMenu->insertAction(before, openWithAction);
0879         return;
0880     }
0881     if (KSandbox::isInside()) {
0882         return;
0883     }
0884 #endif
0885 
0886     QStringList serviceIdList = listPreferredServiceIds(m_mimeTypeList, excludedDesktopEntryNames, traderConstraint);
0887 
0888     // When selecting files with multiple MIME types, offer either "open with <app for all>"
0889     // or a generic <open> (if there are any apps associated).
0890     if (m_mimeTypeList.count() > 1 && !serviceIdList.isEmpty()
0891         && !(serviceIdList.count() == 1 && serviceIdList.first().isEmpty())) { // empty means "no apps associated"
0892 
0893         QAction *runAct = new QAction(this);
0894         if (serviceIdList.count() == 1) {
0895             const KService::Ptr app = preferredService(m_mimeTypeList.first(), excludedDesktopEntryNames, traderConstraint);
0896             runAct->setText(isDir ? i18n("&Open folder with %1", app->name()) : i18n("&Open with %1", app->name()));
0897             runAct->setIcon(QIcon::fromTheme(app->icon()));
0898 
0899             // Remove that app from the offers list (#242731)
0900             for (int i = 0; i < offers.count(); ++i) {
0901                 if (offers[i]->storageId() == app->storageId()) {
0902                     offers.removeAt(i);
0903                     break;
0904                 }
0905             }
0906         } else {
0907             runAct->setText(i18n("&Open"));
0908         }
0909 
0910         QObject::connect(runAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotRunPreferredApplications);
0911         topMenu->insertAction(before, runAct);
0912 
0913         m_traderConstraint = traderConstraint;
0914         m_fileOpenList = m_props.items();
0915     }
0916 
0917     auto openWithAct = makeOpenWithAction();
0918     QObject::connect(openWithAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotOpenWithDialog);
0919 
0920     if (!offers.isEmpty()) {
0921         // Show the top app inline for files, but not folders
0922         if (!isDir) {
0923             QAction *act = createAppAction(offers.takeFirst(), true);
0924             topMenu->insertAction(before, act);
0925         }
0926 
0927         // If there are still more apps, show them in a sub-menu
0928         if (!offers.isEmpty()) { // submenu 'open with'
0929             QMenu *subMenu = new QMenu(isDir ? i18nc("@title:menu", "&Open Folder With") : i18nc("@title:menu", "&Open With"), topMenu);
0930             subMenu->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
0931             subMenu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // For the unittest
0932             // Add other apps to the sub-menu
0933             for (const KServicePtr &service : std::as_const(offers)) {
0934                 QAction *act = createAppAction(service, false);
0935                 subMenu->addAction(act);
0936             }
0937 
0938             subMenu->addSeparator();
0939 
0940             openWithAct->setText(i18nc("@action:inmenu Open With", "&Other Application..."));
0941             subMenu->addAction(openWithAct);
0942 
0943             topMenu->insertMenu(before, subMenu);
0944         } else { // No other apps
0945             topMenu->insertAction(before, openWithAct);
0946         }
0947     } else { // no app offers -> Open With...
0948         openWithAct->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
0949         openWithAct->setObjectName(QStringLiteral("openwith")); // For the unittest
0950         topMenu->insertAction(before, openWithAct);
0951     }
0952 
0953     if (m_props.mimeType() == QLatin1String("application/x-desktop")) {
0954         const QString path = firstItem.localPath();
0955         const KDesktopFile desktopFile(path);
0956         const KConfigGroup cfg = desktopFile.desktopGroup();
0957 
0958         const ServiceList services = KDesktopFileActions::userDefinedServices(KService(path), true /*isLocal*/);
0959 
0960         for (const KServiceAction &serviceAction : services) {
0961             QAction *action = new QAction(this);
0962             action->setText(serviceAction.text());
0963             action->setIcon(QIcon::fromTheme(serviceAction.icon()));
0964 
0965             connect(action, &QAction::triggered, this, [serviceAction] {
0966                 if (KAuthorized::authorizeAction(serviceAction.name())) {
0967                     auto *job = new KIO::ApplicationLauncherJob(serviceAction);
0968                     job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
0969                     job->start();
0970                 }
0971             });
0972 
0973             topMenu->addAction(action);
0974         }
0975     }
0976 
0977     topMenu->insertSeparator(before);
0978 }
0979 
0980 QStringList KFileItemActionsPrivate::serviceMenuFilePaths()
0981 {
0982     QStringList filePaths;
0983 
0984     // Use old KServiceTypeTrader code path
0985     std::set<QString> uniqueFileNames;
0986 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 85)
0987     QT_WARNING_PUSH
0988     QT_WARNING_DISABLE_DEPRECATED
0989     const KService::List entries = KServiceTypeTrader::self()->query(QStringLiteral("KonqPopupMenu/Plugin"));
0990     QT_WARNING_POP
0991     for (const KServicePtr &entry : entries) {
0992         filePaths << QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kservices5/") + entry->entryPath());
0993         uniqueFileNames.insert(entry->entryPath().split(QLatin1Char('/')).last());
0994     }
0995 #endif
0996     // Load servicemenus from new install location
0997     const QStringList paths =
0998         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kio/servicemenus"), QStandardPaths::LocateDirectory);
0999     const QStringList fromDisk = KFileUtils::findAllUniqueFiles(paths, QStringList(QStringLiteral("*.desktop")));
1000     for (const QString &fileFromDisk : fromDisk) {
1001         if (auto [_, inserted] = uniqueFileNames.insert(fileFromDisk.split(QLatin1Char('/')).last()); inserted) {
1002             filePaths << fileFromDisk;
1003         }
1004     }
1005     return filePaths;
1006 }
1007 
1008 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
1009 QAction *KFileItemActions::preferredOpenWithAction(const QString &traderConstraint)
1010 {
1011     const KService::List offers = associatedApplications(d->m_mimeTypeList, traderConstraint);
1012     if (offers.isEmpty()) {
1013         return nullptr;
1014     }
1015     return d->createAppAction(offers.first(), true);
1016 }
1017 #endif
1018 
1019 void KFileItemActions::setParentWidget(QWidget *widget)
1020 {
1021     d->m_parentWidget = widget;
1022 }
1023 
1024 #include "moc_kfileitemactions.cpp"
1025 #include "moc_kfileitemactions_p.cpp"