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