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