diff --git a/autotests/applicationlauncherjobtest.cpp b/autotests/applicationlauncherjobtest.cpp index c6b6d120..c10cbe79 100644 --- a/autotests/applicationlauncherjobtest.cpp +++ b/autotests/applicationlauncherjobtest.cpp @@ -1,223 +1,244 @@ /* This file is part of the KDE libraries Copyright (c) 2014, 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 "applicationlauncherjobtest.h" #include "applicationlauncherjob.h" #include "kiotesthelper.h" // createTestFile etc. #include #include #include #ifdef Q_OS_UNIX #include // kill #endif +#include #include #include #include #include QTEST_GUILESS_MAIN(ApplicationLauncherJobTest) void ApplicationLauncherJobTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); } void ApplicationLauncherJobTest::cleanupTestCase() { std::for_each(m_filesToRemove.begin(), m_filesToRemove.end(), [](const QString & f) { QFile::remove(f); }); } static const char s_tempServiceName[] = "applicationlauncherjobtest_service.desktop"; static void createSrcFile(const QString path) { QFile srcFile(path); QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString())); srcFile.write("Hello world\n"); } void ApplicationLauncherJobTest::startProcess_data() { QTest::addColumn("tempFile"); QTest::addColumn("useExec"); QTest::addColumn("numFiles"); QTest::newRow("1_file_exec") << false << true << 1; QTest::newRow("1_file_waitForStarted") << false << false << 1; QTest::newRow("1_tempfile_exec") << true << true << 1; QTest::newRow("1_tempfile_waitForStarted") << true << false << 1; QTest::newRow("2_files_exec") << false << true << 2; QTest::newRow("2_files_waitForStarted") << false << false << 2; QTest::newRow("2_tempfiles_exec") << true << true << 2; QTest::newRow("2_tempfiles_waitForStarted") << true << false << 2; } void ApplicationLauncherJobTest::startProcess() { QFETCH(bool, tempFile); QFETCH(bool, useExec); QFETCH(int, numFiles); // Given a service desktop file and a number of source files const QString path = createTempService(); QTemporaryDir tempDir; const QString srcDir = tempDir.path(); QList urls; for (int i = 0; i < numFiles; ++i) { const QString srcFile = srcDir + "/srcfile" + QString::number(i + 1); createSrcFile(srcFile); QVERIFY(QFile::exists(srcFile)); urls.append(QUrl::fromLocalFile(srcFile)); } // When running a ApplicationLauncherJob KService::Ptr servicePtr(new KService(path)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); job->setUrls(urls); if (tempFile) { job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); } if (useExec) { QVERIFY(job->exec()); } else { job->start(); QVERIFY(job->waitForStarted()); } const QVector pids = job->pids(); // Then the service should be executed (which copies the source file to "dest") QCOMPARE(pids.count(), numFiles); QVERIFY(!pids.contains(0)); for (int i = 0; i < numFiles; ++i) { const QString dest = srcDir + "/dest_srcfile" + QString::number(i + 1); QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); QVERIFY(QFile::exists(srcDir + "/srcfile" + QString::number(i + 1))); // if tempfile is true, kioexec will delete it... in 3 minutes. QVERIFY(QFile::remove(dest)); // cleanup } #ifdef Q_OS_UNIX // Kill the running kioexec processes for (qint64 pid : pids) { ::kill(pid, SIGTERM); } #endif // The kioexec processes that are waiting for 3 minutes and got killed above, // will now trigger KProcessRunner::slotProcessError, KProcessRunner::slotProcessExited and delete the KProcessRunner. // We wait for that to happen otherwise it gets confusing to see that output from later tests. QTRY_COMPARE(KProcessRunner::instanceCount(), 0); } void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile() { // Given a .desktop file in a temporary directory (outside the trusted paths) QTemporaryDir tempDir; const QString srcDir = tempDir.path(); const QString desktopFilePath = srcDir + "/shouldfail.desktop"; writeTempServiceDesktopFile(desktopFilePath); m_filesToRemove.append(desktopFilePath); const QString srcFile = srcDir + "/srcfile"; createSrcFile(srcFile); const QList urls{QUrl::fromLocalFile(srcFile)}; KService::Ptr servicePtr(new KService(desktopFilePath)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); job->setUrls(urls); QVERIFY(!job->exec()); QCOMPARE(job->error(), KJob::UserDefinedError); QCOMPARE(job->errorString(), QStringLiteral("You are not authorized to execute this file.")); } void ApplicationLauncherJobTest::shouldFailOnNonExistingExecutable_data() { QTest::addColumn("tempFile"); QTest::addColumn("fullPath"); QTest::newRow("file") << false << false; QTest::newRow("tempFile") << true << false; QTest::newRow("file_fullPath") << false << true; QTest::newRow("tempFile_fullPath") << true << true; } void ApplicationLauncherJobTest::shouldFailOnNonExistingExecutable() { QFETCH(bool, tempFile); QFETCH(bool, fullPath); const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/non_existing_executable.desktop"); KDesktopFile file(desktopFilePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestService"); group.writeEntry("Type", "Service"); if (fullPath) { group.writeEntry("Exec", "/usr/bin/does_not_exist %f %d/dest_%n"); } else { group.writeEntry("Exec", "does_not_exist %f %d/dest_%n"); } file.sync(); KService::Ptr servicePtr(new KService(desktopFilePath)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); job->setUrls({QUrl::fromLocalFile(desktopFilePath)}); // just to have one URL as argument, as the desktop file expects if (tempFile) { job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); } QVERIFY(!job->exec()); QCOMPARE(job->error(), KJob::UserDefinedError); if (fullPath) { QCOMPARE(job->errorString(), QStringLiteral("Could not find the program '/usr/bin/does_not_exist'")); } else { QCOMPARE(job->errorString(), QStringLiteral("Could not find the program 'does_not_exist'")); } QFile::remove(desktopFilePath); } +void ApplicationLauncherJobTest::shouldFailOnInvalidService() +{ + const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); + KDesktopFile file(desktopFilePath); + KConfigGroup group = file.desktopGroup(); + group.writeEntry("Name", "KRunUnittestService"); + group.writeEntry("Type", "NoSuchType"); + group.writeEntry("Exec", "does_not_exist"); + file.sync(); + + QTest::ignoreMessage(QtWarningMsg, QRegularExpression("The desktop entry file \".*\" has Type.*\"NoSuchType\" instead of \"Application\" or \"Service\"")); + KService::Ptr servicePtr(new KService(desktopFilePath)); + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); + QVERIFY(!job->exec()); + QCOMPARE(job->error(), KJob::UserDefinedError); + QCOMPARE(job->errorString(), QStringLiteral("The desktop entry file\n%1\nis not valid.").arg(desktopFilePath)); + + QFile::remove(desktopFilePath); +} + void ApplicationLauncherJobTest::writeTempServiceDesktopFile(const QString &filePath) { if (!QFile::exists(filePath)) { KDesktopFile file(filePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestService"); group.writeEntry("Type", "Service"); #ifdef Q_OS_WIN group.writeEntry("Exec", "copy.exe %f %d/dest_%n"); #else group.writeEntry("Exec", "cd %d ; cp %f %d/dest_%n"); // cd is just to show that we can't do QFile::exists(binary) #endif file.sync(); } } QString ApplicationLauncherJobTest::createTempService() { const QString fileName = s_tempServiceName; const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/") + fileName; writeTempServiceDesktopFile(fakeService); m_filesToRemove.append(fakeService); return fakeService; } diff --git a/autotests/applicationlauncherjobtest.h b/autotests/applicationlauncherjobtest.h index c1359f88..87a9a642 100644 --- a/autotests/applicationlauncherjobtest.h +++ b/autotests/applicationlauncherjobtest.h @@ -1,53 +1,55 @@ /* This file is part of the KDE libraries Copyright (c) 2014, 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 APPLICATIONLAUNCHERJOBTEST_H #define APPLICATIONLAUNCHERJOBTEST_H #include #include class ApplicationLauncherJobTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void startProcess_data(); void startProcess(); void shouldFailOnNonExecutableDesktopFile(); void shouldFailOnNonExistingExecutable_data(); void shouldFailOnNonExistingExecutable(); + void shouldFailOnInvalidService(); + private: QString createTempService(); void writeTempServiceDesktopFile(const QString &filePath); QStringList m_filesToRemove; }; #endif /* APPLICATIONLAUNCHERJOBTEST_H */ diff --git a/src/gui/kprocessrunner.cpp b/src/gui/kprocessrunner.cpp index b3316255..a4701a7e 100644 --- a/src/gui/kprocessrunner.cpp +++ b/src/gui/kprocessrunner.cpp @@ -1,418 +1,423 @@ /* 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 "kprocessrunner_p.h" #include "kiogui_debug.h" #include "config-kiogui.h" #include "desktopexecparser.h" #include "krecentdocument.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static int s_instanceCount = 0; // for the unittest static QString findNonExecutableProgram(const QString &executable) { // Relative to current dir, or absolute path const QFileInfo fi(executable); if (fi.exists() && !fi.isExecutable()) { return executable; } #ifdef Q_OS_UNIX // This is a *very* simplified version of QStandardPaths::findExecutable #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const auto skipEmptyParts = QString::SkipEmptyParts; #else const auto skipEmptyParts = Qt::SkipEmptyParts; #endif const QStringList searchPaths = QString::fromLocal8Bit(qgetenv("PATH")).split(QDir::listSeparator(), skipEmptyParts); for (const QString &searchPath : searchPaths) { const QString candidate = searchPath + QLatin1Char('/') + executable; const QFileInfo fileInfo(candidate); if (fileInfo.exists()) { if (fileInfo.isExecutable()) { qWarning() << "Internal program error. QStandardPaths::findExecutable couldn't find" << executable << "but our own logic found it at" << candidate << ". Please report a bug at https://bugs.kde.org"; } else { return candidate; } } } #endif return QString(); } KProcessRunner::KProcessRunner(const KService::Ptr &service, const QList &urls, KIO::ApplicationLauncherJob::RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) : m_process{new KProcess}, m_executable(KIO::DesktopExecParser::executablePath(service->exec())) { ++s_instanceCount; + + if (!service->isValid()) { + emitDelayedError(i18n("The desktop entry file\n%1\nis not valid.", service->entryPath())); + return; + } KIO::DesktopExecParser execParser(*service, urls); const QString realExecutable = execParser.resultingArguments().at(0); // realExecutable is a full path if DesktopExecParser was able to locate it. Otherwise it's still relative, which is a bad sign. if (QDir::isRelativePath(realExecutable) || !QFileInfo(realExecutable).isExecutable()) { // Does it really not exist, or is it non-executable? (bug #415567) const QString nonExecutable = findNonExecutableProgram(realExecutable); if (nonExecutable.isEmpty()) { emitDelayedError(i18n("Could not find the program '%1'", realExecutable)); } else { emitDelayedError(i18n("The program '%1' was found at '%2' but it is missing executable permissions.", realExecutable, nonExecutable)); } return; } execParser.setUrlsAreTempFiles(flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles); execParser.setSuggestedFileName(suggestedFileName); const QStringList args = execParser.resultingArguments(); if (args.isEmpty()) { emitDelayedError(i18n("Error processing Exec field in %1", service->entryPath())); return; } //qDebug() << "KProcess args=" << args; *m_process << args; enum DiscreteGpuCheck { NotChecked, Present, Absent }; static DiscreteGpuCheck s_gpuCheck = NotChecked; if (service->runOnDiscreteGpu() && s_gpuCheck == NotChecked) { // Check whether we have a discrete gpu bool hasDiscreteGpu = false; QDBusInterface iface(QStringLiteral("org.kde.Solid.PowerManagement"), QStringLiteral("/org/kde/Solid/PowerManagement"), QStringLiteral("org.kde.Solid.PowerManagement"), QDBusConnection::sessionBus()); if (iface.isValid()) { QDBusReply reply = iface.call(QStringLiteral("hasDualGpu")); if (reply.isValid()) { hasDiscreteGpu = reply.value(); } } s_gpuCheck = hasDiscreteGpu ? Present : Absent; } if (service->runOnDiscreteGpu() && s_gpuCheck == Present) { m_process->setEnv(QStringLiteral("DRI_PRIME"), QStringLiteral("1")); } QString workingDir(service->workingDirectory()); if (workingDir.isEmpty() && !urls.isEmpty() && urls.first().isLocalFile()) { workingDir = urls.first().adjusted(QUrl::RemoveFilename).toLocalFile(); } m_process->setWorkingDirectory(workingDir); if ((flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles) == 0) { // Remember we opened those urls, for the "recent documents" menu in kicker for (const QUrl &url : urls) { KRecentDocument::add(url, service->desktopEntryName()); } } init(service, service->name(), service->icon(), asn); } KProcessRunner::KProcessRunner(const QString &cmd, const QString &desktopName, const QString &execName, const QString &iconName, const QByteArray &asn, const QString &workingDirectory) : m_process{new KProcess}, m_executable(execName) { ++s_instanceCount; m_process->setShellCommand(cmd); if (!workingDirectory.isEmpty()) { m_process->setWorkingDirectory(workingDirectory); } if (!desktopName.isEmpty()) { KService::Ptr service = KService::serviceByDesktopName(desktopName); if (service) { if (m_executable.isEmpty()) { m_executable = KIO::DesktopExecParser::executablePath(service->exec()); } init(service, service->name(), service->icon(), asn); return; } } init(KService::Ptr(), execName /*user-visible name*/, iconName, asn); } void KProcessRunner::init(const KService::Ptr &service, const QString &userVisibleName, const QString &iconName, const QByteArray &asn) { if (service && !service->entryPath().isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(service->entryPath())) { - qCWarning(KIO_GUI) << "No authorization to execute " << service->entryPath(); + qCWarning(KIO_GUI) << "No authorization to execute" << service->entryPath(); emitDelayedError(i18n("You are not authorized to execute this file.")); return; } #if HAVE_X11 static bool isX11 = QGuiApplication::platformName() == QLatin1String("xcb"); if (isX11) { bool silent; QByteArray wmclass; const bool startup_notify = (asn != "0" && KIOGuiPrivate::checkStartupNotify(service.data(), &silent, &wmclass)); if (startup_notify) { m_startupId.initId(asn); m_startupId.setupStartupEnv(); KStartupInfoData data; data.setHostname(); // When it comes from a desktop file, m_executable can be a full shell command, so here is not 100% reliable. // E.g. it could be "cd", which isn't an existing binary. It's just a heuristic anyway. const QString bin = KIO::DesktopExecParser::executableName(m_executable); data.setBin(bin); if (!userVisibleName.isEmpty()) { data.setName(userVisibleName); } else if (service && !service->name().isEmpty()) { data.setName(service->name()); } data.setDescription(i18n("Launching %1", data.name())); if (!iconName.isEmpty()) { data.setIcon(iconName); } else if (service && !service->icon().isEmpty()) { data.setIcon(service->icon()); } if (!wmclass.isEmpty()) { data.setWMClass(wmclass); } if (silent) { data.setSilent(KStartupInfoData::Yes); } data.setDesktop(KWindowSystem::currentDesktop()); if (service && !service->entryPath().isEmpty()) { data.setApplicationId(service->entryPath()); } KStartupInfo::sendStartup(m_startupId, data); } } #else Q_UNUSED(bin); Q_UNUSED(userVisibleName); Q_UNUSED(iconName); #endif if (service) { m_scopeId = service->desktopEntryName(); } if (m_scopeId.isEmpty()) { m_scopeId = m_executable; } startProcess(); } void KProcessRunner::startProcess() { connect(m_process.get(), QOverload::of(&QProcess::finished), this, &KProcessRunner::slotProcessExited); connect(m_process.get(), &QProcess::started, this, &KProcessRunner::slotProcessStarted, Qt::QueuedConnection); connect(m_process.get(), &QProcess::errorOccurred, this, &KProcessRunner::slotProcessError); m_process->start(); } bool KProcessRunner::waitForStarted() { return m_process->waitForStarted(); } void KProcessRunner::slotProcessError(QProcess::ProcessError errorCode) { // E.g. the process crashed. // This is unlikely to happen while the ApplicationLauncherJob is still connected to the KProcessRunner. // So the emit does nothing, this is really just for debugging. qCDebug(KIO_GUI) << m_executable << "error=" << errorCode << m_process->errorString(); Q_EMIT error(m_process->errorString()); } void KProcessRunner::slotProcessStarted() { m_pid = m_process->processId(); registerCGroup(); #if HAVE_X11 if (!m_startupId.isNull() && m_pid) { KStartupInfoData data; data.addPid(m_pid); KStartupInfo::sendChange(m_startupId, data); KStartupInfo::resetStartupEnv(); } #endif emit processStarted(); } KProcessRunner::~KProcessRunner() { // This destructor deletes m_process, since it's a unique_ptr. --s_instanceCount; } int KProcessRunner::instanceCount() { return s_instanceCount; } qint64 KProcessRunner::pid() const { return m_pid; } void KProcessRunner::terminateStartupNotification() { #if HAVE_X11 if (!m_startupId.isNull()) { KStartupInfoData data; data.addPid(m_pid); // announce this pid for the startup notification has finished data.setHostname(); KStartupInfo::sendFinish(m_startupId, data); } #endif } void KProcessRunner::emitDelayedError(const QString &errorMsg) { terminateStartupNotification(); // Use delayed invocation so the caller has time to connect to the signal QMetaObject::invokeMethod(this, [this, errorMsg]() { emit error(errorMsg); deleteLater(); }, Qt::QueuedConnection); } void KProcessRunner::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus) { qCDebug(KIO_GUI) << m_executable << "exitCode=" << exitCode << "exitStatus=" << exitStatus; terminateStartupNotification(); deleteLater(); } void KProcessRunner::registerCGroup() { // As specified in "XDG standardization for applications" in https://systemd.io/DESKTOP_ENVIRONMENTS/ #ifdef Q_OS_LINUX if (!qEnvironmentVariableIsSet("KDE_APPLICATIONS_AS_SCOPE")) { return; } if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.freedesktop.systemd1"))) { return; } typedef QPair NamedVariant; typedef QList NamedVariantList; static std::once_flag dbusTypesRegistered; std::call_once(dbusTypesRegistered, []() { qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType>(); qDBusRegisterMetaType>>(); }); QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"), QStringLiteral("/org/freedesktop/systemd1"), QStringLiteral("org.freedesktop.systemd1.Manager"), QStringLiteral("StartTransientUnit")); // "-" is a special character in systemd representing a heirachical level. It should be escaped. const QString escapedScopeId = m_scopeId.replace(QLatin1Char('-'), QStringLiteral("\\x2d")); const QString name = QStringLiteral("apps-%1-%2.scope").arg(escapedScopeId, QUuid::createUuid().toString(QUuid::Id128)); // mode defines what to do in the case of a name conflict, in this case, just do nothing const QString mode = QStringLiteral("fail"); const QList pidList = {static_cast(m_process->pid())}; NamedVariantList properties = {NamedVariant({QStringLiteral("PIDs"), QDBusVariant(QVariant::fromValue(pidList))})}; QList> aux; message.setArguments({name, mode, QVariant::fromValue(properties), QVariant::fromValue(aux)}); QDBusPendingCall reply = QDBusConnection::sessionBus().asyncCall(message); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, qApp, [=]() { watcher->deleteLater(); if (reply.isError()) { qCWarning(KIO_GUI) << "Failed to register new cgroup:" << name; } else { qCDebug(KIO_GUI) << "Successfully registered new cgroup:" << name; } }); #endif } // This code is also used in klauncher (and KRun). bool KIOGuiPrivate::checkStartupNotify(const KService *service, bool *silent_arg, QByteArray *wmclass_arg) { bool silent = false; QByteArray wmclass; if (service && service->property(QStringLiteral("StartupNotify")).isValid()) { silent = !service->property(QStringLiteral("StartupNotify")).toBool(); wmclass = service->property(QStringLiteral("StartupWMClass")).toString().toLatin1(); } else if (service && service->property(QStringLiteral("X-KDE-StartupNotify")).isValid()) { silent = !service->property(QStringLiteral("X-KDE-StartupNotify")).toBool(); wmclass = service->property(QStringLiteral("X-KDE-WMClass")).toString().toLatin1(); } else { // non-compliant app if (service) { if (service->isApplication()) { // doesn't have .desktop entries needed, start as non-compliant wmclass = "0"; // krazy:exclude=doublequote_chars } else { return false; // no startup notification at all } } else { #if 0 // Create startup notification even for apps for which there shouldn't be any, // just without any visual feedback. This will ensure they'll be positioned on the proper // virtual desktop, and will get user timestamp from the ASN ID. wmclass = '0'; silent = true; #else // That unfortunately doesn't work, when the launched non-compliant application // launches another one that is compliant and there is any delay inbetween (bnc:#343359) return false; #endif } } if (silent_arg) { *silent_arg = silent; } if (wmclass_arg) { *wmclass_arg = wmclass; } return true; } diff --git a/src/widgets/kdesktopfileactions.cpp b/src/widgets/kdesktopfileactions.cpp index d2a26264..7e38aa7c 100644 --- a/src/widgets/kdesktopfileactions.cpp +++ b/src/widgets/kdesktopfileactions.cpp @@ -1,336 +1,323 @@ /* 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 "../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 #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); + KService service(u.toLocalFile()); + return KRun::runApplication(service, QList(), nullptr /*TODO - window*/, KRun::RunFlags{}, QString(), 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 (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; } // KF6 TODO add QWiget* parameter 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 /*TODO window*/, 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 } } } else { 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); }); job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr /*TODO window*/)); job->start(); } }