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

source code of kio/src/widgets/kfileitemactions.cpp