diff --git a/autotests/applicationlauncherjobtest.cpp b/autotests/applicationlauncherjobtest.cpp index ea6cfd52..73646c86 100644 --- a/autotests/applicationlauncherjobtest.cpp +++ b/autotests/applicationlauncherjobtest.cpp @@ -1,314 +1,366 @@ /* 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 #include #include "kiotesthelper.h" // createTestFile etc. #include #include #include #ifdef Q_OS_UNIX #include // kill #endif #include #include #include #include QTEST_GUILESS_MAIN(ApplicationLauncherJobTest) namespace KIO { KIOGUI_EXPORT void setDefaultUntrustedProgramHandler(KIO::UntrustedProgramHandlerInterface *iface); } class TestUntrustedProgramHandler : public KIO::UntrustedProgramHandlerInterface { public: void showUntrustedProgramWarning(KJob *job, const QString &programName) override { Q_UNUSED(job) m_calls << programName; Q_EMIT result(m_retVal); } void setRetVal(bool b) { m_retVal = b; } QStringList m_calls; bool m_retVal = false; }; static TestUntrustedProgramHandler s_handler; 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()); + QVERIFY2(job->exec(), qPrintable(job->errorString())); } 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_data() { QTest::addColumn("withHandler"); QTest::addColumn("handlerRetVal"); QTest::addColumn("useExec"); QTest::newRow("no_handler_exec") << false << false << true; QTest::newRow("handler_false_exec") << true << false << true; QTest::newRow("handler_true_exec") << true << true << true; QTest::newRow("no_handler_waitForStarted") << false << false << false; QTest::newRow("handler_false_waitForStarted") << true << false << false; QTest::newRow("handler_true_waitForStarted") << true << true << false; } void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile() { QFETCH(bool, useExec); QFETCH(bool, withHandler); QFETCH(bool, handlerRetVal); // 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)); s_handler.m_calls.clear(); s_handler.setRetVal(handlerRetVal); KIO::setDefaultUntrustedProgramHandler(withHandler ? &s_handler : nullptr); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); job->setUrls(urls); bool success; if (useExec) { success = job->exec(); } else { job->start(); success = job->waitForStarted(); } if (!withHandler) { QVERIFY(!success); QCOMPARE(job->error(), KJob::UserDefinedError); QCOMPARE(job->errorString(), QStringLiteral("You are not authorized to execute this file.")); } else { if (handlerRetVal) { QVERIFY(success); // The actual shell process will race against the deletion of the QTemporaryDir, // so don't be surprised by stderr like getcwd: cannot access parent directories: No such file or directory QTest::qWait(50); // this helps a bit } else { QVERIFY(!success); QCOMPARE(job->error(), KIO::ERR_USER_CANCELED); } } if (withHandler) { // check that the handler was called QCOMPARE(s_handler.m_calls.count(), 1); QCOMPARE(s_handler.m_calls.at(0), QStringLiteral("KRunUnittestService")); } } 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::shouldFailOnServiceWithNoExec() +{ + const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); + KDesktopFile file(desktopFilePath); + KConfigGroup group = file.desktopGroup(); + group.writeEntry("Name", "KRunUnittestServiceNoExec"); + group.writeEntry("Type", "Service"); + file.sync(); + + QTest::ignoreMessage(QtWarningMsg, qPrintable(QString("No Exec field in \"%1\"").arg(desktopFilePath))); + 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("No Exec field in %1").arg(desktopFilePath)); + + QFile::remove(desktopFilePath); +} + +void ApplicationLauncherJobTest::shouldFailOnExecutableWithoutPermissions() +{ +#ifdef Q_OS_UNIX + // Given an executable shell script that copies "src" to "dest" (we'll cheat with the mimetype to treat it like a native binary) + QTemporaryDir tempDir; + const QString dir = tempDir.path(); + const QString scriptFilePath = dir + QStringLiteral("/script.sh"); + QFile scriptFile(scriptFilePath); + QVERIFY(scriptFile.open(QIODevice::WriteOnly)); + scriptFile.write("#!/bin/sh\ncp src dest"); + scriptFile.close(); + // Note that it's missing executable permissions + + const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); + KDesktopFile file(desktopFilePath); + KConfigGroup group = file.desktopGroup(); + group.writeEntry("Name", "KRunUnittestServiceNoPermission"); + group.writeEntry("Type", "Service"); + group.writeEntry("Exec", scriptFilePath); + file.sync(); + + 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 program '%1' is missing executable permissions.").arg(scriptFilePath)); + + QFile::remove(desktopFilePath); +#else + QSKIP("This test is not run on Windows"); +#endif +} + 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 722944b8..17a105cf 100644 --- a/autotests/applicationlauncherjobtest.h +++ b/autotests/applicationlauncherjobtest.h @@ -1,56 +1,58 @@ /* 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_data(); void shouldFailOnNonExecutableDesktopFile(); void shouldFailOnNonExistingExecutable_data(); void shouldFailOnNonExistingExecutable(); void shouldFailOnInvalidService(); + void shouldFailOnServiceWithNoExec(); + void shouldFailOnExecutableWithoutPermissions(); private: QString createTempService(); void writeTempServiceDesktopFile(const QString &filePath); QStringList m_filesToRemove; }; #endif /* APPLICATIONLAUNCHERJOBTEST_H */ diff --git a/autotests/krununittest.cpp b/autotests/krununittest.cpp index 67231377..4749c74f 100644 --- a/autotests/krununittest.cpp +++ b/autotests/krununittest.cpp @@ -1,445 +1,444 @@ /* * Copyright (C) 2003 Waldo Bastian * Copyright (C) 2007, 2009 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 as published by the Free Software Foundation; either * version 2 of the License, or (at your option) 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 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. */ #undef QT_USE_FAST_OPERATOR_PLUS #include "krununittest.h" #include QTEST_GUILESS_MAIN(KRunUnitTest) #include #include "krun.h" #include #include #include #include #include #include #include #include #include #include "kiotesthelper.h" // createTestFile etc. #ifdef Q_OS_UNIX #include // kill #endif void KRunUnitTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); // testProcessDesktopExec works only if your terminal application is set to "xterm" KConfigGroup cg(KSharedConfig::openConfig(), "General"); cg.writeEntry("TerminalApplication", "xterm"); // Determine the full path of sh - this is needed to make testProcessDesktopExecNoFile() // pass on systems where QStandardPaths::findExecutable("sh") is not "/bin/sh". m_sh = QStandardPaths::findExecutable(QStringLiteral("sh")); if (m_sh.isEmpty()) { m_sh = QStringLiteral("/bin/sh"); } } void KRunUnitTest::cleanupTestCase() { std::for_each(m_filesToRemove.begin(), m_filesToRemove.end(), [](const QString & f) { QFile::remove(f); }); } void KRunUnitTest::testExecutableName_data() { QTest::addColumn("execLine"); QTest::addColumn("expectedPath"); QTest::addColumn("expectedName"); QTest::newRow("/usr/bin/ls") << "/usr/bin/ls" << "/usr/bin/ls" << "ls"; QTest::newRow("/path/to/wine \"long argument with path\"") << "/path/to/wine \"long argument with path\"" << "/path/to/wine" << "wine"; QTest::newRow("/path/with/a/sp\\ ace/exe arg1 arg2") << "/path/with/a/sp\\ ace/exe arg1 arg2" << "/path/with/a/sp ace/exe" << "exe"; QTest::newRow("\"progname\" \"arg1\"") << "\"progname\" \"arg1\"" << "progname" << "progname"; QTest::newRow("'quoted' \"arg1\"") << "'quoted' \"arg1\"" << "quoted" << "quoted"; QTest::newRow(" 'leading space' arg1") << " 'leading space' arg1" << "leading space" << "leading space"; - QTest::newRow("if_command") << "if test -e /tmp/foo; then kwrite ; else konsole ; fi" << "if" << "if"; + QTest::newRow("if_command") << "if test -e /tmp/foo; then kwrite ; else konsole ; fi" << "" << ""; // "if" isn't a known executable, so this is good... } void KRunUnitTest::testExecutableName() { QFETCH(QString, execLine); QFETCH(QString, expectedPath); QFETCH(QString, expectedName); QCOMPARE(KIO::DesktopExecParser::executableName(execLine), expectedName); QCOMPARE(KIO::DesktopExecParser::executablePath(execLine), expectedPath); } //static const char *bt(bool tr) { return tr?"true":"false"; } static void checkDesktopExecParser(const char *exec, const char *term, const char *sus, const QList &urls, bool tf, const QString &b) { QFile out(QStringLiteral("kruntest.desktop")); if (!out.open(QIODevice::WriteOnly)) { abort(); } QByteArray str("[Desktop Entry]\n" "Type=Application\n" "Name=just_a_test\n" "Icon=~/icon.png\n"); str += QByteArray(exec) + '\n'; str += QByteArray(term) + '\n'; str += QByteArray(sus) + '\n'; out.write(str); out.close(); KService service(QDir::currentPath() + "/kruntest.desktop"); /*qDebug() << QString().sprintf( "processDesktopExec( " "service = {\nexec = %s\nterminal = %s, terminalOptions = %s\nsubstituteUid = %s, user = %s }," "\nURLs = { %s },\ntemp_files = %s )", service.exec().toLatin1().constData(), bt(service.terminal()), service.terminalOptions().toLatin1().constData(), bt(service.substituteUid()), service.username().toLatin1().constData(), KShell::joinArgs(urls.toStringList()).toLatin1().constData(), bt(tf)); */ KIO::DesktopExecParser parser(service, urls); parser.setUrlsAreTempFiles(tf); QCOMPARE(KShell::joinArgs(parser.resultingArguments()), b); QFile::remove(QStringLiteral("kruntest.desktop")); } void KRunUnitTest::testProcessDesktopExec() { QList l0; static const char *const execs[] = { "Exec=date -u", "Exec=echo $PWD" }; static const char *const terms[] = { "Terminal=false", "Terminal=true\nTerminalOptions=-T \"%f - %c\"" }; static const char *const sus[] = { "X-KDE-SubstituteUID=false", "X-KDE-SubstituteUID=true\nX-KDE-Username=sprallo" }; static const char *const results[] = { "/bin/date -u", // 0 "/bin/sh -c 'echo $PWD '", // 1 "/usr/bin/xterm -T ' - just_a_test' -e /bin/date -u", // 2 "/usr/bin/xterm -T ' - just_a_test' -e /bin/sh -c 'echo $PWD '", // 3 /* kdesu */ " -u sprallo -c '/bin/date -u'", // 4 /* kdesu */ " -u sprallo -c '/bin/sh -c '\\''echo $PWD '\\'''", // 5 "/usr/bin/xterm -T ' - just_a_test' -e su sprallo -c '/bin/date -u'", // 6 "/usr/bin/xterm -T ' - just_a_test' -e su sprallo -c '/bin/sh -c '\\''echo $PWD '\\'''", // 7 }; // Find out the full path of the shell which will be used to execute shell commands KProcess process; process.setShellCommand(QLatin1String("")); const QString shellPath = process.program().at(0); // Arch moved /bin/date to /usr/bin/date... const QString datePath = QStandardPaths::findExecutable(QStringLiteral("date")); for (int su = 0; su < 2; su++) for (int te = 0; te < 2; te++) for (int ex = 0; ex < 2; ex++) { int pt = ex + te * 2 + su * 4; QString exe; if (pt == 4 || pt == 5) { exe = QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/kdesu"); if (!QFile::exists(exe)) { qWarning() << "kdesu not found, skipping test"; continue; } } const QString result = QString::fromLatin1(results[pt]) .replace(QLatin1String("/bin/sh"), shellPath) .replace(QLatin1String("/bin/date"), datePath); checkDesktopExecParser(execs[ex], terms[te], sus[su], l0, false, exe + result); } } void KRunUnitTest::testProcessDesktopExecNoFile_data() { QTest::addColumn("execLine"); QTest::addColumn >("urls"); QTest::addColumn("tempfiles"); QTest::addColumn("expected"); QList l0; QList l1; l1 << QUrl(QStringLiteral("file:/tmp")); QList l2; l2 << QUrl(QStringLiteral("http://localhost/foo")); QList l3; l3 << QUrl(QStringLiteral("file:/local/some file")) << QUrl(QStringLiteral("http://remotehost.org/bar")); QList l4; l4 << QUrl(QStringLiteral("http://login:password@www.kde.org")); // A real-world use case would be kate. // But I picked kdeinit5 since it's installed by kdelibs QString kdeinit = QStandardPaths::findExecutable(QStringLiteral("kdeinit5")); if (kdeinit.isEmpty()) { kdeinit = QStringLiteral("kdeinit5"); } QString kioexec = QCoreApplication::applicationDirPath() + "/kioexec"; if (!QFileInfo::exists(kioexec)) { kioexec = CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/kioexec"; } QVERIFY(QFileInfo::exists(kioexec)); QString kioexecQuoted = KShell::quoteArg(kioexec); - QString kmailservice = QStandardPaths::findExecutable(QStringLiteral("kmailservice5")); - if (!QFile::exists(kmailservice)) { - kmailservice = QStringLiteral("kmailservice5"); - } - QTest::newRow("%U l0") << "kdeinit5 %U" << l0 << false << kdeinit; QTest::newRow("%U l1") << "kdeinit5 %U" << l1 << false << kdeinit + " /tmp"; QTest::newRow("%U l2") << "kdeinit5 %U" << l2 << false << kdeinit + " http://localhost/foo"; QTest::newRow("%U l3") << "kdeinit5 %U" << l3 << false << kdeinit + " '/local/some file' http://remotehost.org/bar"; //QTest::newRow("%u l0") << "kdeinit5 %u" << l0 << false << kdeinit; // gives runtime warning QTest::newRow("%u l1") << "kdeinit5 %u" << l1 << false << kdeinit + " /tmp"; QTest::newRow("%u l2") << "kdeinit5 %u" << l2 << false << kdeinit + " http://localhost/foo"; //QTest::newRow("%u l3") << "kdeinit5 %u" << l3 << false << kdeinit; // gives runtime warning QTest::newRow("%F l0") << "kdeinit5 %F" << l0 << false << kdeinit; QTest::newRow("%F l1") << "kdeinit5 %F" << l1 << false << kdeinit + " /tmp"; QTest::newRow("%F l2") << "kdeinit5 %F" << l2 << false << kioexecQuoted + " 'kdeinit5 %F' http://localhost/foo"; QTest::newRow("%F l3") << "kdeinit5 %F" << l3 << false << kioexecQuoted + " 'kdeinit5 %F' 'file:///local/some file' http://remotehost.org/bar"; QTest::newRow("%F l1 tempfile") << "kdeinit5 %F" << l1 << true << kioexecQuoted + " --tempfiles 'kdeinit5 %F' file:///tmp"; QTest::newRow("%f l1 tempfile") << "kdeinit5 %f" << l1 << true << kioexecQuoted + " --tempfiles 'kdeinit5 %f' file:///tmp"; QTest::newRow("sh -c kdeinit5 %F") << "sh -c \"kdeinit5 \"'\\\"'\"%F\"'\\\"'" << l1 << false << m_sh + " -c 'kdeinit5 \\\"/tmp\\\"'"; - QTest::newRow("kmailservice5 %u l1") << "kmailservice5 %u" << l1 << false << kmailservice + " /tmp"; - QTest::newRow("kmailservice5 %u l4") << "kmailservice5 %u" << l4 << false << kmailservice + " http://login:password@www.kde.org"; + // This was originally with kmailservice5, but that relies on it being installed + QTest::newRow("kdeinit5 %u l1") << "kdeinit5 %u" << l1 << false << kdeinit + " /tmp"; + QTest::newRow("kdeinit5 %u l4") << "kdeinit5 %u" << l4 << false << kdeinit + " http://login:password@www.kde.org"; } void KRunUnitTest::testProcessDesktopExecNoFile() { QFETCH(QString, execLine); KService service(QStringLiteral("dummy"), execLine, QStringLiteral("app")); QFETCH(QList, urls); QFETCH(bool, tempfiles); QFETCH(QString, expected); KIO::DesktopExecParser parser(service, urls); parser.setUrlsAreTempFiles(tempfiles); - QCOMPARE(KShell::joinArgs(parser.resultingArguments()), expected); + const QStringList args = parser.resultingArguments(); + if (args.isEmpty()) + qWarning() << parser.errorMessage(); + QCOMPARE(KShell::joinArgs(args), expected); } extern KSERVICE_EXPORT int ksycoca_ms_between_checks; void KRunUnitTest::testKtelnetservice() { const QString ktelnetDesk = QFINDTESTDATA(QStringLiteral("../src/ioslaves/telnet/ktelnetservice5.desktop")); QVERIFY(!ktelnetDesk.isEmpty()); // KMimeTypeTrader::self() in KIO::DesktopExecParser::hasSchemeHandler() needs the .desktop file to be installed const QString destDir = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); QVERIFY(QDir().mkpath(destDir)); QFile::remove(destDir + QLatin1String("/ktelnetservice5.desktop")); QVERIFY(QFile::copy(ktelnetDesk, destDir + QLatin1String("/ktelnetservice5.desktop"))); ksycoca_ms_between_checks = 0; // need it to check the ksycoca mtime KService::Ptr service = KService::serviceByStorageId(QStringLiteral("ktelnetservice5.desktop")); QVERIFY(service); QString ktelnetExec = QStandardPaths::findExecutable(QStringLiteral("ktelnetservice5")); // if KIO is installed we'll find /ktelnetservice5, otherwise KIO::DesktopExecParser will // use the executable from Exec= line if (ktelnetExec.isEmpty()) { ktelnetExec = service->exec().remove(QLatin1String(" %u")); } QVERIFY(!ktelnetExec.isEmpty()); const QStringList protocols({QStringLiteral("ssh"), QStringLiteral("telnet"), QStringLiteral("rlogin")}); for (const QString &protocol : protocols) { // Check that hasSchemeHandler will return true QVERIFY(!KProtocolInfo::isKnownProtocol(protocol)); QVERIFY(!KProtocolInfo::isHelperProtocol(protocol)); QVERIFY(KMimeTypeTrader::self()->preferredService(QLatin1String("x-scheme-handler/") + protocol)); const QList urls({QUrl(QStringLiteral("%1://root@10.1.1.1").arg(protocol))}); KIO::DesktopExecParser parser(*service, urls); QCOMPARE(KShell::joinArgs(parser.resultingArguments()), QStringLiteral("%1 %2://root@10.1.1.1").arg(ktelnetExec, protocol)); } } class KRunImpl : public KRun { public: KRunImpl(const QUrl &url) : KRun(url, nullptr, false), m_errCode(-1) {} void foundMimeType(const QString &type) override { m_mimeType = type; // don't call KRun::foundMimeType, we don't want to start an app ;-) setFinished(true); } void handleInitError(int kioErrorCode, const QString &err) override { m_errCode = kioErrorCode; m_errText = err; } QString mimeTypeFound() const { return m_mimeType; } int errorCode() const { return m_errCode; } QString errorText() const { return m_errText; } private: int m_errCode; QString m_errText; QString m_mimeType; }; void KRunUnitTest::testMimeTypeFile() { const QString filePath = homeTmpDir() + "file"; createTestFile(filePath, true); KRunImpl *krun = new KRunImpl(QUrl::fromLocalFile(filePath)); krun->setAutoDelete(false); QSignalSpy spyFinished(krun, SIGNAL(finished())); QVERIFY(spyFinished.wait(1000)); QCOMPARE(krun->mimeTypeFound(), QString::fromLatin1("text/plain")); delete krun; } void KRunUnitTest::testMimeTypeDirectory() { const QString dir = homeTmpDir() + "dir"; createTestDirectory(dir); KRunImpl *krun = new KRunImpl(QUrl::fromLocalFile(dir)); QSignalSpy spyFinished(krun, SIGNAL(finished())); QVERIFY(spyFinished.wait(1000)); QCOMPARE(krun->mimeTypeFound(), QString::fromLatin1("inode/directory")); } void KRunUnitTest::testMimeTypeBrokenLink() { const QString dir = homeTmpDir() + "dir"; createTestDirectory(dir); KRunImpl *krun = new KRunImpl(QUrl::fromLocalFile(dir + "/testlink")); QSignalSpy spyError(krun, SIGNAL(error())); QSignalSpy spyFinished(krun, SIGNAL(finished())); QVERIFY(spyFinished.wait(1000)); QVERIFY(krun->mimeTypeFound().isEmpty()); QCOMPARE(spyError.count(), 1); QCOMPARE(krun->errorCode(), int(KIO::ERR_DOES_NOT_EXIST)); QVERIFY(krun->errorText().contains("does not exist")); QTest::qWait(100); // let auto-deletion proceed. } void KRunUnitTest::testMimeTypeDoesNotExist() // ported to OpenUrlJobTest::nonExistingFile() { KRunImpl *krun = new KRunImpl(QUrl::fromLocalFile(QStringLiteral("/does/not/exist"))); QSignalSpy spyError(krun, SIGNAL(error())); QSignalSpy spyFinished(krun, SIGNAL(finished())); QVERIFY(spyFinished.wait(1000)); QVERIFY(krun->mimeTypeFound().isEmpty()); QCOMPARE(spyError.count(), 1); QTest::qWait(100); // let auto-deletion proceed. } static const char s_tempServiceName[] = "krununittest_service.desktop"; static void createSrcFile(const QString path) { QFile srcFile(path); QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString())); srcFile.write("Hello world\n"); } #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 71) void KRunUnitTest::KRunRunService_data() { QTest::addColumn("tempFile"); QTest::addColumn("useRunApplication"); QTest::newRow("standard") << false << false; QTest::newRow("tempfile") << true << false; QTest::newRow("runApp") << false << true; QTest::newRow("runApp_tempfile") << true << true; } #endif #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 71) void KRunUnitTest::KRunRunService() { QFETCH(bool, tempFile); QFETCH(bool, useRunApplication); // Given a service desktop file and a source file const QString path = createTempService(); //KService::Ptr service = KService::serviceByDesktopPath(s_tempServiceName); //QVERIFY(service); KService service(path); QTemporaryDir tempDir; const QString srcDir = tempDir.path(); const QString srcFile = srcDir + "/srcfile"; createSrcFile(srcFile); QVERIFY(QFile::exists(srcFile)); QList urls; urls.append(QUrl::fromLocalFile(srcFile)); // When calling KRun::runService or KRun::runApplication qint64 pid = useRunApplication ? KRun::runApplication(service, urls, nullptr, tempFile ? KRun::RunFlags(KRun::DeleteTemporaryFiles) : KRun::RunFlags()) : KRun::runService(service, urls, nullptr, tempFile); // DEPRECATED // Then the service should be executed (which copies the source file to "dest") QVERIFY(pid != 0); const QString dest = srcDir + "/dest"; QTRY_VERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(srcFile)); // if tempfile is true, kioexec will delete it... in 3 minutes. // All done, clean up. QVERIFY(QFile::remove(dest)); #ifdef Q_OS_UNIX ::kill(pid, SIGTERM); #endif } #endif QString KRunUnitTest::createTempService() { // fakeservice: deleted and recreated by testKSycocaUpdate, don't use in other tests const QString fileName = s_tempServiceName; //bool mustUpdateKSycoca = !KService::serviceByDesktopPath(fileName); const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/") + fileName; if (!QFile::exists(fakeService)) { //mustUpdateKSycoca = true; KDesktopFile file(fakeService); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestService"); group.writeEntry("Type", "Service"); #ifdef Q_OS_WIN group.writeEntry("Exec", "copy.exe %f %d/dest"); #else group.writeEntry("Exec", "cp %f %d/dest"); #endif file.sync(); QFile f(fakeService); f.setPermissions(f.permissions() | QFile::ExeOwner | QFile::ExeUser); } m_filesToRemove.append(fakeService); return fakeService; } diff --git a/src/core/desktopexecparser.cpp b/src/core/desktopexecparser.cpp index fcf83d9e..398b0f68 100644 --- a/src/core/desktopexecparser.cpp +++ b/src/core/desktopexecparser.cpp @@ -1,521 +1,586 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Torben Weis Copyright (C) 2006-2013 David Faure Copyright (C) 2009 Michael Pyne This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) 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 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 "desktopexecparser.h" #include "kiofuse_interface.h" #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include // CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 #include "kiocoredebug.h" class KRunMX1 : public KMacroExpanderBase { public: explicit KRunMX1(const KService &_service) : KMacroExpanderBase(QLatin1Char('%')) , hasUrls(false) , hasSpec(false) , service(_service) {} bool hasUrls; bool hasSpec; protected: int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override; private: const KService &service; }; int KRunMX1::expandEscapedMacro(const QString &str, int pos, QStringList &ret) { uint option = str[pos + 1].unicode(); switch (option) { case 'c': ret << service.name().replace(QLatin1Char('%'), QLatin1String("%%")); break; case 'k': ret << service.entryPath().replace(QLatin1Char('%'), QLatin1String("%%")); break; case 'i': ret << QStringLiteral("--icon") << service.icon().replace(QLatin1Char('%'), QLatin1String("%%")); break; case 'm': // ret << "-miniicon" << service.icon().replace( '%', "%%" ); qCWarning(KIO_CORE) << "-miniicon isn't supported anymore (service" << service.name() << ')'; break; case 'u': case 'U': hasUrls = true; Q_FALLTHROUGH(); /* fallthrough */ case 'f': case 'F': case 'n': case 'N': case 'd': case 'D': case 'v': hasSpec = true; Q_FALLTHROUGH(); /* fallthrough */ default: return -2; // subst with same and skip } return 2; } class KRunMX2 : public KMacroExpanderBase { public: explicit KRunMX2(const QList &_urls) : KMacroExpanderBase(QLatin1Char('%')) , ignFile(false), urls(_urls) {} bool ignFile; protected: int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override; private: void subst(int option, const QUrl &url, QStringList &ret); const QList &urls; }; void KRunMX2::subst(int option, const QUrl &url, QStringList &ret) { switch (option) { case 'u': ret << ((url.isLocalFile() && url.fragment().isNull() && url.query().isNull()) ? QDir::toNativeSeparators(url.toLocalFile()) : url.toString()); break; case 'd': ret << url.adjusted(QUrl::RemoveFilename).path(); break; case 'f': ret << QDir::toNativeSeparators(url.toLocalFile()); break; case 'n': ret << url.fileName(); break; case 'v': if (url.isLocalFile() && QFile::exists(url.toLocalFile())) { ret << KDesktopFile(url.toLocalFile()).desktopGroup().readEntry("Dev"); } break; } return; } int KRunMX2::expandEscapedMacro(const QString &str, int pos, QStringList &ret) { uint option = str[pos + 1].unicode(); switch (option) { case 'f': case 'u': case 'n': case 'd': case 'v': if (urls.isEmpty()) { if (!ignFile) { //qCDebug(KIO_CORE) << "No URLs supplied to single-URL service" << str; } } else if (urls.count() > 1) { qCWarning(KIO_CORE) << urls.count() << "URLs supplied to single-URL service" << str; } else { subst(option, urls.first(), ret); } break; case 'F': case 'U': case 'N': case 'D': option += 'a' - 'A'; for (const QUrl &url : urls) { subst(option, url, ret); } break; case '%': ret = QStringList(QStringLiteral("%")); break; default: return -2; // subst with same and skip } return 2; } QStringList KIO::DesktopExecParser::supportedProtocols(const KService &service) { QStringList supportedProtocols = service.property(QStringLiteral("X-KDE-Protocols")).toStringList(); KRunMX1 mx1(service); QString exec = service.exec(); if (mx1.expandMacrosShellQuote(exec) && !mx1.hasUrls) { if (!supportedProtocols.isEmpty()) { qCWarning(KIO_CORE) << service.entryPath() << "contains a X-KDE-Protocols line but doesn't use %u or %U in its Exec line! This is inconsistent."; } return QStringList(); } else { if (supportedProtocols.isEmpty()) { // compat mode: assume KIO if not set and it's a KDE app (or a KDE service) const QStringList categories = service.property(QStringLiteral("Categories")).toStringList(); if (categories.contains(QLatin1String("KDE")) || !service.isApplication() || service.entryPath().isEmpty() /*temp service*/) { supportedProtocols.append(QStringLiteral("KIO")); } else { // if no KDE app, be a bit over-generic supportedProtocols.append(QStringLiteral("http")); supportedProtocols.append(QStringLiteral("https")); // #253294 supportedProtocols.append(QStringLiteral("ftp")); } } } // add x-scheme-handler/ const auto servicesTypes = service.serviceTypes(); for (const auto &mimeType : servicesTypes) { if (mimeType.startsWith(QLatin1String("x-scheme-handler/"))) { supportedProtocols << mimeType.mid(17); } } //qCDebug(KIO_CORE) << "supportedProtocols:" << supportedProtocols; return supportedProtocols; } bool KIO::DesktopExecParser::isProtocolInSupportedList(const QUrl &url, const QStringList &supportedProtocols) { if (supportedProtocols.contains(QLatin1String("KIO"))) { return true; } return url.isLocalFile() || supportedProtocols.contains(url.scheme().toLower()); } // We have up to two sources of data, for protocols not handled by kioslaves (so called "helper") : // 1) the exec line of the .protocol file, if there's one // 2) the application associated with x-scheme-handler/ if there's one // If both exist, then: // A) if the .protocol file says "launch an application", then the new-style handler-app has priority // B) but if the .protocol file is for a kioslave (e.g. kio_http) then this has priority over // firefox or chromium saying x-scheme-handler/http. Gnome people want to send all HTTP urls // to a webbrowser, but we want mimetype-determination-in-calling-application by default // (the user can configure a BrowserApplication though) bool KIO::DesktopExecParser::hasSchemeHandler(const QUrl &url) { if (KProtocolInfo::isHelperProtocol(url)) { return true; } if (KProtocolInfo::isKnownProtocol(url)) { return false; // this is case B, we prefer kioslaves over the competition } const KService::Ptr service = KMimeTypeTrader::self()->preferredService(QLatin1String("x-scheme-handler/") + url.scheme()); if (service) { qCDebug(KIO_CORE) << QLatin1String("preferred service for x-scheme-handler/") + url.scheme() << service->desktopEntryName(); } return service; } class KIO::DesktopExecParserPrivate { public: DesktopExecParserPrivate(const KService &_service, const QList &_urls) : service(_service), urls(_urls), tempFiles(false) {} const KService &service; QList urls; bool tempFiles; QString suggestedFileName; + QString m_errorString; }; KIO::DesktopExecParser::DesktopExecParser(const KService &service, const QList &urls) : d(new DesktopExecParserPrivate(service, urls)) { } KIO::DesktopExecParser::~DesktopExecParser() { } void KIO::DesktopExecParser::setUrlsAreTempFiles(bool tempFiles) { d->tempFiles = tempFiles; } void KIO::DesktopExecParser::setSuggestedFileName(const QString &suggestedFileName) { d->suggestedFileName = suggestedFileName; } static const QString kioexecPath() { QString kioexec = QCoreApplication::applicationDirPath() + QLatin1String("/kioexec"); if (!QFileInfo::exists(kioexec)) kioexec = QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/kioexec"); Q_ASSERT(QFileInfo::exists(kioexec)); return kioexec; } +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(); +} + QStringList KIO::DesktopExecParser::resultingArguments() const { QString exec = d->service.exec(); if (exec.isEmpty()) { - qCWarning(KIO_CORE) << "No Exec field in `" << d->service.entryPath() << "' !"; + d->m_errorString = i18n("No Exec field in %1", d->service.entryPath()); + qCWarning(KIO_CORE) << "No Exec field in" << d->service.entryPath(); return QStringList(); } + // Extract the name of the binary to execute from the full Exec line, to see if it exists + const QString binary = executablePath(exec); + QString executableFullPath; + if (!binary.isEmpty()) { // skip all this if the Exec line is a complex shell command + if (QDir::isRelativePath(binary)) { + // Resolve the executable to ensure that helpers in libexec are found. + // Too bad for commands that need a shell - they must reside in $PATH. + executableFullPath = QStandardPaths::findExecutable(binary); + qDebug() << "findExecutable(" << binary << ") said" << executableFullPath; + if (executableFullPath.isEmpty()) { + executableFullPath = QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/") + binary; + } + } else { + executableFullPath = binary; + } + + // Now check that the binary exists and has the executable flag + if (!QFileInfo(executableFullPath).isExecutable()) { + // Does it really not exist, or is it non-executable (on Unix)? (bug #415567) + const QString nonExecutable = findNonExecutableProgram(binary); + if (nonExecutable.isEmpty()) { + d->m_errorString = i18n("Could not find the program '%1'", binary); + } else { + if (QDir::isRelativePath(binary)) { + d->m_errorString = i18n("The program '%1' was found at '%2' but it is missing executable permissions.", binary, nonExecutable); + } else { + d->m_errorString = i18n("The program '%1' is missing executable permissions.", nonExecutable); + } + } + return QStringList(); + } + } + QStringList result; bool appHasTempFileOption; KRunMX1 mx1(d->service); KRunMX2 mx2(d->urls); if (!mx1.expandMacrosShellQuote(exec)) { // Error in shell syntax + d->m_errorString = i18n("Syntax error in command %1 coming from %2", exec, d->service.entryPath()); qCWarning(KIO_CORE) << "Syntax error in command" << d->service.exec() << ", service" << d->service.name(); return QStringList(); } // FIXME: the current way of invoking kioexec disables term and su use // Check if we need "tempexec" (kioexec in fact) appHasTempFileOption = d->tempFiles && d->service.property(QStringLiteral("X-KDE-HasTempFileOption")).toBool(); if (d->tempFiles && !appHasTempFileOption && d->urls.size()) { result << kioexecPath() << QStringLiteral("--tempfiles") << exec; if (!d->suggestedFileName.isEmpty()) { result << QStringLiteral("--suggestedfilename"); result << d->suggestedFileName; } result += QUrl::toStringList(d->urls); return result; } // Return true for non-KIO desktop files with explicit X-KDE-Protocols list, like vlc, for the special case below auto isNonKIO = [this]() { const QStringList protocols = d->service.property(QStringLiteral("X-KDE-Protocols")).toStringList(); return !protocols.isEmpty() && !protocols.contains(QLatin1String("KIO")); }; // Check if we need kioexec, or KIOFuse bool useKioexec = false; org::kde::KIOFuse::VFS kiofuse_iface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus()); struct MountRequest { QDBusPendingReply reply; int urlIndex; }; QVector requests; requests.reserve(d->urls.count()); const QStringList appSupportedProtocols = supportedProtocols(d->service); for (int i = 0; i < d->urls.count(); ++i) { const QUrl url = d->urls.at(i); const bool supported = mx1.hasUrls ? isProtocolInSupportedList(url, appSupportedProtocols) : url.isLocalFile(); if (!supported) { // if FUSE fails, we'll have to fallback to kioexec useKioexec = true; } // NOTE: Some non-KIO apps may support the URLs (e.g. VLC supports smb://) // but will not have the password if they are not in the URL itself. // Hence convert URL to KIOFuse equivalent in case there is a password. // @see https://pointieststick.com/2018/01/17/videos-on-samba-shares/ // @see https://bugs.kde.org/show_bug.cgi?id=330192 if (!supported || (!url.userName().isEmpty() && url.password().isEmpty() && isNonKIO())) { requests.push_back({ kiofuse_iface.mountUrl(url.toString()), i }); } } for (auto &request : requests) { request.reply.waitForFinished(); } const bool fuseError = std::any_of(requests.cbegin(), requests.cend(), [](const MountRequest &request) { return request.reply.isError(); }); if (fuseError && useKioexec) { // We need to run the app through kioexec result << kioexecPath(); if (d->tempFiles) { result << QStringLiteral("--tempfiles"); } if (!d->suggestedFileName.isEmpty()) { result << QStringLiteral("--suggestedfilename"); result << d->suggestedFileName; } result << exec; result += QUrl::toStringList(d->urls); return result; } // At this point we know we're not using kioexec, so feel free to replace // KIO URLs with their KIOFuse local path. for (const auto &request : qAsConst(requests)) { if (!request.reply.isError()) { d->urls[request.urlIndex] = QUrl::fromLocalFile(request.reply.value()); } } if (appHasTempFileOption) { exec += QLatin1String(" --tempfile"); } // Did the user forget to append something like '%f'? // If so, then assume that '%f' is the right choice => the application // accepts only local files. if (!mx1.hasSpec) { exec += QLatin1String(" %f"); mx2.ignFile = true; } mx2.expandMacrosShellQuote(exec); // syntax was already checked, so don't check return value /* 1 = need_shell, 2 = terminal, 4 = su 0 << split(cmd) 1 << "sh" << "-c" << cmd 2 << split(term) << "-e" << split(cmd) 3 << split(term) << "-e" << "sh" << "-c" << cmd 4 << "kdesu" << "-u" << user << "-c" << cmd 5 << "kdesu" << "-u" << user << "-c" << ("sh -c " + quote(cmd)) 6 << split(term) << "-e" << "su" << user << "-c" << cmd 7 << split(term) << "-e" << "su" << user << "-c" << ("sh -c " + quote(cmd)) "sh -c" is needed in the "su" case, too, as su uses the user's login shell, not sh. this could be optimized with the -s switch of some su versions (e.g., debian linux). */ if (d->service.terminal()) { KConfigGroup cg(KSharedConfig::openConfig(), "General"); QString terminal = cg.readPathEntry("TerminalApplication", QStringLiteral("konsole")); const bool isKonsole = (terminal == QLatin1String("konsole")); QString terminalPath = QStandardPaths::findExecutable(terminal); if (terminalPath.isEmpty()) { + d->m_errorString = i18n("Terminal %1 not found while trying to run %2", terminal, d->service.entryPath()); qCWarning(KIO_CORE) << "Terminal" << terminal << "not found, service" << d->service.name(); return QStringList(); } terminal = terminalPath; if (isKonsole) { if (!d->service.workingDirectory().isEmpty()) { terminal += QLatin1String(" --workdir ") + KShell::quoteArg(d->service.workingDirectory()); } terminal += QLatin1String(" -qwindowtitle '%c'"); if(!d->service.icon().isEmpty()) { terminal += QLatin1String(" -qwindowicon ") + KShell::quoteArg(d->service.icon().replace(QLatin1Char('%'), QLatin1String("%%"))); } } terminal += QLatin1Char(' ') + d->service.terminalOptions(); if (!mx1.expandMacrosShellQuote(terminal)) { + d->m_errorString = i18n("Syntax error in command %1 while trying to run %2", terminal, d->service.entryPath()); qCWarning(KIO_CORE) << "Syntax error in command" << terminal << ", service" << d->service.name(); return QStringList(); } mx2.expandMacrosShellQuote(terminal); result = KShell::splitArgs(terminal); // assuming that the term spec never needs a shell! result << QStringLiteral("-e"); } KShell::Errors err; QStringList execlist = KShell::splitArgs(exec, KShell::AbortOnMeta | KShell::TildeExpand, &err); - if (err == KShell::NoError && !execlist.isEmpty()) { // mx1 checked for syntax errors already - const QString executable = execlist.at(0); - if (QDir::isRelativePath(executable)) { - // Resolve the executable to ensure that helpers in libexec are found. - // Too bad for commands that need a shell - they must reside in $PATH. - QString exePath = QStandardPaths::findExecutable(executable); - if (exePath.isEmpty()) { - exePath = QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/") + executable; - } - if (QFile::exists(exePath)) { - execlist[0] = exePath; - } - } + if (!executableFullPath.isEmpty()) { + execlist[0] = executableFullPath; } + if (d->service.substituteUid()) { if (d->service.terminal()) { result << QStringLiteral("su"); } else { QString kdesu = QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/kdesu"); if (!QFile::exists(kdesu)) { kdesu = QStandardPaths::findExecutable(QStringLiteral("kdesu")); } if (!QFile::exists(kdesu)) { // Insert kdesu as string so we show a nice warning: 'Could not launch kdesu' result << QStringLiteral("kdesu"); return result; } else { result << kdesu << QStringLiteral("-u"); } } result << d->service.username() << QStringLiteral("-c"); if (err == KShell::FoundMeta) { exec = QLatin1String("/bin/sh -c ") + KShell::quoteArg(exec); } else { exec = KShell::joinArgs(execlist); } result << exec; } else { if (err == KShell::FoundMeta) { result << QStringLiteral("/bin/sh") << QStringLiteral("-c") << exec; } else { result += execlist; } } return result; } +QString KIO::DesktopExecParser::errorMessage() const +{ + return d->m_errorString; +} + //static QString KIO::DesktopExecParser::executableName(const QString &execLine) { const QString bin = executablePath(execLine); return bin.mid(bin.lastIndexOf(QLatin1Char('/')) + 1); } //static QString KIO::DesktopExecParser::executablePath(const QString &execLine) { // Remove parameters and/or trailing spaces. - const QStringList args = KShell::splitArgs(execLine); + const QStringList args = KShell::splitArgs(execLine, KShell::AbortOnMeta | KShell::TildeExpand); for (const QString &arg : args) { if (!arg.contains(QLatin1Char('='))) { return arg; } } return QString(); } diff --git a/src/core/desktopexecparser.h b/src/core/desktopexecparser.h index a748c6b6..10a25b95 100644 --- a/src/core/desktopexecparser.h +++ b/src/core/desktopexecparser.h @@ -1,125 +1,132 @@ /* * Copyright 2013 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.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see */ #ifndef KIO_DESKTOPEXECPARSER_H #define KIO_DESKTOPEXECPARSER_H #include "kiocore_export.h" #include #include class QUrl; class QStringList; class KService; namespace KIO { class DesktopExecParserPrivate; /** * @class KIO::DesktopExecParser desktopexecparser.h * * Parses the Exec= line from a .desktop file, * and process all the '\%' placeholders, e.g. handling URLs vs local files. * * The processing actually happens when calling resultingArguments(), after * setting everything up. * * @since 5.0 */ class KIOCORE_EXPORT DesktopExecParser { public: /** * Creates a parser for a desktop file Exec line. * * @param service the service to extract information from. * The KService instance must remain alive as long as the parser is alive. * @param urls The urls the service should open. */ DesktopExecParser(const KService &service, const QList &urls); /** * Destructor */ ~DesktopExecParser(); /** * If @p tempFiles is set to true and the urls given to the constructor are local files, * they will be deleted when the application exits. */ void setUrlsAreTempFiles(bool tempFiles); /** * 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. */ void setSuggestedFileName(const QString &suggestedFileName); /** * @return a list of arguments suitable for QProcess. + * Returns an empty list on error, check errorMessage() for details. */ QStringList resultingArguments() const; + /** + * @return an error message for when resultingArguments() returns an empty list + * @since 5.71 + */ + QString errorMessage() const; + /** * Returns the list of protocols which the application supports. * This can be a list of actual protocol names, or just "KIO" for KIO-based apps. */ static QStringList supportedProtocols(const KService &service); /** * Returns true if @p protocol is in the list of protocols returned by supportedProtocols(). * The only reason for this method is the special handling of "KIO". */ static bool isProtocolInSupportedList(const QUrl &url, const QStringList &supportedProtocols); /** * Returns true if @p protocol should be opened by a "handler" application, i.e. an application * associated to _all_ URLs using this protocol (a.k.a. scheme). */ static bool hasSchemeHandler(const QUrl &url); /** * Given a full command line (e.g. the Exec= line from a .desktop file), * extract the name of the executable being run (removing the path, if specified). * @param execLine the full command line * @return the name of the executable to run, example: "ls" */ static QString executableName(const QString &execLine); /** * Given a full command line (e.g. the Exec= line from a .desktop file), * extract the name of the executable being run, including its full path, if specified. * @param execLine the full command line * @return the name of the executable to run, example: "/bin/ls" */ static QString executablePath(const QString &execLine); private: QScopedPointer d; }; } // namespace KIO #endif diff --git a/src/gui/kprocessrunner.cpp b/src/gui/kprocessrunner.cpp index cc57b541..9f68289b 100644 --- a/src/gui/kprocessrunner.cpp +++ b/src/gui/kprocessrunner.cpp @@ -1,424 +1,380 @@ /* 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); 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; - } - - const QString realExecutable = args.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)); - } + emitDelayedError(execParser.errorMessage()); 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(); 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/kioexec/main.cpp b/src/kioexec/main.cpp index 917b0ce9..38288f57 100644 --- a/src/kioexec/main.cpp +++ b/src/kioexec/main.cpp @@ -1,318 +1,323 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (C) 2000-2005 David Faure Copyright (C) 2001 Waldo Bastian This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "main.h" #include "kio_version.h" #include "kioexecdinterface.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_X11 #include #endif static const char description[] = I18N_NOOP("KIO Exec - Opens remote files, watches modifications, asks for upload"); KIOExec::KIOExec(const QStringList &args, bool tempFiles, const QString &suggestedFileName) : mExited(false) , mTempFiles(tempFiles) , mUseDaemon(false) , mSuggestedFileName(suggestedFileName) , expectedCounter(0) , command(args.first()) , jobCounter(0) { qDebug() << "command=" << command << "args=" << args; for (int i = 1; i < args.count(); i++) { const QUrl urlArg = QUrl::fromUserInput(args.value(i)); if (!urlArg.isValid()) { KMessageBox::error(nullptr, i18n("Invalid URL: %1", args.value(i))); exit(1); } KIO::StatJob* mostlocal = KIO::mostLocalUrl(urlArg); bool b = mostlocal->exec(); if (!b) { KMessageBox::error(nullptr, i18n("File not found: %1", urlArg.toDisplayString())); exit(1); } Q_ASSERT(b); const QUrl url = mostlocal->mostLocalUrl(); //kDebug() << "url=" << url.url() << " filename=" << url.fileName(); // A local file, not an URL ? // => It is not encoded and not shell escaped, too. if (url.isLocalFile()) { FileInfo file; file.path = url.toLocalFile(); file.url = url; fileList.append(file); } else { // It is an URL if (!url.isValid()) { KMessageBox::error(nullptr, i18n("The URL %1\nis malformed" , url.url())); } else if (mTempFiles) { KMessageBox::error(nullptr, i18n("Remote URL %1\nnot allowed with --tempfiles switch" , url.toDisplayString())); } else { // We must fetch the file QString fileName = KIO::encodeFileName(url.fileName()); if (!suggestedFileName.isEmpty()) fileName = suggestedFileName; if (fileName.isEmpty()) fileName = QStringLiteral("unnamed"); // Build the destination filename, in ~/.cache/kioexec/krun/ // Unlike KDE-1.1, we put the filename at the end so that the extension is kept // (Some programs rely on it) QString krun_writable = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/krun/%1_%2/").arg(QCoreApplication::applicationPid()).arg(jobCounter++); QDir().mkpath(krun_writable); // error handling will be done by the job QString tmp = krun_writable + fileName; FileInfo file; file.path = tmp; file.url = url; fileList.append(file); expectedCounter++; const QUrl dest = QUrl::fromLocalFile(tmp); qDebug() << "Copying" << url << " to" << dest; KIO::Job *job = KIO::file_copy(url, dest); jobList.append(job); connect(job, &KJob::result, this, &KIOExec::slotResult); } } } if (mTempFiles) { // delay call so QApplication::exit passes the exit code to exec() QTimer::singleShot(0, this, &KIOExec::slotRunApp); return; } counter = 0; if (counter == expectedCounter) { slotResult(nullptr); } } void KIOExec::slotResult(KJob *job) { if (job) { KIO::FileCopyJob *copyJob = static_cast(job); const QString path = copyJob->destUrl().path(); if (job->error()) { // That error dialog would be queued, i.e. not immediate... //job->showErrorDialog(); if (job->error() != KIO::ERR_USER_CANCELED) { KMessageBox::error(nullptr, job->errorString()); } auto it = std::find_if(fileList.begin(), fileList.end(), [&path](const FileInfo &i) { return i.path == path; }); if (it != fileList.end()) { fileList.erase(it); } else { qDebug() << path << " not found in list"; } } else { // Tell kioexecd to watch the file for changes. const QString dest = copyJob->srcUrl().toString(); qDebug() << "Telling kioexecd to watch path" << path << "dest" << dest; OrgKdeKIOExecdInterface kioexecd(QStringLiteral("org.kde.kioexecd"), QStringLiteral("/modules/kioexecd"), QDBusConnection::sessionBus()); kioexecd.watch(path, dest); mUseDaemon = !kioexecd.lastError().isValid(); if (!mUseDaemon) { qDebug() << "Not using kioexecd"; } } } counter++; if (counter < expectedCounter) { return; } qDebug() << "All files downloaded, will call slotRunApp shortly"; // We know we can run the app now - but let's finish the job properly first. QTimer::singleShot(0, this, &KIOExec::slotRunApp); jobList.clear(); } void KIOExec::slotRunApp() { if (fileList.isEmpty()) { qDebug() << "No files downloaded -> exiting"; mExited = true; QApplication::exit(1); return; } KService service(QStringLiteral("dummy"), command, QString()); QList list; list.reserve(fileList.size()); // Store modification times QList::Iterator it = fileList.begin(); for (; it != fileList.end() ; ++it) { QFileInfo info(it->path); it->time = info.lastModified(); QUrl url = QUrl::fromLocalFile(it->path); list << url; } KIO::DesktopExecParser execParser(service, list); QStringList params = execParser.resultingArguments(); + if (params.isEmpty()) { + qWarning() << execParser.errorMessage(); + QApplication::exit(-1); + return; + } qDebug() << "EXEC" << params.join(QLatin1Char(' ')); // propagate the startup identification to the started process KStartupInfoId id; QByteArray startupId; #if HAVE_X11 if (QX11Info::isPlatformX11()) { startupId = QX11Info::nextStartupId(); } #endif id.initId(startupId); id.setupStartupEnv(); QString exe(params.takeFirst()); const int exit_code = QProcess::execute(exe, params); KStartupInfo::resetStartupEnv(); qDebug() << "EXEC done"; // Test whether one of the files changed for (it = fileList.begin(); it != fileList.end(); ++it) { QString src = it->path; const QUrl dest = it->url; QFileInfo info(src); const bool uploadChanges = !mUseDaemon && !dest.isLocalFile(); if (info.exists() && (it->time != info.lastModified())) { if (mTempFiles) { if (KMessageBox::questionYesNo(nullptr, i18n("The supposedly temporary file\n%1\nhas been modified.\nDo you still want to delete it?", dest.toDisplayString(QUrl::PreferLocalFile)), i18n("File Changed"), KStandardGuiItem::del(), KGuiItem(i18n("Do Not Delete"))) != KMessageBox::Yes) continue; // don't delete the temp file } else if (uploadChanges) { // no upload when it's already a local file or kioexecd already did it. if (KMessageBox::questionYesNo(nullptr, i18n("The file\n%1\nhas been modified.\nDo you want to upload the changes?" , dest.toDisplayString()), i18n("File Changed"), KGuiItem(i18n("Upload")), KGuiItem(i18n("Do Not Upload"))) == KMessageBox::Yes) { qDebug() << "src='" << src << "' dest='" << dest << "'"; // Do it the synchronous way. KIO::CopyJob* job = KIO::copy(QUrl::fromLocalFile(src), dest); if (!job->exec()) { KMessageBox::error(nullptr, job->errorText()); continue; // don't delete the temp file } } } } if ((uploadChanges || mTempFiles) && exit_code == 0) { // Wait for a reasonable time so that even if the application forks on startup (like OOo or amarok) // it will have time to start up and read the file before it gets deleted. #130709. const int sleepSecs = 180; qDebug() << "sleeping for" << sleepSecs << "seconds before deleting file..."; QThread::sleep(sleepSecs); const QString parentDir = info.path(); qDebug() << sleepSecs << "seconds have passed, deleting" << info.filePath(); QFile(src).remove(); // NOTE: this is not necessarily a temporary directory. if (QDir().rmdir(parentDir)) { qDebug() << "Removed empty parent directory" << parentDir; } } } mExited = true; QApplication::exit(exit_code); } int main(int argc, char **argv) { QApplication app(argc, argv); KAboutData aboutData(QStringLiteral("kioexec"), i18n("KIOExec"), QStringLiteral(KIO_VERSION_STRING), i18n(description), KAboutLicense::GPL, i18n("(c) 1998-2000,2003 The KFM/Konqueror Developers")); aboutData.addAuthor(i18n("David Faure"), QString(), QStringLiteral("faure@kde.org")); aboutData.addAuthor(i18n("Stephan Kulow"), QString(), QStringLiteral("coolo@kde.org")); aboutData.addAuthor(i18n("Bernhard Rosenkraenzer"), QString(), QStringLiteral("bero@arklinux.org")); aboutData.addAuthor(i18n("Waldo Bastian"), QString(), QStringLiteral("bastian@kde.org")); aboutData.addAuthor(i18n("Oswald Buddenhagen"), QString(), QStringLiteral("ossi@kde.org")); KAboutData::setApplicationData(aboutData); KDBusService service(KDBusService::Multiple); QCommandLineParser parser; parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("tempfiles") , i18n("Treat URLs as local files and delete them afterwards"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("suggestedfilename"), i18n("Suggested file name for the downloaded file"), QStringLiteral("filename"))); parser.addPositionalArgument(QStringLiteral("command"), i18n("Command to execute")); parser.addPositionalArgument(QStringLiteral("urls"), i18n("URL(s) or local file(s) used for 'command'")); app.setQuitOnLastWindowClosed(false); aboutData.setupCommandLine(&parser); parser.process(app); aboutData.processCommandLine(&parser); if (parser.positionalArguments().count() < 1) { parser.showHelp(-1); return -1; } const bool tempfiles = parser.isSet(QStringLiteral("tempfiles")); const QString suggestedfilename = parser.value(QStringLiteral("suggestedfilename")); KIOExec exec(parser.positionalArguments(), tempfiles, suggestedfilename); // Don't go into the event loop if we already want to exit (#172197) if (exec.exited()) { return 0; } return app.exec(); }