File indexing completed on 2024-12-08 09:41:31

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2014 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "dropjob.h"
0009 
0010 #include "job_p.h"
0011 #include "jobuidelegate.h"
0012 #include "jobuidelegateextension.h"
0013 #include "kio_widgets_debug.h"
0014 #include "pastejob.h"
0015 #include "pastejob_p.h"
0016 
0017 #include <KConfigGroup>
0018 #include <KCoreDirLister>
0019 #include <KDesktopFile>
0020 #include <KFileItem>
0021 #include <KFileItemListProperties>
0022 #include <KIO/ApplicationLauncherJob>
0023 #include <KIO/CopyJob>
0024 #include <KIO/DndPopupMenuPlugin>
0025 #include <KIO/FileUndoManager>
0026 #include <KJobWidgets>
0027 #include <KLocalizedString>
0028 #include <KPluginFactory>
0029 #include <KPluginMetaData>
0030 #include <KProtocolManager>
0031 #include <KService>
0032 #include <KUrlMimeData>
0033 
0034 #include <QDBusPendingCall>
0035 #include <QDropEvent>
0036 #include <QFileInfo>
0037 #include <QMenu>
0038 #include <QMimeData>
0039 #include <QProcess>
0040 #include <QTimer>
0041 
0042 using namespace KIO;
0043 
0044 Q_DECLARE_METATYPE(Qt::DropAction)
0045 
0046 namespace KIO
0047 {
0048 class DropMenu;
0049 }
0050 
0051 class KIO::DropMenu : public QMenu
0052 {
0053     Q_OBJECT
0054 public:
0055     explicit DropMenu(QWidget *parent = nullptr);
0056     ~DropMenu() override;
0057 
0058     void addCancelAction();
0059     void addExtraActions(const QList<QAction *> &appActions, const QList<QAction *> &pluginActions);
0060 
0061 private:
0062     QList<QAction *> m_appActions;
0063     QList<QAction *> m_pluginActions;
0064     QAction *m_lastSeparator;
0065     QAction *m_extraActionsSeparator;
0066     QAction *m_cancelAction;
0067 };
0068 
0069 static const QString s_applicationSlashXDashKDEDashArkDashDnDExtractDashService = //
0070     QStringLiteral("application/x-kde-ark-dndextract-service");
0071 static const QString s_applicationSlashXDashKDEDashArkDashDnDExtractDashPath = //
0072     QStringLiteral("application/x-kde-ark-dndextract-path");
0073 
0074 class KIO::DropJobPrivate : public KIO::JobPrivate
0075 {
0076 public:
0077     DropJobPrivate(const QDropEvent *dropEvent, const QUrl &destUrl, DropJobFlags dropjobFlags, JobFlags flags)
0078         : JobPrivate()
0079         , m_mimeData(dropEvent->mimeData()) // Extract everything from the dropevent, since it will be deleted before the job starts
0080         , m_urls(KUrlMimeData::urlsFromMimeData(m_mimeData, KUrlMimeData::PreferLocalUrls, &m_metaData))
0081         , m_dropAction(dropEvent->dropAction())
0082         , m_relativePos(dropEvent->pos())
0083         , m_keyboardModifiers(dropEvent->keyboardModifiers())
0084         , m_hasArkFormat(m_mimeData->hasFormat(s_applicationSlashXDashKDEDashArkDashDnDExtractDashService)
0085                          && m_mimeData->hasFormat(s_applicationSlashXDashKDEDashArkDashDnDExtractDashPath))
0086         , m_destUrl(destUrl)
0087         , m_destItem(KCoreDirLister::cachedItemForUrl(destUrl))
0088         , m_flags(flags)
0089         , m_dropjobFlags(dropjobFlags)
0090         , m_triggered(false)
0091     {
0092         // Check for the drop of a bookmark -> we want a Link action
0093         if (m_mimeData->hasFormat(QStringLiteral("application/x-xbel"))) {
0094             m_keyboardModifiers |= Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier);
0095             m_dropAction = Qt::LinkAction;
0096         }
0097         if (m_destItem.isNull() && m_destUrl.isLocalFile()) {
0098             m_destItem = KFileItem(m_destUrl);
0099         }
0100 
0101         if (m_hasArkFormat) {
0102             m_remoteArkDBusClient = QString::fromUtf8(m_mimeData->data(s_applicationSlashXDashKDEDashArkDashDnDExtractDashService));
0103             m_remoteArkDBusPath = QString::fromUtf8(m_mimeData->data(s_applicationSlashXDashKDEDashArkDashDnDExtractDashPath));
0104         }
0105 
0106         if (!(m_flags & KIO::NoPrivilegeExecution)) {
0107             m_privilegeExecutionEnabled = true;
0108             switch (m_dropAction) {
0109             case Qt::CopyAction:
0110                 m_operationType = Copy;
0111                 break;
0112             case Qt::MoveAction:
0113                 m_operationType = Move;
0114                 break;
0115             case Qt::LinkAction:
0116                 m_operationType = Symlink;
0117                 break;
0118             default:
0119                 m_operationType = Other;
0120                 break;
0121             }
0122         }
0123     }
0124 
0125     bool destIsDirectory() const
0126     {
0127         if (!m_destItem.isNull()) {
0128             return m_destItem.isDir();
0129         }
0130         // We support local dir, remote dir, local desktop file, local executable.
0131         // So for remote URLs, we just assume they point to a directory, the user will get an error from KIO::copy if not.
0132         return true;
0133     }
0134     void handleCopyToDirectory();
0135     void slotDropActionDetermined(int error);
0136     void handleDropToDesktopFile();
0137     void handleDropToExecutable();
0138     void fillPopupMenu(KIO::DropMenu *popup);
0139     void addPluginActions(KIO::DropMenu *popup, const KFileItemListProperties &itemProps);
0140     void doCopyToDirectory();
0141 
0142     QPointer<const QMimeData> m_mimeData;
0143     const QList<QUrl> m_urls;
0144     QMap<QString, QString> m_metaData;
0145     Qt::DropAction m_dropAction;
0146     QPoint m_relativePos;
0147     Qt::KeyboardModifiers m_keyboardModifiers;
0148     bool m_hasArkFormat;
0149     QString m_remoteArkDBusClient;
0150     QString m_remoteArkDBusPath;
0151     QUrl m_destUrl;
0152     KFileItem m_destItem; // null for remote URLs not found in the dirlister cache
0153     const JobFlags m_flags;
0154     const DropJobFlags m_dropjobFlags;
0155     QList<QAction *> m_appActions;
0156     QList<QAction *> m_pluginActions;
0157     bool m_triggered; // Tracks whether an action has been triggered in the popup menu.
0158     QSet<KIO::DropMenu *> m_menus;
0159 
0160     Q_DECLARE_PUBLIC(DropJob)
0161 
0162     void slotStart();
0163     void slotTriggered(QAction *);
0164     void slotAboutToHide();
0165 
0166     static inline DropJob *newJob(const QDropEvent *dropEvent, const QUrl &destUrl, DropJobFlags dropjobFlags, JobFlags flags)
0167     {
0168         DropJob *job = new DropJob(*new DropJobPrivate(dropEvent, destUrl, dropjobFlags, flags));
0169         job->setUiDelegate(KIO::createDefaultJobUiDelegate());
0170         // Note: never KIO::getJobTracker()->registerJob here.
0171         // We don't want a progress dialog during the copy/move/link popup, it would in fact close
0172         // the popup
0173         return job;
0174     }
0175 };
0176 
0177 DropMenu::DropMenu(QWidget *parent)
0178     : QMenu(parent)
0179     , m_extraActionsSeparator(nullptr)
0180 {
0181     m_cancelAction = new QAction(i18n("C&ancel") + QLatin1Char('\t') + QKeySequence(Qt::Key_Escape).toString(QKeySequence::NativeText), this);
0182     m_cancelAction->setIcon(QIcon::fromTheme(QStringLiteral("process-stop")));
0183 
0184     m_lastSeparator = new QAction(this);
0185     m_lastSeparator->setSeparator(true);
0186 }
0187 
0188 DropMenu::~DropMenu()
0189 {
0190 }
0191 
0192 void DropMenu::addExtraActions(const QList<QAction *> &appActions, const QList<QAction *> &pluginActions)
0193 {
0194     removeAction(m_lastSeparator);
0195     removeAction(m_cancelAction);
0196 
0197     removeAction(m_extraActionsSeparator);
0198     for (QAction *action : std::as_const(m_appActions)) {
0199         removeAction(action);
0200     }
0201     for (QAction *action : std::as_const(m_pluginActions)) {
0202         removeAction(action);
0203     }
0204 
0205     m_appActions = appActions;
0206     m_pluginActions = pluginActions;
0207 
0208     if (!m_appActions.isEmpty() || !m_pluginActions.isEmpty()) {
0209         QAction *firstExtraAction = m_appActions.value(0, m_pluginActions.value(0, nullptr));
0210         if (firstExtraAction && !firstExtraAction->isSeparator()) {
0211             if (!m_extraActionsSeparator) {
0212                 m_extraActionsSeparator = new QAction(this);
0213                 m_extraActionsSeparator->setSeparator(true);
0214             }
0215             addAction(m_extraActionsSeparator);
0216         }
0217         addActions(appActions);
0218         addActions(pluginActions);
0219     }
0220 
0221     addAction(m_lastSeparator);
0222     addAction(m_cancelAction);
0223 }
0224 
0225 DropJob::DropJob(DropJobPrivate &dd)
0226     : Job(dd)
0227 {
0228     Q_D(DropJob);
0229 
0230     QTimer::singleShot(0, this, [d]() {
0231         d->slotStart();
0232     });
0233 }
0234 
0235 DropJob::~DropJob()
0236 {
0237 }
0238 
0239 void DropJobPrivate::slotStart()
0240 {
0241     Q_Q(DropJob);
0242 
0243     if (m_hasArkFormat) {
0244         QDBusMessage message = QDBusMessage::createMethodCall(m_remoteArkDBusClient,
0245                                                               m_remoteArkDBusPath,
0246                                                               QStringLiteral("org.kde.ark.DndExtract"),
0247                                                               QStringLiteral("extractSelectedFilesTo"));
0248         message.setArguments({m_destUrl.toDisplayString(QUrl::PreferLocalFile)});
0249         const auto pending = QDBusConnection::sessionBus().asyncCall(message);
0250         auto watcher = std::make_shared<QDBusPendingCallWatcher>(pending);
0251         QObject::connect(watcher.get(), &QDBusPendingCallWatcher::finished, q, [this, watcher] {
0252             Q_Q(DropJob);
0253 
0254             if (watcher->isError()) {
0255                 q->setError(KIO::ERR_UNKNOWN);
0256             }
0257             q->emitResult();
0258         });
0259 
0260         return;
0261     }
0262 
0263     if (!m_urls.isEmpty()) {
0264         if (destIsDirectory()) {
0265             handleCopyToDirectory();
0266         } else { // local file
0267             const QString destFile = m_destUrl.toLocalFile();
0268             if (KDesktopFile::isDesktopFile(destFile)) {
0269                 handleDropToDesktopFile();
0270             } else if (QFileInfo(destFile).isExecutable()) {
0271                 handleDropToExecutable();
0272             } else {
0273                 // should not happen, if KDirModel::flags is correct
0274                 q->setError(KIO::ERR_ACCESS_DENIED);
0275                 q->emitResult();
0276             }
0277         }
0278     } else if (m_mimeData) {
0279         // Dropping raw data
0280         KIO::PasteJob *job = KIO::PasteJobPrivate::newJob(m_mimeData, m_destUrl, KIO::HideProgressInfo, false /*not clipboard*/);
0281         QObject::connect(job, &KIO::PasteJob::itemCreated, q, &KIO::DropJob::itemCreated);
0282         q->addSubjob(job);
0283     }
0284 }
0285 
0286 void DropJobPrivate::fillPopupMenu(KIO::DropMenu *popup)
0287 {
0288     Q_Q(DropJob);
0289 
0290     // Check what the source can do
0291     // TODO: Determining the MIME type of the source URLs is difficult for remote URLs,
0292     // we would need to KIO::stat each URL in turn, asynchronously....
0293     KFileItemList fileItems;
0294     fileItems.reserve(m_urls.size());
0295     for (const QUrl &url : m_urls) {
0296         fileItems.append(KFileItem(url));
0297     }
0298     const KFileItemListProperties itemProps(fileItems);
0299 
0300     Q_EMIT q->popupMenuAboutToShow(itemProps);
0301 
0302     const bool sReading = itemProps.supportsReading();
0303     const bool sDeleting = itemProps.supportsDeleting();
0304     const bool sMoving = itemProps.supportsMoving();
0305 
0306     const int separatorLength = QCoreApplication::translate("QShortcut", "+").size();
0307     QString seq = QKeySequence(Qt::ShiftModifier).toString(QKeySequence::NativeText);
0308     seq.chop(separatorLength); // chop superfluous '+'
0309     QAction *popupMoveAction = new QAction(i18n("&Move Here") + QLatin1Char('\t') + seq, popup);
0310     popupMoveAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-move"), QIcon::fromTheme(QStringLiteral("go-jump"))));
0311     popupMoveAction->setData(QVariant::fromValue(Qt::MoveAction));
0312     seq = QKeySequence(Qt::ControlModifier).toString(QKeySequence::NativeText);
0313     seq.chop(separatorLength);
0314     QAction *popupCopyAction = new QAction(i18n("&Copy Here") + QLatin1Char('\t') + seq, popup);
0315     popupCopyAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
0316     popupCopyAction->setData(QVariant::fromValue(Qt::CopyAction));
0317     seq = QKeySequence(Qt::ControlModifier | Qt::ShiftModifier).toString(QKeySequence::NativeText);
0318     seq.chop(separatorLength);
0319     QAction *popupLinkAction = new QAction(i18n("&Link Here") + QLatin1Char('\t') + seq, popup);
0320     popupLinkAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-link")));
0321     popupLinkAction->setData(QVariant::fromValue(Qt::LinkAction));
0322 
0323     if (sMoving || (sReading && sDeleting)) {
0324         const bool equalDestination = std::all_of(m_urls.cbegin(), m_urls.cend(), [this](const QUrl &src) {
0325             return m_destUrl.matches(src.adjusted(QUrl::RemoveFilename), QUrl::StripTrailingSlash);
0326         });
0327 
0328         if (!equalDestination) {
0329             popup->addAction(popupMoveAction);
0330         }
0331     }
0332 
0333     if (sReading) {
0334         popup->addAction(popupCopyAction);
0335     }
0336 
0337     popup->addAction(popupLinkAction);
0338 
0339     addPluginActions(popup, itemProps);
0340 }
0341 
0342 void DropJobPrivate::addPluginActions(KIO::DropMenu *popup, const KFileItemListProperties &itemProps)
0343 {
0344     const QVector<KPluginMetaData> plugin_offers = KPluginMetaData::findPlugins(QStringLiteral("kf" QT_STRINGIFY(QT_VERSION_MAJOR) "/kio_dnd"));
0345     for (const KPluginMetaData &data : plugin_offers) {
0346         if (auto plugin = KPluginFactory::instantiatePlugin<KIO::DndPopupMenuPlugin>(data).plugin) {
0347             const auto actions = plugin->setup(itemProps, m_destUrl);
0348             for (auto action : actions) {
0349                 action->setParent(popup);
0350             }
0351             m_pluginActions += actions;
0352         }
0353     }
0354 
0355     popup->addExtraActions(m_appActions, m_pluginActions);
0356 }
0357 
0358 void DropJob::setApplicationActions(const QList<QAction *> &actions)
0359 {
0360     Q_D(DropJob);
0361 
0362     d->m_appActions = actions;
0363 
0364     for (KIO::DropMenu *menu : std::as_const(d->m_menus)) {
0365         menu->addExtraActions(d->m_appActions, d->m_pluginActions);
0366     }
0367 }
0368 
0369 void DropJob::showMenu(const QPoint &p, QAction *atAction)
0370 {
0371     Q_D(DropJob);
0372 
0373     if (!(d->m_dropjobFlags & KIO::ShowMenuManually)) {
0374         return;
0375     }
0376 
0377     for (KIO::DropMenu *menu : std::as_const(d->m_menus)) {
0378         menu->popup(p, atAction);
0379     }
0380 }
0381 
0382 void DropJobPrivate::slotTriggered(QAction *action)
0383 {
0384     Q_Q(DropJob);
0385     if (m_appActions.contains(action) || m_pluginActions.contains(action)) {
0386         q->emitResult();
0387         return;
0388     }
0389     const QVariant data = action->data();
0390     if (!data.canConvert<Qt::DropAction>()) {
0391         q->setError(KIO::ERR_USER_CANCELED);
0392         q->emitResult();
0393         return;
0394     }
0395     m_dropAction = data.value<Qt::DropAction>();
0396     doCopyToDirectory();
0397 }
0398 
0399 void DropJobPrivate::slotAboutToHide()
0400 {
0401     Q_Q(DropJob);
0402     // QMenu emits aboutToHide before triggered.
0403     // So we need to give the menu time in case it needs to emit triggered.
0404     // If it does, the cleanup will be done by slotTriggered.
0405     QTimer::singleShot(0, q, [=]() {
0406         if (!m_triggered) {
0407             q->setError(KIO::ERR_USER_CANCELED);
0408             q->emitResult();
0409         }
0410     });
0411 }
0412 
0413 void DropJobPrivate::handleCopyToDirectory()
0414 {
0415     Q_Q(DropJob);
0416 
0417     // Process m_dropAction as set by Qt at the time of the drop event
0418     if (!KProtocolManager::supportsWriting(m_destUrl)) {
0419         slotDropActionDetermined(KIO::ERR_CANNOT_WRITE);
0420         return;
0421     }
0422 
0423     if (!m_destItem.isNull() && !m_destItem.isWritable() && (m_flags & KIO::NoPrivilegeExecution)) {
0424         slotDropActionDetermined(KIO::ERR_WRITE_ACCESS_DENIED);
0425         return;
0426     }
0427 
0428     bool allItemsAreFromTrash = true;
0429     bool containsTrashRoot = false;
0430     for (const QUrl &url : m_urls) {
0431         const bool local = url.isLocalFile();
0432         if (!local /*optimization*/ && url.scheme() == QLatin1String("trash")) {
0433             if (url.path().isEmpty() || url.path() == QLatin1String("/")) {
0434                 containsTrashRoot = true;
0435             }
0436         } else {
0437             allItemsAreFromTrash = false;
0438         }
0439         if (url.matches(m_destUrl, QUrl::StripTrailingSlash)) {
0440             slotDropActionDetermined(KIO::ERR_DROP_ON_ITSELF);
0441             return;
0442         }
0443     }
0444 
0445     const bool trashing = m_destUrl.scheme() == QLatin1String("trash");
0446     if (trashing) {
0447         if (allItemsAreFromTrash) {
0448             qCDebug(KIO_WIDGETS) << "Dropping items from trash to trash";
0449             slotDropActionDetermined(KIO::ERR_DROP_ON_ITSELF);
0450             return;
0451         }
0452         m_dropAction = Qt::MoveAction;
0453 
0454         auto *askUserInterface = KIO::delegateExtension<AskUserActionInterface *>(q);
0455 
0456         // No UI Delegate set for this job, or a delegate that doesn't implement
0457         // AskUserActionInterface, then just proceed with the job without asking.
0458         // This is useful for non-interactive usage, (which doesn't actually apply
0459         // here as a DropJob is always interactive), but this is useful for unittests,
0460         // which are typically non-interactive.
0461         if (!askUserInterface) {
0462             slotDropActionDetermined(KJob::NoError);
0463             return;
0464         }
0465 
0466         QObject::connect(askUserInterface, &KIO::AskUserActionInterface::askUserDeleteResult, q, [this](bool allowDelete) {
0467             if (allowDelete) {
0468                 slotDropActionDetermined(KJob::NoError);
0469             } else {
0470                 slotDropActionDetermined(KIO::ERR_USER_CANCELED);
0471             }
0472         });
0473 
0474         askUserInterface->askUserDelete(m_urls, KIO::AskUserActionInterface::Trash, KIO::AskUserActionInterface::DefaultConfirmation, KJobWidgets::window(q));
0475         return;
0476     }
0477 
0478     // If we can't determine the action below, we use ERR::UNKNOWN as we need to ask
0479     // the user via a popup menu.
0480     int err = KIO::ERR_UNKNOWN;
0481     const bool implicitCopy = m_destUrl.scheme() == QLatin1String("stash");
0482     if (implicitCopy) {
0483         m_dropAction = Qt::CopyAction;
0484         err = KJob::NoError; // Ok
0485     } else if (containsTrashRoot) {
0486         // Dropping a link to the trash: don't move the full contents, just make a link (#319660)
0487         m_dropAction = Qt::LinkAction;
0488         err = KJob::NoError; // Ok
0489     } else if (allItemsAreFromTrash) {
0490         // No point in asking copy/move/link when using dragging from the trash, just move the file out.
0491         m_dropAction = Qt::MoveAction;
0492         err = KJob::NoError; // Ok
0493     } else if (m_keyboardModifiers & (Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier)) {
0494         // Qt determined m_dropAction from the modifiers already
0495         err = KJob::NoError; // Ok
0496     }
0497     slotDropActionDetermined(err);
0498 }
0499 
0500 void DropJobPrivate::slotDropActionDetermined(int error)
0501 {
0502     Q_Q(DropJob);
0503 
0504     if (error == KJob::NoError) {
0505         doCopyToDirectory();
0506         return;
0507     }
0508 
0509     // There was an error, handle it
0510     if (error == KIO::ERR_UNKNOWN) {
0511         auto *window = KJobWidgets::window(q);
0512         KIO::DropMenu *menu = new KIO::DropMenu(window);
0513         QObject::connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater);
0514 
0515         // If the user clicks outside the menu, it will be destroyed without emitting the triggered signal.
0516         QObject::connect(menu, &QMenu::aboutToHide, q, [this]() {
0517             slotAboutToHide();
0518         });
0519 
0520         fillPopupMenu(menu);
0521         QObject::connect(menu, &QMenu::triggered, q, [this](QAction *action) {
0522             m_triggered = true;
0523             slotTriggered(action);
0524         });
0525 
0526         if (!(m_dropjobFlags & KIO::ShowMenuManually)) {
0527             menu->popup(window ? window->mapToGlobal(m_relativePos) : QCursor::pos());
0528         }
0529         m_menus.insert(menu);
0530         QObject::connect(menu, &QObject::destroyed, q, [this, menu]() {
0531             m_menus.remove(menu);
0532         });
0533     } else {
0534         q->setError(error);
0535         q->emitResult();
0536     }
0537 }
0538 
0539 void DropJobPrivate::doCopyToDirectory()
0540 {
0541     Q_Q(DropJob);
0542     KIO::CopyJob *job = nullptr;
0543     switch (m_dropAction) {
0544     case Qt::MoveAction:
0545         job = KIO::move(m_urls, m_destUrl, m_flags);
0546         KIO::FileUndoManager::self()->recordJob(m_destUrl.scheme() == QLatin1String("trash") ? KIO::FileUndoManager::Trash : KIO::FileUndoManager::Move,
0547                                                 m_urls,
0548                                                 m_destUrl,
0549                                                 job);
0550         break;
0551     case Qt::CopyAction:
0552         job = KIO::copy(m_urls, m_destUrl, m_flags);
0553         KIO::FileUndoManager::self()->recordCopyJob(job);
0554         break;
0555     case Qt::LinkAction:
0556         job = KIO::link(m_urls, m_destUrl, m_flags);
0557         KIO::FileUndoManager::self()->recordCopyJob(job);
0558         break;
0559     default:
0560         qCWarning(KIO_WIDGETS) << "Unknown drop action" << int(m_dropAction);
0561         q->setError(KIO::ERR_UNSUPPORTED_ACTION);
0562         q->emitResult();
0563         return;
0564     }
0565     Q_ASSERT(job);
0566     job->setParentJob(q);
0567     job->setMetaData(m_metaData);
0568     QObject::connect(job, &KIO::CopyJob::copyingDone, q, [q](KIO::Job *, const QUrl &, const QUrl &to) {
0569         Q_EMIT q->itemCreated(to);
0570     });
0571     QObject::connect(job, &KIO::CopyJob::copyingLinkDone, q, [q](KIO::Job *, const QUrl &, const QString &, const QUrl &to) {
0572         Q_EMIT q->itemCreated(to);
0573     });
0574     q->addSubjob(job);
0575 
0576     Q_EMIT q->copyJobStarted(job);
0577 }
0578 
0579 void DropJobPrivate::handleDropToDesktopFile()
0580 {
0581     Q_Q(DropJob);
0582     const QString urlKey = QStringLiteral("URL");
0583     const QString destFile = m_destUrl.toLocalFile();
0584     const KDesktopFile desktopFile(destFile);
0585     const KConfigGroup desktopGroup = desktopFile.desktopGroup();
0586     if (desktopFile.hasApplicationType()) {
0587         // Drop to application -> start app with urls as argument
0588         KService::Ptr service(new KService(destFile));
0589         // Can't use setParentJob() because ApplicationLauncherJob isn't a KIO::Job,
0590         // instead pass q as parent so that KIO::delegateExtension() can find a delegate
0591         KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service, q);
0592         job->setUrls(m_urls);
0593         QObject::connect(job, &KJob::result, q, [=]() {
0594             if (job->error()) {
0595                 q->setError(KIO::ERR_CANNOT_LAUNCH_PROCESS);
0596                 q->setErrorText(destFile);
0597             }
0598             q->emitResult();
0599         });
0600         job->start();
0601     } else if (desktopFile.hasLinkType() && desktopGroup.hasKey(urlKey)) {
0602         // Drop to link -> adjust destination directory
0603         m_destUrl = QUrl::fromUserInput(desktopGroup.readPathEntry(urlKey, QString()));
0604         handleCopyToDirectory();
0605     } else {
0606         if (desktopFile.hasDeviceType()) {
0607             qCWarning(KIO_WIDGETS) << "Not re-implemented; please email kde-frameworks-devel@kde.org if you need this.";
0608             // take code from libkonq's old konq_operations.cpp
0609             // for now, fallback
0610         }
0611         // Some other kind of .desktop file (service, servicetype...)
0612         q->setError(KIO::ERR_UNSUPPORTED_ACTION);
0613         q->emitResult();
0614     }
0615 }
0616 
0617 void DropJobPrivate::handleDropToExecutable()
0618 {
0619     Q_Q(DropJob);
0620     // Launch executable for each of the files
0621     QStringList args;
0622     args.reserve(m_urls.size());
0623     for (const QUrl &url : std::as_const(m_urls)) {
0624         args << url.toLocalFile(); // assume local files
0625     }
0626     QProcess::startDetached(m_destUrl.toLocalFile(), args);
0627     q->emitResult();
0628 }
0629 
0630 void DropJob::slotResult(KJob *job)
0631 {
0632     if (job->error()) {
0633         KIO::Job::slotResult(job); // will set the error and emit result(this)
0634         return;
0635     }
0636     removeSubjob(job);
0637     emitResult();
0638 }
0639 
0640 DropJob *KIO::drop(const QDropEvent *dropEvent, const QUrl &destUrl, JobFlags flags)
0641 {
0642     return DropJobPrivate::newJob(dropEvent, destUrl, KIO::DropJobDefaultFlags, flags);
0643 }
0644 
0645 DropJob *KIO::drop(const QDropEvent *dropEvent, const QUrl &destUrl, DropJobFlags dropjobFlags, JobFlags flags)
0646 {
0647     return DropJobPrivate::newJob(dropEvent, destUrl, dropjobFlags, flags);
0648 }
0649 
0650 #include "dropjob.moc"
0651 #include "moc_dropjob.cpp"