File indexing completed on 2024-06-16 06:56:49

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