diff --git a/src/gui/applicationlauncherjob.cpp b/src/gui/applicationlauncherjob.cpp index 0eaaa031..f46e6eaa 100644 --- a/src/gui/applicationlauncherjob.cpp +++ b/src/gui/applicationlauncherjob.cpp @@ -1,141 +1,141 @@ /* This file is part of the KDE libraries Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "applicationlauncherjob.h" #include "kprocessrunner_p.h" #include "kiogui_debug.h" class KIO::ApplicationLauncherJobPrivate { public: explicit ApplicationLauncherJobPrivate(const KService::Ptr &service) : m_service(service) {} void slotStarted(KIO::ApplicationLauncherJob *q, KProcessRunner *processRunner) { m_pids.append(processRunner->pid()); if (--m_numProcessesPending == 0) { q->emitResult(); } } KService::Ptr m_service; QList m_urls; KIO::ApplicationLauncherJob::RunFlags m_runFlags; QString m_suggestedFileName; QByteArray m_startupId; QVector m_pids; QVector m_processRunners; int m_numProcessesPending = 0; }; KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KService::Ptr &service, QObject *parent) : KJob(parent), d(new ApplicationLauncherJobPrivate(service)) { } -KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KService::Ptr &service, const KServiceAction &serviceAction, QObject *parent) - : ApplicationLauncherJob(service, parent) +KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KServiceAction &serviceAction, QObject *parent) + : ApplicationLauncherJob(serviceAction.service(), parent) { Q_ASSERT(d->m_service); d->m_service.detach(); d->m_service->setExec(serviceAction.exec()); } KIO::ApplicationLauncherJob::~ApplicationLauncherJob() { // Do *NOT* delete the KProcessRunner instances here. // We need it to keep running so it can terminate startup notification on process exit. } void KIO::ApplicationLauncherJob::setUrls(const QList &urls) { d->m_urls = urls; } void KIO::ApplicationLauncherJob::setRunFlags(RunFlags runFlags) { d->m_runFlags = runFlags; } void KIO::ApplicationLauncherJob::setSuggestedFileName(const QString &suggestedFileName) { d->m_suggestedFileName = suggestedFileName; } void KIO::ApplicationLauncherJob::setStartupId(const QByteArray &startupId) { d->m_startupId = startupId; } void KIO::ApplicationLauncherJob::start() { if (d->m_urls.count() > 1 && !d->m_service->allowMultipleFiles()) { // We need to launch the application N times. // We ignore the result for application 2 to N. // For the first file we launch the application in the // usual way. The reported result is based on this application. d->m_numProcessesPending = d->m_urls.count(); d->m_processRunners.reserve(d->m_numProcessesPending); for (int i = 1; i < d->m_urls.count(); ++i) { auto *processRunner = new KProcessRunner(d->m_service, { d->m_urls.at(i) }, d->m_runFlags, d->m_suggestedFileName, QByteArray()); d->m_processRunners.push_back(processRunner); connect(processRunner, &KProcessRunner::processStarted, this, [this, processRunner]() { d->slotStarted(this, processRunner); }); } d->m_urls = { d->m_urls.at(0) }; } else { d->m_numProcessesPending = 1; } auto *processRunner = new KProcessRunner(d->m_service, d->m_urls, d->m_runFlags, d->m_suggestedFileName, d->m_startupId); d->m_processRunners.push_back(processRunner); connect(processRunner, &KProcessRunner::error, this, [this](const QString &errorText) { setError(KJob::UserDefinedError); setErrorText(errorText); emitResult(); }); connect(processRunner, &KProcessRunner::processStarted, this, [this, processRunner]() { d->slotStarted(this, processRunner); }); } bool KIO::ApplicationLauncherJob::waitForStarted() { const bool ret = std::all_of(d->m_processRunners.cbegin(), d->m_processRunners.cend(), [](KProcessRunner *r) { return r->waitForStarted(); }); for (KProcessRunner *r : qAsConst(d->m_processRunners)) { qApp->sendPostedEvents(r); // so slotStarted gets called } return ret; } qint64 KIO::ApplicationLauncherJob::pid() const { return d->m_pids.at(0); } QVector KIO::ApplicationLauncherJob::pids() const { return d->m_pids; } diff --git a/src/gui/applicationlauncherjob.h b/src/gui/applicationlauncherjob.h index 4c476251..d251781b 100644 --- a/src/gui/applicationlauncherjob.h +++ b/src/gui/applicationlauncherjob.h @@ -1,157 +1,156 @@ /* This file is part of the KDE libraries Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIO_APPLICATIONLAUNCHERJOB_H #define KIO_APPLICATIONLAUNCHERJOB_H #include "kiogui_export.h" #include #include #include class KRunPrivate; // KF6 REMOVE class ApplicationLauncherJobTest; // KF6 REMOVE namespace KIO { class ApplicationLauncherJobPrivate; /** * @brief ApplicationLauncherJob runs an application and watches it while running. * * It creates a startup notification and finishes it on success or on error (for the taskbar). * It also emits an error message if necessary (e.g. "program not found"). * * Note that this class doesn't support warning the user if a desktop file or a binary * does not have the executable bit set and offering to make it so. Therefore file managers * should use KRun::runApplication rather than using ApplicationLauncherJob directly. * * When passing multiple URLs to an application that doesn't support opening * multiple files, the application will be launched once for each URL. * * The job finishes when the application is successfully started. At that point you can * query the PID(s). * * For error handling, either connect to the result() signal, or for a simple messagebox on error, * you can do * @code * auto *delegate = new KDialogJobUiDelegate; * delegate->setAutoErrorHandlingEnabled(true); * job->setUiDelegate(delegate); * @endcode * * @since 5.69 */ class KIOGUI_EXPORT ApplicationLauncherJob : public KJob { public: /** * @brief Creates a ApplicationLauncherJob * @param service the service (application desktop file) to run * @param parent the parent QObject */ explicit ApplicationLauncherJob(const KService::Ptr &service, QObject *parent = nullptr); /** * @brief Creates a ApplicationLauncherJob - * @param service the service (application desktop file) to run - * @param serviceAction the service action within the service to run + * @param serviceAction the service action to run * @param parent the parent QObject */ - explicit ApplicationLauncherJob(const KService::Ptr &service, const KServiceAction &serviceAction, QObject *parent = nullptr); + explicit ApplicationLauncherJob(const KServiceAction &serviceAction, QObject *parent = nullptr); /** * Destructor. * Note that jobs auto-delete themselves after emitting result. * Deleting/killing the job will not stop the started application. */ ~ApplicationLauncherJob() override; /** * @brief setUrls specifies the URLs to be passed to the application * @param urls list of files (local or remote) to open * * Note that when passing multiple URLs to an application that doesn't support opening * multiple files, the application will be launched once for each URL. */ void setUrls(const QList &urls); enum RunFlag { DeleteTemporaryFiles = 0x1, ///< the URLs passed to the service will be deleted when it exits (if the URLs are local files) }; Q_DECLARE_FLAGS(RunFlags, RunFlag) /** * @brief setRunFlags specifies various flags * @param runFlags the flags to be set. For instance, whether the URLs are temporary files that should be deleted after execution. */ void setRunFlags(RunFlags runFlags); /** * Sets the file name to use in the case of downloading the file to a tempfile * in order to give to a non-url-aware application. Some apps rely on the extension * to determine the mimetype of the file. Usually the file name comes from the URL, * but in the case of the HTTP Content-Disposition header, we need to override the * file name. * @param suggestedFileName the file name */ void setSuggestedFileName(const QString &suggestedFileName); /** * @brief setStartupId sets the startupId of the new application * @param startupId Application startup notification id, if any (otherwise ""). */ void setStartupId(const QByteArray &startupId); /** * @brief start starts the job. You must call this, after all the setters. */ void start() override; /** * @return the PID of the application that was started. * Convenience method for pids().at(0). You should only use this when specifying zero or one URL, * or when you are sure that the application supports opening multiple files. Otherwise use pids(). * Available after the job emits result(). */ qint64 pid() const; /** * @return the PIDs of the applications that were started. * Available after the job emits result(). */ QVector pids() const; private: friend class ::KRunPrivate; // KF6 REMOVE friend class ::ApplicationLauncherJobTest; // KF6 REMOVE /** * Blocks until the process has started. Only exists for KRun, will disappear in KF6. */ bool waitForStarted(); friend class ApplicationLauncherJobPrivate; QScopedPointer d; }; } // namespace KIO #endif diff --git a/src/widgets/kdesktopfileactions.cpp b/src/widgets/kdesktopfileactions.cpp index de8b415d..e257f44d 100644 --- a/src/widgets/kdesktopfileactions.cpp +++ b/src/widgets/kdesktopfileactions.cpp @@ -1,395 +1,403 @@ /* This file is part of the KDE libraries * Copyright (C) 1999 Waldo Bastian * David Faure * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation; * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. **/ #include "kdesktopfileactions.h" #include "config-kiowidgets.h" // KIO_NO_SOLID #include "../core/config-kmountpoint.h" // for HAVE_VOLMGT (yes I cheat a bit) #include "kio_widgets_debug.h" +#include #include "krun.h" #include "kautomount.h" #include #include +#include #include #include #include #include #include #if ! KIO_NO_SOLID //Solid #include #include #include #include #include #include #include #include #endif #include #include enum BuiltinServiceType { ST_MOUNT = 0x0E1B05B0, ST_UNMOUNT = 0x0E1B05B1 }; // random numbers static bool runFSDevice(const QUrl &_url, const KDesktopFile &cfg, const QByteArray &asn); static bool runApplication(const QUrl &_url, const QString &_serviceFile, const QByteArray &asn); static bool runLink(const QUrl &_url, const KDesktopFile &cfg, const QByteArray &asn); bool KDesktopFileActions::run(const QUrl &u, bool _is_local) { return runWithStartup(u, _is_local, QByteArray()); } bool KDesktopFileActions::runWithStartup(const QUrl &u, bool _is_local, const QByteArray &asn) { // It might be a security problem to run external untrusted desktop // entry files if (!_is_local) { return false; } if (u.fileName() == QLatin1String(".directory")) { // We cannot execute a .directory file. Open with a text editor instead. return KRun::runUrl(u, QStringLiteral("text/plain"), nullptr, KRun::RunFlags(), QString(), asn); } KDesktopFile cfg(u.toLocalFile()); if (!cfg.desktopGroup().hasKey("Type")) { QString tmp = i18n("The desktop entry file %1 " "has no Type=... entry.", u.toLocalFile()); KMessageBox::error(nullptr, tmp); return false; } //qDebug() << "TYPE = " << type.data(); if (cfg.hasDeviceType()) { return runFSDevice(u, cfg, asn); } else if (cfg.hasApplicationType() || (cfg.readType() == QLatin1String("Service") && !cfg.desktopGroup().readEntry("Exec").isEmpty())) { // for kio_settings return runApplication(u, u.toLocalFile(), asn); } else if (cfg.hasLinkType()) { return runLink(u, cfg, asn); } QString tmp = i18n("The desktop entry of type\n%1\nis unknown.", cfg.readType()); KMessageBox::error(nullptr, tmp); return false; } static bool runFSDevice(const QUrl &_url, const KDesktopFile &cfg, const QByteArray &asn) { bool retval = false; QString dev = cfg.readDevice(); if (dev.isEmpty()) { QString tmp = i18n("The desktop entry file\n%1\nis of type FSDevice but has no Dev=... entry.", _url.toLocalFile()); KMessageBox::error(nullptr, tmp); return retval; } KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByDevice(dev); // Is the device already mounted ? if (mp) { const QUrl mpURL = QUrl::fromLocalFile(mp->mountPoint()); // Open a new window retval = KRun::runUrl(mpURL, QStringLiteral("inode/directory"), nullptr /*TODO - window*/, KRun::RunFlags(KRun::RunExecutables), QString(), asn); } else { KConfigGroup cg = cfg.desktopGroup(); bool ro = cg.readEntry("ReadOnly", false); QString fstype = cg.readEntry("FSType"); if (fstype == QLatin1String("Default")) { // KDE-1 thing fstype.clear(); } QString point = cg.readEntry("MountPoint"); #ifndef Q_OS_WIN (void) new KAutoMount(ro, fstype.toLatin1(), dev, point, _url.toLocalFile()); #endif retval = false; } return retval; } static bool runApplication(const QUrl &_url, const QString &_serviceFile, const QByteArray &asn) { KService s(_serviceFile); if (!s.isValid()) { QString tmp = i18n("The desktop entry file\n%1\nis not valid.", _url.toString()); KMessageBox::error(nullptr, tmp); return false; } return KRun::runApplication(s, QList(), nullptr /*TODO - window*/, KRun::RunFlags{}, QString(), asn); } static bool runLink(const QUrl &_url, const KDesktopFile &cfg, const QByteArray &asn) { QString u = cfg.readUrl(); if (u.isEmpty()) { QString tmp = i18n("The desktop entry file\n%1\nis of type Link but has no URL=... entry.", _url.toString()); KMessageBox::error(nullptr, tmp); return false; } QUrl url = QUrl::fromUserInput(u); KRun *run = new KRun(url, (QWidget *)nullptr, true, asn); // X-KDE-LastOpenedWith holds the service desktop entry name that // was should be preferred for opening this URL if possible. // This is used by the Recent Documents menu for instance. QString lastOpenedWidth = cfg.desktopGroup().readEntry("X-KDE-LastOpenedWith"); if (!lastOpenedWidth.isEmpty()) { run->setPreferredService(lastOpenedWidth); } return false; } QList KDesktopFileActions::builtinServices(const QUrl &_url) { QList result; if (!_url.isLocalFile()) { return result; } bool offerMount = false; bool offerUnmount = false; KDesktopFile cfg(_url.toLocalFile()); if (cfg.hasDeviceType()) { // url to desktop file const QString dev = cfg.readDevice(); if (dev.isEmpty()) { QString tmp = i18n("The desktop entry file\n%1\nis of type FSDevice but has no Dev=... entry.", _url.toLocalFile()); KMessageBox::error(nullptr, tmp); return result; } KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByDevice(dev); if (mp) { offerUnmount = true; } else { offerMount = true; } } #if ! KIO_NO_SOLID else { // url to device Solid::Predicate predicate(Solid::DeviceInterface::Block, "device", _url.toLocalFile()); const QList devList = Solid::Device::listFromQuery(predicate, QString()); if (devList.empty()) { //qDebug() << "Device" << _url.toLocalFile() << "not found"; return result; } Solid::Device device = devList[0]; Solid::StorageAccess *access = device.as(); Solid::StorageDrive *drive = device.parent().as(); bool mounted = access && access->isAccessible(); if ((mounted || device.is()) && drive && drive->isRemovable()) { offerUnmount = true; } if (!mounted && ((drive && drive->isHotpluggable()) || device.is())) { offerMount = true; } } #endif if (offerMount) { KServiceAction mount(QStringLiteral("mount"), i18n("Mount"), QString(), QString(), false); mount.setData(QVariant(ST_MOUNT)); result.append(mount); } if (offerUnmount) { QString text; #ifdef HAVE_VOLMGT /* * Solaris' volume management can only umount+eject */ text = i18n("Eject"); #else text = i18n("Unmount"); #endif KServiceAction unmount(QStringLiteral("unmount"), text, QString(), QString(), false); unmount.setData(QVariant(ST_UNMOUNT)); result.append(unmount); } return result; } QList KDesktopFileActions::userDefinedServices(const QString &path, bool bLocalFiles) { KDesktopFile cfg(path); return userDefinedServices(path, cfg, bLocalFiles); } QList KDesktopFileActions::userDefinedServices(const QString &path, const KDesktopFile &cfg, bool bLocalFiles, const QList &file_list) { Q_UNUSED(path); // this was just for debugging; we use service.entryPath() now. KService service(&cfg); return userDefinedServices(service, bLocalFiles, file_list); } QList KDesktopFileActions::userDefinedServices(const KService &service, bool bLocalFiles, const QList &file_list) { QList result; if (!service.isValid()) { // e.g. TryExec failed return result; } QStringList keys; const QString actionMenu = service.property(QStringLiteral("X-KDE-GetActionMenu"), QVariant::String).toString(); if (!actionMenu.isEmpty()) { const QStringList dbuscall = actionMenu.split(QLatin1Char(' ')); if (dbuscall.count() >= 4) { const QString &app = dbuscall.at(0); const QString &object = dbuscall.at(1); const QString &interface = dbuscall.at(2); const QString &function = dbuscall.at(3); QDBusInterface remote(app, object, interface); // Do NOT use QDBus::BlockWithGui here. It runs a nested event loop, // in which timers can fire, leading to crashes like #149736. QDBusReply reply = remote.call(function, QUrl::toStringList(file_list)); keys = reply; // ensures that the reply was a QStringList if (keys.isEmpty()) { return result; } } else { qCWarning(KIO_WIDGETS) << "The desktop file" << service.entryPath() << "has an invalid X-KDE-GetActionMenu entry." << "Syntax is: app object interface function"; } } // Now, either keys is empty (all actions) or it's set to the actions we want const QList list = service.actions(); for (const KServiceAction &action : list) { if (keys.isEmpty() || keys.contains(action.name())) { const QString exec = action.exec(); if (bLocalFiles || exec.contains(QLatin1String("%U")) || exec.contains(QLatin1String("%u"))) { result.append(action); } } } return result; } void KDesktopFileActions::executeService(const QList &urls, const KServiceAction &action) { //qDebug() << "EXECUTING Service " << action.name(); int actionData = action.data().toInt(); if (actionData == ST_MOUNT || actionData == ST_UNMOUNT) { Q_ASSERT(urls.count() == 1); const QString path = urls.first().toLocalFile(); //qDebug() << "MOUNT&UNMOUNT"; KDesktopFile cfg(path); if (cfg.hasDeviceType()) { // path to desktop file const QString dev = cfg.readDevice(); if (dev.isEmpty()) { QString tmp = i18n("The desktop entry file\n%1\nis of type FSDevice but has no Dev=... entry.", path); KMessageBox::error(nullptr, tmp); return; } KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByDevice(dev); if (actionData == ST_MOUNT) { // Already mounted? Strange, but who knows ... if (mp) { //qDebug() << "ALREADY Mounted"; return; } const KConfigGroup group = cfg.desktopGroup(); bool ro = group.readEntry("ReadOnly", false); QString fstype = group.readEntry("FSType"); if (fstype == QLatin1String("Default")) { // KDE-1 thing fstype.clear(); } QString point = group.readEntry("MountPoint"); #ifndef Q_OS_WIN (void)new KAutoMount(ro, fstype.toLatin1(), dev, point, path, false); #endif } else if (actionData == ST_UNMOUNT) { // Not mounted? Strange, but who knows ... if (!mp) { return; } #ifndef Q_OS_WIN (void)new KAutoUnmount(mp->mountPoint(), path); #endif } } #if ! KIO_NO_SOLID else { // path to device Solid::Predicate predicate(Solid::DeviceInterface::Block, "device", path); const QList devList = Solid::Device::listFromQuery(predicate, QString()); if (!devList.empty()) { Solid::Device device = devList[0]; if (actionData == ST_MOUNT) { if (device.is()) { Solid::StorageAccess *access = device.as(); if (access) { access->setup(); } } } else if (actionData == ST_UNMOUNT) { if (device.is()) { Solid::OpticalDrive *drive = device.parent().as(); if (drive != 0) { drive->eject(); } } else if (device.is()) { Solid::StorageAccess *access = device.as(); if (access && access->isAccessible()) { access->teardown(); } } } } else { //qDebug() << "Device" << path << "not found"; } } #endif } else { - //qDebug() << action.name() << "first url's path=" << urls.first().toLocalFile() << "exec=" << action.exec(); - KRun::run(action.exec(), urls, nullptr, action.text(), action.icon()); - // The action may update the desktop file. Example: eject unmounts (#5129). - org::kde::KDirNotify::emitFilesChanged(urls); + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(action); + job->setUrls(urls); + QObject::connect(job, &KJob::result, qApp, [urls]() { + // The action may update the desktop file. Example: eject unmounts (#5129). + org::kde::KDirNotify::emitFilesChanged(urls); + }); + auto *delegate = new KDialogJobUiDelegate; + delegate->setAutoErrorHandlingEnabled(true); + job->setUiDelegate(delegate); + job->start(); } }