1 /*
2     This file is part of the KDE project
3     SPDX-FileCopyrightText: 1998-2009 David Faure <faure@kde.org>
4     SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
5 
6     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
7 */
8 
9 #include "kfileitemactions.h"
10 #include "kfileitemactions_p.h"
11 #include <KAbstractFileItemActionPlugin>
12 #include <KApplicationTrader>
13 #include <KAuthorized>
14 #include <KConfigGroup>
15 #include <KDesktopFile>
16 #include <KFileUtils>
17 #include <KIO/ApplicationLauncherJob>
18 #include <KIO/JobUiDelegate>
19 #include <KLocalizedString>
20 #include <KMimeTypeTrader>
21 #include <KPluginMetaData>
22 #include <KServiceTypeTrader>
23 #include <kapplicationtrader.h>
24 #include <kdesktopfileactions.h>
25 #include <kdirnotify.h>
26 #include <kurlauthorized.h>
27 
28 #include <QFile>
29 #include <QMenu>
30 #include <QMimeDatabase>
31 #include <QtAlgorithms>
32 
33 #ifndef KIO_ANDROID_STUB
34 #include <QDBusConnection>
35 #include <QDBusConnectionInterface>
36 #include <QDBusInterface>
37 #include <QDBusMessage>
38 #endif
39 #include <algorithm>
40 #include <kio_widgets_debug.h>
41 
KIOSKAuthorizedAction(const KConfigGroup & cfg)42 static bool KIOSKAuthorizedAction(const KConfigGroup &cfg)
43 {
44     const QStringList list = cfg.readEntry("X-KDE-AuthorizeAction", QStringList());
45     return std::all_of(list.constBegin(), list.constEnd(), [](const QString &action) {
46         return KAuthorized::authorize(action.trimmed());
47     });
48 }
49 
mimeTypeListContains(const QStringList & list,const KFileItem & item)50 static bool mimeTypeListContains(const QStringList &list, const KFileItem &item)
51 {
52     const QString itemMimeType = item.mimetype();
53     return std::any_of(list.cbegin(), list.cend(), [&](const QString &mt) {
54         if (mt == itemMimeType || mt == QLatin1String("all/all")) {
55             return true;
56         }
57 
58         if (item.isFile() //
59             && (mt == QLatin1String("allfiles") || mt == QLatin1String("all/allfiles") || mt == QLatin1String("application/octet-stream"))) {
60             return true;
61         }
62 
63         if (item.currentMimeType().inherits(mt)) {
64             return true;
65         }
66 
67         if (mt.endsWith(QLatin1String("/*"))) {
68             const int slashPos = mt.indexOf(QLatin1Char('/'));
69             const auto topLevelType = QStringView(mt).mid(0, slashPos);
70             return itemMimeType.startsWith(topLevelType);
71         }
72         return false;
73     });
74 }
75 
76 // This helper class stores the .desktop-file actions and the servicemenus
77 // in order to support X-KDE-Priority and X-KDE-Submenu.
78 namespace KIO
79 {
80 class PopupServices
81 {
82 public:
83     ServiceList &selectList(const QString &priority, const QString &submenuName);
84 
85 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
86     ServiceList builtin;
87 #endif
88 
89     ServiceList user;
90     ServiceList userToplevel;
91     ServiceList userPriority;
92 
93     QMap<QString, ServiceList> userSubmenus;
94     QMap<QString, ServiceList> userToplevelSubmenus;
95     QMap<QString, ServiceList> userPrioritySubmenus;
96 };
97 
selectList(const QString & priority,const QString & submenuName)98 ServiceList &PopupServices::selectList(const QString &priority, const QString &submenuName)
99 {
100     // we use the categories .desktop entry to define submenus
101     // if none is defined, we just pop it in the main menu
102     if (submenuName.isEmpty()) {
103         if (priority == QLatin1String("TopLevel")) {
104             return userToplevel;
105         } else if (priority == QLatin1String("Important")) {
106             return userPriority;
107         }
108     } else if (priority == QLatin1String("TopLevel")) {
109         return userToplevelSubmenus[submenuName];
110     } else if (priority == QLatin1String("Important")) {
111         return userPrioritySubmenus[submenuName];
112     } else {
113         return userSubmenus[submenuName];
114     }
115     return user;
116 }
117 } // namespace
118 
119 ////
120 
KFileItemActionsPrivate(KFileItemActions * qq)121 KFileItemActionsPrivate::KFileItemActionsPrivate(KFileItemActions *qq)
122     : QObject()
123     , q(qq)
124     , m_executeServiceActionGroup(static_cast<QWidget *>(nullptr))
125     , m_runApplicationActionGroup(static_cast<QWidget *>(nullptr))
126     , m_parentWidget(nullptr)
127     , m_config(QStringLiteral("kservicemenurc"), KConfig::NoGlobals)
128 {
129     QObject::connect(&m_executeServiceActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotExecuteService);
130     QObject::connect(&m_runApplicationActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotRunApplication);
131 }
132 
~KFileItemActionsPrivate()133 KFileItemActionsPrivate::~KFileItemActionsPrivate()
134 {
135 }
136 
insertServicesSubmenus(const QMap<QString,ServiceList> & submenus,QMenu * menu,bool isBuiltin)137 int KFileItemActionsPrivate::insertServicesSubmenus(const QMap<QString, ServiceList> &submenus, QMenu *menu, bool isBuiltin)
138 {
139     int count = 0;
140     QMap<QString, ServiceList>::ConstIterator it;
141     for (it = submenus.begin(); it != submenus.end(); ++it) {
142         if (it.value().isEmpty()) {
143             // avoid empty sub-menus
144             continue;
145         }
146 
147         QMenu *actionSubmenu = new QMenu(menu);
148         actionSubmenu->setTitle(it.key());
149         actionSubmenu->setIcon(QIcon::fromTheme(it.value().first().icon()));
150         actionSubmenu->menuAction()->setObjectName(QStringLiteral("services_submenu")); // for the unittest
151         menu->addMenu(actionSubmenu);
152         count += insertServices(it.value(), actionSubmenu, isBuiltin);
153     }
154 
155     return count;
156 }
157 
insertServices(const ServiceList & list,QMenu * menu,bool isBuiltin)158 int KFileItemActionsPrivate::insertServices(const ServiceList &list, QMenu *menu, bool isBuiltin)
159 {
160     ServiceList sortedList = list;
161     std::sort(sortedList.begin(), sortedList.end(), [](const KServiceAction &a1, const KServiceAction &a2) {
162         return a1.name() < a2.name();
163     });
164     int count = 0;
165     for (const KServiceAction &serviceAction : std::as_const(sortedList)) {
166         if (serviceAction.isSeparator()) {
167             const QList<QAction *> actions = menu->actions();
168             if (!actions.isEmpty() && !actions.last()->isSeparator()) {
169                 menu->addSeparator();
170             }
171             continue;
172         }
173 
174         if (isBuiltin || !serviceAction.noDisplay()) {
175             QAction *act = new QAction(q);
176             act->setObjectName(QStringLiteral("menuaction")); // for the unittest
177             QString text = serviceAction.text();
178             text.replace(QLatin1Char('&'), QLatin1String("&&"));
179             act->setText(text);
180             if (!serviceAction.icon().isEmpty()) {
181                 act->setIcon(QIcon::fromTheme(serviceAction.icon()));
182             }
183             act->setData(QVariant::fromValue(serviceAction));
184             m_executeServiceActionGroup.addAction(act);
185 
186             menu->addAction(act); // Add to toplevel menu
187             ++count;
188         }
189     }
190 
191     return count;
192 }
193 
slotExecuteService(QAction * act)194 void KFileItemActionsPrivate::slotExecuteService(QAction *act)
195 {
196     const KServiceAction serviceAction = act->data().value<KServiceAction>();
197     if (KAuthorized::authorizeAction(serviceAction.name())) {
198         auto *job = new KIO::ApplicationLauncherJob(serviceAction);
199         job->setUrls(m_props.urlList());
200         job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
201         job->start();
202     }
203 }
204 
KFileItemActions(QObject * parent)205 KFileItemActions::KFileItemActions(QObject *parent)
206     : QObject(parent)
207     , d(new KFileItemActionsPrivate(this))
208 {
209 }
210 
~KFileItemActions()211 KFileItemActions::~KFileItemActions()
212 {
213     delete d;
214 }
215 
setItemListProperties(const KFileItemListProperties & itemListProperties)216 void KFileItemActions::setItemListProperties(const KFileItemListProperties &itemListProperties)
217 {
218     d->m_props = itemListProperties;
219 
220     d->m_mimeTypeList.clear();
221     const KFileItemList items = d->m_props.items();
222     for (const KFileItem &item : items) {
223         if (!d->m_mimeTypeList.contains(item.mimetype())) {
224             d->m_mimeTypeList << item.mimetype();
225         }
226     }
227 }
228 
229 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 79)
addServiceActionsTo(QMenu * mainMenu)230 int KFileItemActions::addServiceActionsTo(QMenu *mainMenu)
231 {
232     return d->addServiceActionsTo(mainMenu, {}, {}).first;
233 }
234 #endif
235 
236 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 79)
addPluginActionsTo(QMenu * mainMenu)237 int KFileItemActions::addPluginActionsTo(QMenu *mainMenu)
238 {
239     return d->addPluginActionsTo(mainMenu, mainMenu, {});
240 }
241 #endif
242 
addActionsTo(QMenu * menu,MenuActionSources sources,const QList<QAction * > & additionalActions,const QStringList & excludeList)243 void KFileItemActions::addActionsTo(QMenu *menu, MenuActionSources sources, const QList<QAction *> &additionalActions, const QStringList &excludeList)
244 {
245     QMenu *actionsMenu = menu;
246     if (sources & MenuActionSource::Services) {
247         actionsMenu = d->addServiceActionsTo(menu, additionalActions, excludeList).second;
248     } else {
249         // Since we didn't call addServiceActionsTo(), we have to add additional actions manually
250         for (QAction *action : additionalActions) {
251             actionsMenu->addAction(action);
252         }
253     }
254     if (sources & MenuActionSource::Plugins) {
255         d->addPluginActionsTo(menu, actionsMenu, excludeList);
256     }
257 }
258 
259 // static
associatedApplications(const QStringList & mimeTypeList)260 KService::List KFileItemActions::associatedApplications(const QStringList &mimeTypeList)
261 {
262     return KFileItemActionsPrivate::associatedApplications(mimeTypeList, QString(), QStringList{});
263 }
264 
265 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 83)
266 // static
associatedApplications(const QStringList & mimeTypeList,const QString & traderConstraint)267 KService::List KFileItemActions::associatedApplications(const QStringList &mimeTypeList, const QString &traderConstraint)
268 {
269     return KFileItemActionsPrivate::associatedApplications(mimeTypeList, traderConstraint, QStringList{});
270 }
271 
272 #endif
273 
preferredService(const QString & mimeType,const QStringList & excludedDesktopEntryNames,const QString & constraint)274 static KService::Ptr preferredService(const QString &mimeType, const QStringList &excludedDesktopEntryNames, const QString &constraint)
275 {
276     KService::List services;
277     if (constraint.isEmpty()) {
278         services = KApplicationTrader::queryByMimeType(mimeType, [&](const KService::Ptr &serv) {
279             return !excludedDesktopEntryNames.contains(serv->desktopEntryName());
280         });
281     }
282 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
283     else {
284         Q_ASSERT(excludedDesktopEntryNames.isEmpty());
285         // KMimeTypeTrader::preferredService doesn't take a constraint
286         QT_WARNING_PUSH
287         QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
288         QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
289         services = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("Application"), constraint);
290         QT_WARNING_POP
291     }
292 #endif
293     return services.isEmpty() ? KService::Ptr() : services.first();
294 }
295 
296 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
addOpenWithActionsTo(QMenu * topMenu,const QString & traderConstraint)297 void KFileItemActions::addOpenWithActionsTo(QMenu *topMenu, const QString &traderConstraint)
298 {
299     d->insertOpenWithActionsTo(nullptr, topMenu, QStringList(), traderConstraint);
300 }
301 #endif
302 
303 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
insertOpenWithActionsTo(QAction * before,QMenu * topMenu,const QString & traderConstraint)304 void KFileItemActions::insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QString &traderConstraint)
305 {
306     d->insertOpenWithActionsTo(before, topMenu, QStringList(), traderConstraint);
307 }
308 #endif
309 
insertOpenWithActionsTo(QAction * before,QMenu * topMenu,const QStringList & excludedDesktopEntryNames)310 void KFileItemActions::insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QStringList &excludedDesktopEntryNames)
311 {
312     d->insertOpenWithActionsTo(before, topMenu, excludedDesktopEntryNames, QString());
313 }
314 
slotRunPreferredApplications()315 void KFileItemActionsPrivate::slotRunPreferredApplications()
316 {
317     const KFileItemList fileItems = m_fileOpenList;
318     const QStringList mimeTypeList = listMimeTypes(fileItems);
319     const QStringList serviceIdList = listPreferredServiceIds(mimeTypeList, QStringList(), m_traderConstraint);
320 
321     for (const QString &serviceId : serviceIdList) {
322         KFileItemList serviceItems;
323         for (const KFileItem &item : fileItems) {
324             const KService::Ptr serv = preferredService(item.mimetype(), QStringList(), m_traderConstraint);
325             const QString preferredServiceId = serv ? serv->storageId() : QString();
326             if (preferredServiceId == serviceId) {
327                 serviceItems << item;
328             }
329         }
330 
331         if (serviceId.isEmpty()) { // empty means: no associated app for this MIME type
332             openWithByMime(serviceItems);
333             continue;
334         }
335 
336         const KService::Ptr servicePtr = KService::serviceByStorageId(serviceId); // can be nullptr
337         auto *job = new KIO::ApplicationLauncherJob(servicePtr);
338         job->setUrls(serviceItems.urlList());
339         job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
340         job->start();
341     }
342 }
343 
344 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 83)
runPreferredApplications(const KFileItemList & fileOpenList,const QString & traderConstraint)345 void KFileItemActions::runPreferredApplications(const KFileItemList &fileOpenList, const QString &traderConstraint)
346 {
347     d->m_fileOpenList = fileOpenList;
348     d->m_traderConstraint = traderConstraint;
349     d->slotRunPreferredApplications();
350 }
351 #endif
352 
runPreferredApplications(const KFileItemList & fileOpenList)353 void KFileItemActions::runPreferredApplications(const KFileItemList &fileOpenList)
354 {
355     d->m_fileOpenList = fileOpenList;
356     d->m_traderConstraint = QString();
357     d->slotRunPreferredApplications();
358 }
359 
openWithByMime(const KFileItemList & fileItems)360 void KFileItemActionsPrivate::openWithByMime(const KFileItemList &fileItems)
361 {
362     const QStringList mimeTypeList = listMimeTypes(fileItems);
363     for (const QString &mimeType : mimeTypeList) {
364         KFileItemList mimeItems;
365         for (const KFileItem &item : fileItems) {
366             if (item.mimetype() == mimeType) {
367                 mimeItems << item;
368             }
369         }
370         // Show Open With dialog
371         auto *job = new KIO::ApplicationLauncherJob();
372         job->setUrls(mimeItems.urlList());
373         job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
374         job->start();
375     }
376 }
377 
slotRunApplication(QAction * act)378 void KFileItemActionsPrivate::slotRunApplication(QAction *act)
379 {
380     // Is it an application, from one of the "Open With" actions?
381     KService::Ptr app = act->data().value<KService::Ptr>();
382     Q_ASSERT(app);
383     auto *job = new KIO::ApplicationLauncherJob(app);
384     job->setUrls(m_props.urlList());
385     job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
386     job->start();
387 }
388 
slotOpenWithDialog()389 void KFileItemActionsPrivate::slotOpenWithDialog()
390 {
391     // The item 'Other...' or 'Open With...' has been selected
392     Q_EMIT q->openWithDialogAboutToBeShown();
393     auto *job = new KIO::ApplicationLauncherJob();
394     job->setUrls(m_props.urlList());
395     job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
396     job->start();
397 }
398 
listMimeTypes(const KFileItemList & items)399 QStringList KFileItemActionsPrivate::listMimeTypes(const KFileItemList &items)
400 {
401     QStringList mimeTypeList;
402     for (const KFileItem &item : items) {
403         if (!mimeTypeList.contains(item.mimetype())) {
404             mimeTypeList << item.mimetype();
405         }
406     }
407     return mimeTypeList;
408 }
409 
410 QStringList
listPreferredServiceIds(const QStringList & mimeTypeList,const QStringList & excludedDesktopEntryNames,const QString & traderConstraint)411 KFileItemActionsPrivate::listPreferredServiceIds(const QStringList &mimeTypeList, const QStringList &excludedDesktopEntryNames, const QString &traderConstraint)
412 {
413     QStringList serviceIdList;
414     serviceIdList.reserve(mimeTypeList.size());
415     for (const QString &mimeType : mimeTypeList) {
416         const KService::Ptr serv = preferredService(mimeType, excludedDesktopEntryNames, traderConstraint);
417         serviceIdList << (serv ? serv->storageId() : QString()); // empty string means mimetype has no associated apps
418     }
419     serviceIdList.removeDuplicates();
420     return serviceIdList;
421 }
422 
createAppAction(const KService::Ptr & service,bool singleOffer)423 QAction *KFileItemActionsPrivate::createAppAction(const KService::Ptr &service, bool singleOffer)
424 {
425     QString actionName(service->name().replace(QLatin1Char('&'), QLatin1String("&&")));
426     if (singleOffer) {
427         actionName = i18n("Open &with %1", actionName);
428     } else {
429         actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName);
430     }
431 
432     QAction *act = new QAction(q);
433     act->setObjectName(QStringLiteral("openwith")); // for the unittest
434     act->setIcon(QIcon::fromTheme(service->icon()));
435     act->setText(actionName);
436     act->setData(QVariant::fromValue(service));
437     m_runApplicationActionGroup.addAction(act);
438     return act;
439 }
440 
shouldDisplayServiceMenu(const KConfigGroup & cfg,const QString & protocol) const441 bool KFileItemActionsPrivate::shouldDisplayServiceMenu(const KConfigGroup &cfg, const QString &protocol) const
442 {
443     const QList<QUrl> urlList = m_props.urlList();
444     if (!KIOSKAuthorizedAction(cfg)) {
445         return false;
446     }
447 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 76) && !defined(KIO_ANDROID_STUB)
448     if (cfg.hasKey("X-KDE-ShowIfRunning")) {
449         qCWarning(KIO_WIDGETS) << "The property X-KDE-ShowIfRunning is deprecated and will be removed in future releases";
450         const QString app = cfg.readEntry("X-KDE-ShowIfRunning");
451         if (QDBusConnection::sessionBus().interface()->isServiceRegistered(app)) {
452             return false;
453         }
454     }
455     if (cfg.hasKey("X-KDE-ShowIfDBusCall")) {
456         qCWarning(KIO_WIDGETS) << "The property X-KDE-ShowIfDBusCall is deprecated and will be removed in future releases";
457         QString calldata = cfg.readEntry("X-KDE-ShowIfDBusCall");
458         const QStringList parts = calldata.split(QLatin1Char(' '));
459         const QString &app = parts.at(0);
460         const QString &obj = parts.at(1);
461         QString interface = parts.at(2);
462         QString method;
463         int pos = interface.lastIndexOf(QLatin1Char('.'));
464         if (pos != -1) {
465             method = interface.mid(pos + 1);
466             interface.truncate(pos);
467         }
468 
469         QDBusMessage reply = QDBusInterface(app, obj, interface).call(method, QUrl::toStringList(urlList));
470         if (reply.arguments().count() < 1 || reply.arguments().at(0).type() != QVariant::Bool || !reply.arguments().at(0).toBool()) {
471             return false;
472         }
473     }
474 #endif
475     if (cfg.hasKey("X-KDE-Protocol")) {
476         const QString theProtocol = cfg.readEntry("X-KDE-Protocol");
477         if (theProtocol.startsWith(QLatin1Char('!'))) { // Is it excluded?
478             if (QStringView(theProtocol).mid(1) == protocol) {
479                 return false;
480             }
481         } else if (protocol != theProtocol) {
482             return false;
483         }
484     } else if (cfg.hasKey("X-KDE-Protocols")) {
485         const QStringList protocols = cfg.readEntry("X-KDE-Protocols", QStringList());
486         if (!protocols.contains(protocol)) {
487             return false;
488         }
489     } else if (protocol == QLatin1String("trash")) {
490         // Require servicemenus for the trash to ask for protocol=trash explicitly.
491         // Trashed files aren't supposed to be available for actions.
492         // One might want a servicemenu for trash.desktop itself though.
493         return false;
494     }
495 
496 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 76)
497     if (cfg.hasKey("X-KDE-Require")) {
498         qCWarning(KIO_WIDGETS) << "The property X-KDE-Require is deprecated and will be removed in future releases";
499         const QStringList capabilities = cfg.readEntry("X-KDE-Require", QStringList());
500         if (capabilities.contains(QLatin1String("Write")) && !m_props.supportsWriting()) {
501             return false;
502         }
503     }
504 #endif
505 
506     const auto requiredNumbers = cfg.readEntry("X-KDE-RequiredNumberOfUrls", QList<int>());
507     if (!requiredNumbers.isEmpty() && !requiredNumbers.contains(urlList.count())) {
508         return false;
509     }
510     if (cfg.hasKey("X-KDE-MinNumberOfUrls")) {
511         const int minNumber = cfg.readEntry("X-KDE-MinNumberOfUrls").toInt();
512         if (urlList.count() < minNumber) {
513             return false;
514         }
515     }
516     if (cfg.hasKey("X-KDE-MaxNumberOfUrls")) {
517         const int maxNumber = cfg.readEntry("X-KDE-MaxNumberOfUrls").toInt();
518         if (urlList.count() > maxNumber) {
519             return false;
520         }
521     }
522     return true;
523 }
524 
checkTypesMatch(const KConfigGroup & cfg) const525 bool KFileItemActionsPrivate::checkTypesMatch(const KConfigGroup &cfg) const
526 {
527     // Like KService, we support ServiceTypes, X-KDE-ServiceTypes, and MimeType.
528     const QStringList types = cfg.readEntry("ServiceTypes", QStringList())
529         << cfg.readEntry("X-KDE-ServiceTypes", QStringList()) << cfg.readXdgListEntry("MimeType");
530 
531     if (types.isEmpty()) {
532         return false;
533     }
534 
535     const QStringList excludeTypes = cfg.readEntry("ExcludeServiceTypes", QStringList());
536     const KFileItemList items = m_props.items();
537     return std::all_of(items.constBegin(), items.constEnd(), [&types, &excludeTypes](const KFileItem &i) {
538         return mimeTypeListContains(types, i) && !mimeTypeListContains(excludeTypes, i);
539     });
540 }
541 
addServiceActionsTo(QMenu * mainMenu,const QList<QAction * > & additionalActions,const QStringList & excludeList)542 QPair<int, QMenu *> KFileItemActionsPrivate::addServiceActionsTo(QMenu *mainMenu, const QList<QAction *> &additionalActions, const QStringList &excludeList)
543 {
544     const KFileItemList items = m_props.items();
545     const KFileItem &firstItem = items.first();
546     const QString protocol = firstItem.url().scheme(); // assumed to be the same for all items
547     const bool isLocal = !firstItem.localPath().isEmpty();
548     const bool isSingleLocal = items.count() == 1 && isLocal;
549     const QList<QUrl> urlList = m_props.urlList();
550 
551     KIO::PopupServices s;
552 
553     // TODO KF6 remove mention of "builtin" (deprecated)
554     // 1 - Look for builtin and user-defined services
555     if (isSingleLocal && m_props.mimeType() == QLatin1String("application/x-desktop")) {
556 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
557         // Get builtin services, like mount/unmount
558         s.builtin = KDesktopFileActions::builtinServices(QUrl::fromLocalFile(firstItem.localPath()));
559 #endif
560         const QString path = firstItem.localPath();
561         const KDesktopFile desktopFile(path);
562         const KConfigGroup cfg = desktopFile.desktopGroup();
563         const QString priority = cfg.readEntry("X-KDE-Priority");
564         const QString submenuName = cfg.readEntry("X-KDE-Submenu");
565         ServiceList &list = s.selectList(priority, submenuName);
566         list = KDesktopFileActions::userDefinedServices(KService(path), true /*isLocal*/);
567     }
568 
569     // 2 - Look for "servicemenus" bindings (user-defined services)
570 
571     // first check the .directory if this is a directory
572     if (m_props.isDirectory() && isSingleLocal) {
573         const QString dotDirectoryFile = QUrl::fromLocalFile(firstItem.localPath()).path().append(QLatin1String("/.directory"));
574         if (QFile::exists(dotDirectoryFile)) {
575             const KDesktopFile desktopFile(dotDirectoryFile);
576             const KConfigGroup cfg = desktopFile.desktopGroup();
577 
578             if (KIOSKAuthorizedAction(cfg)) {
579                 const QString priority = cfg.readEntry("X-KDE-Priority");
580                 const QString submenuName = cfg.readEntry("X-KDE-Submenu");
581                 ServiceList &list = s.selectList(priority, submenuName);
582                 list += KDesktopFileActions::userDefinedServices(KService(dotDirectoryFile), true);
583             }
584         }
585     }
586 
587     const KConfigGroup showGroup = m_config.group("Show");
588 
589     const QMimeDatabase db;
590     const QStringList files = serviceMenuFilePaths();
591     for (const QString &file : files) {
592         const KDesktopFile desktopFile(file);
593         const KConfigGroup cfg = desktopFile.desktopGroup();
594 
595         if (!shouldDisplayServiceMenu(cfg, protocol)) {
596             continue;
597         }
598 
599         if (cfg.hasKey("Actions") || cfg.hasKey("X-KDE-GetActionMenu")) {
600             if (!checkTypesMatch(cfg)) {
601                 continue;
602             }
603 
604             const QString priority = cfg.readEntry("X-KDE-Priority");
605             const QString submenuName = cfg.readEntry("X-KDE-Submenu");
606 
607             ServiceList &list = s.selectList(priority, submenuName);
608             const ServiceList userServices = KDesktopFileActions::userDefinedServices(KService(file), isLocal, urlList);
609             for (const KServiceAction &action : userServices) {
610                 if (showGroup.readEntry(action.name(), true) && !excludeList.contains(action.name())) {
611                     list += action;
612                 }
613             }
614         }
615     }
616 
617     QMenu *actionMenu = mainMenu;
618     int userItemCount = 0;
619     if (s.user.count() + s.userSubmenus.count() + s.userPriority.count() + s.userPrioritySubmenus.count() + additionalActions.count() > 3) {
620         // we have more than three items, so let's make a submenu
621         actionMenu = new QMenu(i18nc("@title:menu", "&Actions"), mainMenu);
622         actionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic")));
623         actionMenu->menuAction()->setObjectName(QStringLiteral("actions_submenu")); // for the unittest
624         mainMenu->addMenu(actionMenu);
625     }
626 
627     userItemCount += additionalActions.count();
628     for (QAction *action : additionalActions) {
629         actionMenu->addAction(action);
630     }
631     userItemCount += insertServicesSubmenus(s.userPrioritySubmenus, actionMenu, false);
632     userItemCount += insertServices(s.userPriority, actionMenu, false);
633     userItemCount += insertServicesSubmenus(s.userSubmenus, actionMenu, false);
634     userItemCount += insertServices(s.user, actionMenu, false);
635 
636 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
637     userItemCount += insertServices(s.builtin, mainMenu, true);
638 #endif
639 
640     userItemCount += insertServicesSubmenus(s.userToplevelSubmenus, mainMenu, false);
641     userItemCount += insertServices(s.userToplevel, mainMenu, false);
642 
643     return {userItemCount, actionMenu};
644 }
645 
addPluginActionsTo(QMenu * mainMenu,QMenu * actionsMenu,const QStringList & excludeList)646 int KFileItemActionsPrivate::addPluginActionsTo(QMenu *mainMenu, QMenu *actionsMenu, const QStringList &excludeList)
647 {
648     QString commonMimeType = m_props.mimeType();
649     if (commonMimeType.isEmpty() && m_props.isFile()) {
650         commonMimeType = QStringLiteral("application/octet-stream");
651     }
652 
653     QStringList addedPlugins;
654     int itemCount = 0;
655 
656     const KConfigGroup showGroup = m_config.group("Show");
657 
658     const QMimeDatabase db;
659     const auto jsonPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf5/kfileitemaction"), [&db, commonMimeType](const KPluginMetaData &metaData) {
660         auto mimeType = db.mimeTypeForName(commonMimeType);
661         const QStringList list = metaData.mimeTypes();
662         return std::any_of(list.constBegin(), list.constEnd(), [mimeType](const QString &supportedMimeType) {
663             return mimeType.inherits(supportedMimeType);
664         });
665     });
666 
667     for (const auto &jsonMetadata : jsonPlugins) {
668         // The plugin has been disabled
669         const QString pluginId = jsonMetadata.pluginId();
670         if (!showGroup.readEntry(pluginId, true) || excludeList.contains(pluginId)) {
671             continue;
672         }
673 
674         KAbstractFileItemActionPlugin *abstractPlugin = m_loadedPlugins.value(pluginId);
675         if (!abstractPlugin) {
676             abstractPlugin = KPluginFactory::instantiatePlugin<KAbstractFileItemActionPlugin>(jsonMetadata, this).plugin;
677             m_loadedPlugins.insert(pluginId, abstractPlugin);
678         }
679         if (abstractPlugin) {
680             connect(abstractPlugin, &KAbstractFileItemActionPlugin::error, q, &KFileItemActions::error);
681             const QList<QAction *> actions = abstractPlugin->actions(m_props, m_parentWidget);
682             itemCount += actions.count();
683             const QString showInSubmenu = jsonMetadata.value(QStringLiteral("X-KDE-Show-In-Submenu"));
684             if (showInSubmenu == QLatin1String("true")) {
685                 actionsMenu->addActions(actions);
686             } else {
687                 mainMenu->addActions(actions);
688             }
689             addedPlugins.append(jsonMetadata.pluginId());
690         }
691     }
692 
693 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
694     QT_WARNING_PUSH
695     QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
696     QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
697     const KService::List fileItemPlugins =
698         KMimeTypeTrader::self()->query(commonMimeType, QStringLiteral("KFileItemAction/Plugin"), QStringLiteral("exist Library"));
699     QT_WARNING_POP
700     for (const auto &service : fileItemPlugins) {
701         if (!showGroup.readEntry(service->desktopEntryName(), true)) {
702             // The plugin has been disabled
703             continue;
704         }
705 
706         // The plugin also has a JSON metadata and has already been added.
707         if (addedPlugins.contains(service->desktopEntryName())) {
708             continue;
709         }
710 
711         KAbstractFileItemActionPlugin *abstractPlugin = m_loadedPlugins.value(service->desktopEntryName());
712         if (!abstractPlugin) {
713             abstractPlugin = service->createInstance<KAbstractFileItemActionPlugin>(this);
714             if (abstractPlugin) {
715                 connect(abstractPlugin, &KAbstractFileItemActionPlugin::error, q, &KFileItemActions::error);
716                 m_loadedPlugins.insert(service->desktopEntryName(), abstractPlugin);
717                 qCWarning(KIO_WIDGETS) << "The" << service->name()
718                                     << "plugin still installs the desktop file for plugin loading. Please use JSON metadata instead, see "
719                                     << "KAbstractFileItemActionPlugin class docs for instructions.";
720             }
721         }
722         if (abstractPlugin) {
723             auto actions = abstractPlugin->actions(m_props, m_parentWidget);
724             itemCount += actions.count();
725             mainMenu->addActions(actions);
726             addedPlugins.append(service->desktopEntryName());
727         }
728     }
729 #endif
730 
731     return itemCount;
732 }
733 
734 KService::List
associatedApplications(const QStringList & mimeTypeList,const QString & traderConstraint,const QStringList & excludedDesktopEntryNames)735 KFileItemActionsPrivate::associatedApplications(const QStringList &mimeTypeList, const QString &traderConstraint, const QStringList &excludedDesktopEntryNames)
736 {
737     if (!KAuthorized::authorizeAction(QStringLiteral("openwith")) || mimeTypeList.isEmpty()) {
738         return KService::List();
739     }
740 
741     KService::List firstOffers;
742     if (traderConstraint.isEmpty()) {
743         firstOffers = KApplicationTrader::queryByMimeType(mimeTypeList.first(), [excludedDesktopEntryNames](const KService::Ptr &service) {
744             return !excludedDesktopEntryNames.contains(service->desktopEntryName());
745         });
746     }
747 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
748     else {
749         Q_ASSERT(excludedDesktopEntryNames.isEmpty());
750         QT_WARNING_PUSH
751         QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
752         QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
753         firstOffers = KMimeTypeTrader::self()->query(mimeTypeList.first(), QStringLiteral("Application"), traderConstraint);
754         QT_WARNING_POP
755     }
756 #endif
757 
758     QList<KFileItemActionsPrivate::ServiceRank> rankings;
759     QStringList serviceList;
760 
761     // This section does two things.  First, it determines which services are common to all the given MIME types.
762     // Second, it ranks them based on their preference level in the associated applications list.
763     // The more often a service appear near the front of the list, the LOWER its score.
764 
765     rankings.reserve(firstOffers.count());
766     serviceList.reserve(firstOffers.count());
767     for (int i = 0; i < firstOffers.count(); ++i) {
768         KFileItemActionsPrivate::ServiceRank tempRank;
769         tempRank.service = firstOffers[i];
770         tempRank.score = i;
771         rankings << tempRank;
772         serviceList << tempRank.service->storageId();
773     }
774 
775     for (int j = 1; j < mimeTypeList.count(); ++j) {
776         KService::List offers;
777         QStringList subservice; // list of services that support this MIME type
778         if (traderConstraint.isEmpty()) {
779             offers = KApplicationTrader::queryByMimeType(mimeTypeList[j], [excludedDesktopEntryNames](const KService::Ptr &service) {
780                 return !excludedDesktopEntryNames.contains(service->desktopEntryName());
781             });
782         }
783 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
784         else {
785             QT_WARNING_PUSH
786             QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
787             QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
788             Q_ASSERT(excludedDesktopEntryNames.isEmpty());
789             offers = KMimeTypeTrader::self()->query(mimeTypeList[j], QStringLiteral("Application"), traderConstraint);
790             QT_WARNING_POP
791         }
792 #endif
793         subservice.reserve(offers.count());
794         for (int i = 0; i != offers.count(); ++i) {
795             const QString serviceId = offers[i]->storageId();
796             subservice << serviceId;
797             const int idPos = serviceList.indexOf(serviceId);
798             if (idPos != -1) {
799                 rankings[idPos].score += i;
800             } // else: we ignore the services that didn't support the previous MIME types
801         }
802 
803         // Remove services which supported the previous MIME types but don't support this one
804         for (int i = 0; i < serviceList.count(); ++i) {
805             if (!subservice.contains(serviceList[i])) {
806                 serviceList.removeAt(i);
807                 rankings.removeAt(i);
808                 --i;
809             }
810         }
811         // Nothing left -> there is no common application for these MIME types
812         if (rankings.isEmpty()) {
813             return KService::List();
814         }
815     }
816 
817     std::sort(rankings.begin(), rankings.end(), KFileItemActionsPrivate::lessRank);
818 
819     KService::List result;
820     result.reserve(rankings.size());
821     for (const KFileItemActionsPrivate::ServiceRank &tempRank : std::as_const(rankings)) {
822         result << tempRank.service;
823     }
824 
825     return result;
826 }
827 
insertOpenWithActionsTo(QAction * before,QMenu * topMenu,const QStringList & excludedDesktopEntryNames,const QString & traderConstraint)828 void KFileItemActionsPrivate::insertOpenWithActionsTo(QAction *before,
829                                                       QMenu *topMenu,
830                                                       const QStringList &excludedDesktopEntryNames,
831                                                       const QString &traderConstraint)
832 {
833     if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
834         return;
835     }
836 
837     m_traderConstraint = traderConstraint;
838     // TODO Overload with excludedDesktopEntryNames, but this method in public API and will be handled in a new MR
839     KService::List offers = associatedApplications(m_mimeTypeList, traderConstraint, excludedDesktopEntryNames);
840 
841     //// Ok, we have everything, now insert
842 
843     const KFileItemList items = m_props.items();
844     const KFileItem &firstItem = items.first();
845     const bool isLocal = firstItem.url().isLocalFile();
846     const bool isDir = m_props.isDirectory();
847     // "Open With..." for folders is really not very useful, especially for remote folders.
848     // (media:/something, or trash:/, or ftp://...).
849     // Don't show "open with" actions for remote dirs only
850     if (isDir && !isLocal) {
851         return;
852     }
853 
854     QStringList serviceIdList = listPreferredServiceIds(m_mimeTypeList, excludedDesktopEntryNames, traderConstraint);
855 
856     // When selecting files with multiple MIME types, offer either "open with <app for all>"
857     // or a generic <open> (if there are any apps associated).
858     if (m_mimeTypeList.count() > 1 && !serviceIdList.isEmpty()
859         && !(serviceIdList.count() == 1 && serviceIdList.first().isEmpty())) { // empty means "no apps associated"
860 
861         QAction *runAct = new QAction(this);
862         if (serviceIdList.count() == 1) {
863             const KService::Ptr app = preferredService(m_mimeTypeList.first(), excludedDesktopEntryNames, traderConstraint);
864             runAct->setText(i18n("&Open with %1", app->name()));
865             runAct->setIcon(QIcon::fromTheme(app->icon()));
866 
867             // Remove that app from the offers list (#242731)
868             for (int i = 0; i < offers.count(); ++i) {
869                 if (offers[i]->storageId() == app->storageId()) {
870                     offers.removeAt(i);
871                     break;
872                 }
873             }
874         } else {
875             runAct->setText(i18n("&Open"));
876         }
877 
878         QObject::connect(runAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotRunPreferredApplications);
879         topMenu->insertAction(before, runAct);
880 
881         m_traderConstraint = traderConstraint;
882         m_fileOpenList = m_props.items();
883     }
884 
885     QAction *openWithAct = new QAction(this);
886     openWithAct->setText(i18nc("@title:menu", "&Open With..."));
887     openWithAct->setObjectName(QStringLiteral("openwith_browse")); // For the unittest
888     QObject::connect(openWithAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotOpenWithDialog);
889 
890     if (!offers.isEmpty()) {
891         // Show the top app inline for files, but not folders
892         if (!isDir) {
893             QAction *act = createAppAction(offers.takeFirst(), true);
894             topMenu->insertAction(before, act);
895         }
896 
897         // If there are still more apps, show them in a sub-menu
898         if (!offers.isEmpty()) { // submenu 'open with'
899             QMenu *subMenu = new QMenu(i18nc("@title:menu", "&Open With"), topMenu);
900             subMenu->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
901             subMenu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // For the unittest
902             // Add other apps to the sub-menu
903             for (const KServicePtr &service : std::as_const(offers)) {
904                 QAction *act = createAppAction(service, false);
905                 subMenu->addAction(act);
906             }
907 
908             subMenu->addSeparator();
909 
910             openWithAct->setText(i18nc("@action:inmenu Open With", "&Other Application..."));
911             subMenu->addAction(openWithAct);
912 
913             topMenu->insertMenu(before, subMenu);
914             topMenu->insertSeparator(before);
915         } else { // No other apps
916             topMenu->insertAction(before, openWithAct);
917         }
918     } else { // no app offers -> Open With...
919         openWithAct->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
920         openWithAct->setObjectName(QStringLiteral("openwith")); // For the unittest
921         topMenu->insertAction(before, openWithAct);
922     }
923 }
924 
serviceMenuFilePaths()925 QStringList KFileItemActionsPrivate::serviceMenuFilePaths()
926 {
927     QStringList filePaths;
928 
929     // Use old KServiceTypeTrader code path
930 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 85)
931     const KService::List entries = KServiceTypeTrader::self()->query(QStringLiteral("KonqPopupMenu/Plugin"));
932     for (const KServicePtr &entry : entries) {
933         filePaths << QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kservices5/") + entry->entryPath());
934     }
935 #endif
936     // Load servicemenus from new install location
937     const QStringList paths =
938         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kio/servicemenus"), QStandardPaths::LocateDirectory);
939     filePaths << KFileUtils::findAllUniqueFiles(paths, QStringList(QStringLiteral("*.desktop")));
940     return filePaths;
941 }
942 
943 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 82)
preferredOpenWithAction(const QString & traderConstraint)944 QAction *KFileItemActions::preferredOpenWithAction(const QString &traderConstraint)
945 {
946     const KService::List offers = associatedApplications(d->m_mimeTypeList, traderConstraint);
947     if (offers.isEmpty()) {
948         return nullptr;
949     }
950     return d->createAppAction(offers.first(), true);
951 }
952 #endif
953 
setParentWidget(QWidget * widget)954 void KFileItemActions::setParentWidget(QWidget *widget)
955 {
956     d->m_parentWidget = widget;
957 }
958