diff --git a/autotests/krununittest.cpp b/autotests/krununittest.cpp index 4e6c51b1..7c35ad34 100644 --- a/autotests/krununittest.cpp +++ b/autotests/krununittest.cpp @@ -1,443 +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); + + qputenv("PATH", qgetenv("PATH") + QFile::encodeName(QDir::listSeparator() + QCoreApplication::applicationDirPath())); + // 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" 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 ktrash5 since it's installed by kio QString ktrash = QStandardPaths::findExecutable(QStringLiteral("ktrash5")); - if (ktrash.isEmpty()) { - ktrash = QStringLiteral("ktrash5"); - } + QVERIFY(!ktrash.isEmpty()); 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); QTest::newRow("%U l0") << "ktrash5 %U" << l0 << false << ktrash; QTest::newRow("%U l1") << "ktrash5 %U" << l1 << false << ktrash + " /tmp"; QTest::newRow("%U l2") << "ktrash5 %U" << l2 << false << ktrash + " http://localhost/foo"; QTest::newRow("%U l3") << "ktrash5 %U" << l3 << false << ktrash + " '/local/some file' http://remotehost.org/bar"; //QTest::newRow("%u l0") << "ktrash5 %u" << l0 << false << ktrash; // gives runtime warning QTest::newRow("%u l1") << "ktrash5 %u" << l1 << false << ktrash + " /tmp"; QTest::newRow("%u l2") << "ktrash5 %u" << l2 << false << ktrash + " http://localhost/foo"; //QTest::newRow("%u l3") << "ktrash5 %u" << l3 << false << ktrash; // gives runtime warning QTest::newRow("%F l0") << "ktrash5 %F" << l0 << false << ktrash; QTest::newRow("%F l1") << "ktrash5 %F" << l1 << false << ktrash + " /tmp"; QTest::newRow("%F l2") << "ktrash5 %F" << l2 << false << kioexecQuoted + " 'ktrash5 %F' http://localhost/foo"; QTest::newRow("%F l3") << "ktrash5 %F" << l3 << false << kioexecQuoted + " 'ktrash5 %F' 'file:///local/some file' http://remotehost.org/bar"; QTest::newRow("%F l1 tempfile") << "ktrash5 %F" << l1 << true << kioexecQuoted + " --tempfiles 'ktrash5 %F' file:///tmp"; QTest::newRow("%f l1 tempfile") << "ktrash5 %f" << l1 << true << kioexecQuoted + " --tempfiles 'ktrash5 %f' file:///tmp"; QTest::newRow("sh -c ktrash5 %F") << "sh -c \"ktrash5 \"'\\\"'\"%F\"'\\\"'" << l1 << false << m_sh + " -c 'ktrash5 \\\"/tmp\\\"'"; // This was originally with kmailservice5, but that relies on it being installed QTest::newRow("ktrash5 %u l1") << "ktrash5 %u" << l1 << false << ktrash + " /tmp"; QTest::newRow("ktrash5 %u l4") << "ktrash5 %u" << l4 << false << ktrash + " 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); const QStringList args = parser.resultingArguments(); QVERIFY2(!args.isEmpty(), qPrintable(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 398b0f68..c3396374 100644 --- a/src/core/desktopexecparser.cpp +++ b/src/core/desktopexecparser.cpp @@ -1,586 +1,585 @@ /* 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()) { 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 (!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, KShell::AbortOnMeta | KShell::TildeExpand); for (const QString &arg : args) { if (!arg.contains(QLatin1Char('='))) { return arg; } } return QString(); }