diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 05965ff5..f394af81 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,164 +1,165 @@ remove_definitions(-DQT_NO_CAST_FROM_ASCII) remove_definitions(-DQT_NO_CAST_FROM_BYTEARRAY) include(ECMAddTests) add_subdirectory(http) add_subdirectory(kcookiejar) find_package(Qt5Widgets REQUIRED) ########### unittests ############### find_package(Qt5Concurrent ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) ecm_add_tests( kacltest.cpp listdirtest.cpp kmountpointtest.cpp upurltest.cpp dataprotocoltest.cpp jobtest.cpp jobremotetest.cpp kfileitemtest.cpp kprotocolinfotest.cpp ktcpsockettest.cpp globaltest.cpp mkpathjobtest.cpp threadtest.cpp udsentrytest.cpp udsentry_benchmark.cpp kcoredirlister_benchmark.cpp deletejobtest.cpp urlutiltest.cpp batchrenamejobtest.cpp ksambasharetest.cpp NAME_PREFIX "kiocore-" LINK_LIBRARIES KF5::KIOCore KF5::I18n Qt5::Test Qt5::Network ) target_link_libraries(threadtest Qt5::Concurrent) ecm_add_test( http_jobtest.cpp httpserver_p.cpp TEST_NAME http_jobtest NAME_PREFIX "kiocore-" LINK_LIBRARIES KF5::KIOCore KF5::I18n Qt5::Test Qt5::Network ) include(FindGem) find_gem(ftpd) set_package_properties(Gem_ftpd PROPERTIES DESCRIPTION "Ruby gem 'ftpd' required for testing the ftp slave.") if(Gem_ftpd_FOUND) add_definitions(-DRubyExe_EXECUTABLE="${RubyExe_EXECUTABLE}") ecm_add_tests( ftptest.cpp NAME_PREFIX "kiocore-" LINK_LIBRARIES KF5::KIOCore KF5::I18n Qt5::Test Qt5::Network ) endif() if(UNIX) ecm_add_tests( privilegejobtest.cpp NAME_PREFIX "kiocore-" LINK_LIBRARIES KF5::KIOCore KF5::I18n Qt5::Test Qt5::Network ) endif() if (TARGET KF5::KIOGui) ecm_add_tests( favicontest.cpp applicationlauncherjobtest.cpp + openurljobtest.cpp commandlauncherjobtest.cpp NAME_PREFIX "kiogui-" LINK_LIBRARIES KF5::KIOCore KF5::KIOGui Qt5::Test ) target_link_libraries(favicontest Qt5::Concurrent) endif() if (TARGET KF5::KIOWidgets) ecm_add_tests( clipboardupdatertest.cpp dropjobtest.cpp kdynamicjobtrackernowidgetstest.cpp krununittest.cpp kdirlistertest.cpp kdirmodeltest.cpp kfileitemactionstest.cpp fileundomanagertest.cpp kurifiltertest.cpp kurlcompletiontest.cpp jobguitest.cpp pastetest.cpp accessmanagertest.cpp kurifiltersearchprovideractionstest.cpp NAME_PREFIX "kiowidgets-" LINK_LIBRARIES KF5::KIOCore KF5::KIOWidgets Qt5::Test Qt5::DBus ) set_target_properties(krununittest PROPERTIES COMPILE_FLAGS "-DCMAKE_INSTALL_FULL_LIBEXECDIR_KF5=\"\\\"${CMAKE_INSTALL_FULL_LIBEXECDIR_KF5}\\\"\"") # Same as accessmanagertest, but using QNetworkAccessManager, to make sure we # behave the same ecm_add_test( accessmanagertest.cpp TEST_NAME accessmanagertest-qnam NAME_PREFIX "kiowidgets-" LINK_LIBRARIES KF5::KIOCore KF5::KIOWidgets Qt5::Test ) set_target_properties(accessmanagertest-qnam PROPERTIES COMPILE_FLAGS "-DUSE_QNAM") # Same as kurlcompletiontest, but with immediate return, and results posted by thread later ecm_add_test( kurlcompletiontest.cpp TEST_NAME kurlcompletiontest-nowait NAME_PREFIX "kiowidgets-" LINK_LIBRARIES KF5::KIOCore KF5::KIOWidgets Qt5::Test ) set_target_properties(kurlcompletiontest-nowait PROPERTIES COMPILE_FLAGS "-DNO_WAIT") endif() if (TARGET KF5::KIOFileWidgets) find_package(KF5XmlGui ${KF5_DEP_VERSION} REQUIRED) include_directories(${CMAKE_SOURCE_DIR}/src/filewidgets ${CMAKE_BINARY_DIR}/src/filewidgets) ecm_add_tests( kurlnavigatortest.cpp kurlcomboboxtest.cpp kdiroperatortest.cpp kfilewidgettest.cpp kfilecustomdialogtest.cpp knewfilemenutest.cpp kfilecopytomenutest.cpp kfileplacesmodeltest.cpp kfileplacesviewtest.cpp kurlrequestertest.cpp NAME_PREFIX "kiofilewidgets-" LINK_LIBRARIES KF5::KIOFileWidgets KF5::KIOWidgets KF5::XmlGui KF5::Bookmarks Qt5::Test KF5::I18n ) # TODO: fix symbol exports for windows -> 'KSambaShare::KSambaShare': inconsistent dll linkage if (NOT WIN32) ecm_add_test( ksambashareprivatetest.cpp ../src/core/ksambashare.cpp ../src/core/kiocoredebug.cpp TEST_NAME ksambashareprivatetest NAME_PREFIX "kiocore-" LINK_LIBRARIES KF5::KIOCore Qt5::Test Qt5::Network ) endif() set_tests_properties(kiofilewidgets-kfileplacesmodeltest PROPERTIES RUN_SERIAL TRUE) set_tests_properties(kiofilewidgets-kfileplacesviewtest PROPERTIES RUN_SERIAL TRUE) endif() # this should be done by cmake, see bug 371721 if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND Qt5Core_VERSION VERSION_GREATER 5.8.0) set_property(TARGET jobtest APPEND PROPERTY AUTOMOC_MOC_OPTIONS --include ${CMAKE_BINARY_DIR}/src/core/moc_predefs.h) endif() diff --git a/autotests/krununittest.cpp b/autotests/krununittest.cpp index e9a9000c..9dfd423a 100644 --- a/autotests/krununittest.cpp +++ b/autotests/krununittest.cpp @@ -1,440 +1,440 @@ /* * 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 "x-term" KConfigGroup cg(KSharedConfig::openConfig(), "General"); cg.writeEntry("TerminalApplication", "x-term"); // 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"; } 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 "x-term -T ' - just_a_test' -e /bin/date -u", // 2 "x-term -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 "x-term -T ' - just_a_test' -e su sprallo -c '/bin/date -u'", // 6 "x-term -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"; } 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); } 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() +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"); } 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; } 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 } 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/autotests/openurljobtest.cpp b/autotests/openurljobtest.cpp new file mode 100644 index 00000000..7b968541 --- /dev/null +++ b/autotests/openurljobtest.cpp @@ -0,0 +1,453 @@ +/* + 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 "openurljobtest.h" +#include "openurljob.h" +#include +#include + +#include "kiotesthelper.h" // createTestFile etc. + +#include +#include +#include + +#ifdef Q_OS_UNIX +#include // kill +#endif + +#include +#include +#include +#include +#include +#include + +QTEST_GUILESS_MAIN(OpenUrlJobTest) + +extern KSERVICE_EXPORT int ksycoca_ms_between_checks; + +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; + +static const char s_tempServiceName[] = "openurljobtest_service.desktop"; + +void OpenUrlJobTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); + + // Ensure no leftovers from other tests + QDir(QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)).removeRecursively(); + // (including a mimeapps.list file) + // Don't remove ConfigLocation completely, it's useful when enabling debug output with kdebugsettings --test-mode + const QString mimeApps = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1String("/mimeapps.list"); + QFile::remove(mimeApps); + + ksycoca_ms_between_checks = 0; // need it to check the ksycoca mtime + const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/') + s_tempServiceName; + writeApplicationDesktopFile(fakeService); + m_filesToRemove.append(fakeService); + + // Ensure our service is the preferred one + KConfig mimeAppsCfg(mimeApps); + KConfigGroup grp = mimeAppsCfg.group("Default Applications"); + grp.writeEntry("text/plain", s_tempServiceName); + grp.writeEntry("text/html", s_tempServiceName); + grp.writeEntry("application/x-shellscript", s_tempServiceName); + grp.sync(); + + for (const char *mimeType : {"text/plain", "application/x-shellscript"}) { + KService::Ptr preferredTextEditor = KApplicationTrader::preferredService(QString::fromLatin1(mimeType)); + QVERIFY(preferredTextEditor); + QCOMPARE(preferredTextEditor->entryPath(), fakeService); + } + + // As used for preferredService + QVERIFY(KService::serviceByDesktopName("openurljobtest_service")); + + ksycoca_ms_between_checks = 5000; // all done, speed up again +} + +void OpenUrlJobTest::cleanupTestCase() +{ + for (const QString &file : m_filesToRemove) { + QFile::remove(file); + }; +} + +void OpenUrlJobTest::init() +{ + QFile::remove(m_tempDir.path() + "/dest"); +} + +static void createSrcFile(const QString &path) +{ + QFile srcFile(path); + QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString())); + srcFile.write("Hello world\n"); +} + +static QString readFile(const QString &path) +{ + QFile file(path); + file.open(QIODevice::ReadOnly); + return QString::fromLocal8Bit(file.readAll()).trimmed(); +} + +void OpenUrlJobTest::startProcess_data() +{ + QTest::addColumn("mimeType"); + QTest::addColumn("fileName"); + + // Known mimetype + QTest::newRow("text_file") << "text/plain" << "srcfile.txt"; + QTest::newRow("directory_file") << "application/x-desktop" << ".directory"; + QTest::newRow("desktop_file_link") << "application/x-desktop" << "srcfile.txt"; + QTest::newRow("desktop_file_link_preferred_service") << "application/x-desktop" << "srcfile.html"; + QTest::newRow("non_executable_script_running_not_allowed") << "application/x-shellscript" << "srcfile.sh"; + QTest::newRow("executable_script_running_not_allowed") << "application/x-shellscript" << "srcfile.sh"; + + // Require mimetype determination + QTest::newRow("text_file_no_mimetype") << QString() << "srcfile.txt"; + QTest::newRow("directory_file_no_mimetype") << QString() << ".directory"; +} + +void OpenUrlJobTest::startProcess() +{ + QFETCH(QString, mimeType); + QFETCH(QString, fileName); + + // Given a file to open + QTemporaryDir tempDir; + const QString srcDir = tempDir.path(); + const QString srcFile = srcDir + QLatin1Char('/') + fileName; + createSrcFile(srcFile); + QVERIFY(QFile::exists(srcFile)); + const bool isLink = QByteArray(QTest::currentDataTag()).startsWith("desktop_file_link"); + QUrl url = QUrl::fromLocalFile(srcFile); + if (isLink) { + const QString desktopFilePath = srcDir + QLatin1String("/link.desktop"); + KDesktopFile linkDesktopFile(desktopFilePath); + linkDesktopFile.desktopGroup().writeEntry("Type", "Link"); + linkDesktopFile.desktopGroup().writeEntry("URL", url); + const bool linkHasPreferredService = QByteArray(QTest::currentDataTag()) == "desktop_file_link_preferred_service"; + if (linkHasPreferredService) { + linkDesktopFile.desktopGroup().writeEntry("X-KDE-LastOpenedWith", "openurljobtest_service"); + } + url = QUrl::fromLocalFile(desktopFilePath); + } + if (QByteArray(QTest::currentDataTag()).startsWith("executable")) { + QFile file(srcFile); + QVERIFY(file.setPermissions(QFile::ExeUser | file.permissions())); + } + + // When running a OpenUrlJob + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mimeType, this); + QVERIFY2(job->exec(), qPrintable(job->errorString())); + + // Then the service should be executed (which writes to "dest") + const QString dest = m_tempDir.path() + "/dest"; + QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); + QCOMPARE(readFile(dest), srcFile); +} + +void OpenUrlJobTest::noServiceNoHandler() +{ + QTemporaryFile tempFile; + QVERIFY(tempFile.open()); + const QUrl url = QUrl::fromLocalFile(tempFile.fileName()); + const QString mimeType = QStringLiteral("application/x-zerosize"); + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mimeType, this); + // This is going to try QDesktopServices::openUrl which will fail because we are no QGuiApplication, good. + QTest::ignoreMessage(QtWarningMsg, "QDesktopServices::openUrl: Application is not a GUI application"); + QVERIFY(!job->exec()); + QCOMPARE(job->error(), KJob::UserDefinedError); + QCOMPARE(job->errorString(), QStringLiteral("Failed to open the file.")); +} + +void OpenUrlJobTest::invalidUrl() +{ + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl(":/"), QStringLiteral("text/plain"), this); + QVERIFY(!job->exec()); + QCOMPARE(job->error(), KIO::ERR_MALFORMED_URL); + QCOMPARE(job->errorString(), QStringLiteral("Malformed URL\nRelative URL's path component contains ':' before any '/'; source was \":/\"; path = \":/\"")); + + QUrl u; + u.setPath(QStringLiteral("/pathonly")); + KIO::OpenUrlJob *job2 = new KIO::OpenUrlJob(u, QStringLiteral("text/plain"), this); + QVERIFY(!job2->exec()); + QCOMPARE(job2->error(), KIO::ERR_MALFORMED_URL); + QCOMPARE(job2->errorString(), QStringLiteral("Malformed URL\n/pathonly")); +} + +void OpenUrlJobTest::refuseRunningNativeExecutables() +{ + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(QCoreApplication::applicationFilePath()), QStringLiteral("application/x-executable"), this); + QVERIFY(!job->exec()); + QCOMPARE(job->error(), KJob::UserDefinedError); + QVERIFY2(job->errorString().contains("For safety it will not be started"), qPrintable(job->errorString())); +} + +void OpenUrlJobTest::refuseRunningRemoteNativeExecutables() +{ + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl("protocol://host/path/exe"), QStringLiteral("application/x-executable"), this); + job->setRunExecutables(true); // even with this enabled, an error will occur + QVERIFY(!job->exec()); + QCOMPARE(job->error(), KJob::UserDefinedError); + QVERIFY2(job->errorString().contains("For safety it will not be started"), qPrintable(job->errorString())); +} + +KCONFIGCORE_EXPORT void reloadUrlActionRestrictions(); + +void OpenUrlJobTest::notAuthorized() +{ + KConfigGroup cg(KSharedConfig::openConfig(), "KDE URL Restrictions"); + cg.writeEntry("rule_count", 1); + cg.writeEntry("rule_1", QStringList{"open", {}, {}, {}, "file", "", "", "false"}); + cg.sync(); + reloadUrlActionRestrictions(); + + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl("file:///"), QStringLiteral("text/plain"), this); + QVERIFY(!job->exec()); + QCOMPARE(job->error(), KIO::ERR_ACCESS_DENIED); + QCOMPARE(job->errorString(), QStringLiteral("Access denied to file:///.")); + + cg.deleteEntry("rule_1"); + cg.deleteEntry("rule_count"); + cg.sync(); + reloadUrlActionRestrictions(); +} + +void OpenUrlJobTest::runScript_data() +{ + QTest::addColumn("mimeType"); + + QTest::newRow("shellscript") << "application/x-shellscript"; + QTest::newRow("native") << "application/x-executable"; +} + +void OpenUrlJobTest::runScript() +{ +#ifdef Q_OS_UNIX + QFETCH(QString, mimeType); + + // Given an executable shell script that copies "src" to "dest" + QTemporaryDir tempDir; + const QString dir = tempDir.path(); + createSrcFile(dir + QLatin1String("/src")); + const QString scriptFile = dir + QLatin1String("/script.sh"); + QFile file(scriptFile); + QVERIFY(file.open(QIODevice::WriteOnly)); + file.write("#!/bin/sh\ncp src dest"); + file.close(); + QVERIFY(file.setPermissions(QFile::ExeUser | file.permissions())); + + // When using OpenUrlJob to run the script + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(scriptFile), mimeType, this); + job->setRunExecutables(true); // startProcess tests the case where this isn't set + + // Then it works :-) + QVERIFY2(job->exec(), qPrintable(job->errorString())); + QTRY_VERIFY(QFileInfo::exists(dir + QLatin1String("/dest"))); // TRY because CommandLineLauncherJob finishes immediately +#endif +} + +void OpenUrlJobTest::runNativeExecutable_data() +{ + QTest::addColumn("withHandler"); + QTest::addColumn("handlerRetVal"); + + QTest::newRow("no_handler") << false << false; + QTest::newRow("handler_false") << true << false; + QTest::newRow("handler_true") << true << true; +} + +void OpenUrlJobTest::runNativeExecutable() +{ + QFETCH(bool, withHandler); + QFETCH(bool, handlerRetVal); + +#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(); + createSrcFile(dir + QLatin1String("/src")); + const QString scriptFile = dir + QLatin1String("/script.sh"); + QFile file(scriptFile); + QVERIFY(file.open(QIODevice::WriteOnly)); + file.write("#!/bin/sh\ncp src dest"); + file.close(); + // Note that it's missing executable permissions + + s_handler.m_calls.clear(); + s_handler.setRetVal(handlerRetVal); + KIO::setDefaultUntrustedProgramHandler(withHandler ? &s_handler : nullptr); + + // When using OpenUrlJob to run the executable + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(scriptFile), QStringLiteral("application/x-executable"), this); + job->setRunExecutables(true); // startProcess tests the case where this isn't set + const bool success = job->exec(); + + // Then --- it depends on what the user says via the handler + if (!withHandler) { + QVERIFY(!success); + QCOMPARE(job->error(), KJob::UserDefinedError); + QCOMPARE(job->errorString(), QStringLiteral("The program \"%1\" needs to have executable permission before it can be launched.").arg(scriptFile)); + } else { + if (handlerRetVal) { + QVERIFY(success); + QTRY_VERIFY(QFileInfo::exists(dir + QLatin1String("/dest"))); // TRY because CommandLineLauncherJob finishes immediately + } else { + QVERIFY(!success); + QCOMPARE(job->error(), KIO::ERR_USER_CANCELED); + } + } +#endif +} + +void OpenUrlJobTest::launchExternalBrowser_data() +{ + QTest::addColumn("useBrowserApp"); + QTest::addColumn("useSchemeHandler"); + + QTest::newRow("browserapp") << true << false; + QTest::newRow("scheme_handler") << false << true; +} + +void OpenUrlJobTest::launchExternalBrowser() +{ +#ifdef Q_OS_UNIX + QFETCH(bool, useBrowserApp); + QFETCH(bool, useSchemeHandler); + + QTemporaryDir tempDir; + const QString dir = tempDir.path(); + createSrcFile(dir + QLatin1String("/src")); + const QString scriptFile = dir + QLatin1String("/browser.sh"); + QFile file(scriptFile); + QVERIFY(file.open(QIODevice::WriteOnly)); + file.write("#!/bin/sh\necho $1 > `dirname $0`/destbrowser"); + file.close(); + QVERIFY(file.setPermissions(QFile::ExeUser | file.permissions())); + + QUrl remoteImage("http://example.org/image.jpg"); + if (useBrowserApp) { + KConfigGroup(KSharedConfig::openConfig(), "General").writeEntry("BrowserApplication", QString(QLatin1Char('!') + scriptFile)); + } else if (useSchemeHandler) { + remoteImage.setScheme("scheme"); + } + + // When using OpenUrlJob to run the script + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(remoteImage, this); + + // Then it works :-) + QVERIFY2(job->exec(), qPrintable(job->errorString())); + QString dest; + if (useBrowserApp) { + dest = dir + QLatin1String("/destbrowser"); + } else if (useSchemeHandler) { + dest = m_tempDir.path() + QLatin1String("/dest"); // see the .desktop file in writeApplicationDesktopFile + } + QTRY_VERIFY(QFileInfo::exists(dest)); // TRY because CommandLineLauncherJob finishes immediately + QCOMPARE(readFile(dest), remoteImage.toString()); + + // Restore settings + KConfigGroup(KSharedConfig::openConfig(), "General").deleteEntry("BrowserApplication"); +#endif +} + +void OpenUrlJobTest::nonExistingFile() +{ + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(QStringLiteral("/does/not/exist")), this); + QVERIFY(!job->exec()); + QCOMPARE(job->error(), KIO::ERR_DOES_NOT_EXIST); + QCOMPARE(job->errorString(), "The file or folder /does/not/exist does not exist."); +} + +void OpenUrlJobTest::httpUrlWithKIO() +{ + // This tests the scanFileWithGet() code path + const QUrl url(QStringLiteral("http://www.google.com/")); + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, this); + job->setFollowRedirections(false); + QVERIFY2(job->exec(), qPrintable(job->errorString())); + + // Then the service should be executed (which writes to "dest") + const QString dest = m_tempDir.path() + "/dest"; + QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); + QCOMPARE(readFile(dest), url.toString()); +} + +void OpenUrlJobTest::ftpUrlWithKIO() +{ + // This is just to test the statFile() code at least a bit + const QUrl url(QStringLiteral("ftp://localhost:2")); // unlikely that anything is running on that port + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, this); + QVERIFY(!job->exec()); + QCOMPARE(job->errorString(), "Could not connect to host localhost: Connection refused."); +} + +void OpenUrlJobTest::takeOverAfterMimeTypeFound() +{ + // Given a local image file + QTemporaryDir tempDir; + const QString srcDir = tempDir.path(); + const QString srcFile = srcDir + QLatin1String("/image.jpg"); + createSrcFile(srcFile); + + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(srcFile), this); + QString foundMime = QStringLiteral("NONE"); + connect(job, &KIO::OpenUrlJob::mimeTypeFound, this, [&](const QString &mimeType) { + foundMime = mimeType; + job->kill(); + }); + QVERIFY(!job->exec()); + QCOMPARE(job->error(), KJob::KilledJobError); + QCOMPARE(foundMime, "image/jpeg"); +} + +void OpenUrlJobTest::writeApplicationDesktopFile(const QString &filePath) +{ + KDesktopFile file(filePath); + KConfigGroup group = file.desktopGroup(); + group.writeEntry("Name", "KRunUnittestService"); + group.writeEntry("MimeType", "text/plain;application/x-shellscript;x-scheme-handler/scheme"); + group.writeEntry("Type", "Application"); + group.writeEntry("Exec", QByteArray("echo %u > " + QFile::encodeName(m_tempDir.path()) + "/dest")); // not using %d because of remote urls + QVERIFY(file.sync()); +} diff --git a/autotests/openurljobtest.h b/autotests/openurljobtest.h new file mode 100644 index 00000000..d6d0a632 --- /dev/null +++ b/autotests/openurljobtest.h @@ -0,0 +1,67 @@ +/* + This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef OPENURLJOBTEST_H +#define OPENURLJOBTEST_H + +#include +#include +#include + +class OpenUrlJobTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void init(); + + void startProcess_data(); + void startProcess(); + + void noServiceNoHandler(); + void invalidUrl(); + void refuseRunningNativeExecutables(); + void refuseRunningRemoteNativeExecutables(); + void notAuthorized(); + void runScript_data(); + void runScript(); + void runNativeExecutable_data(); + void runNativeExecutable(); + void launchExternalBrowser_data(); + void launchExternalBrowser(); + void nonExistingFile(); + + void httpUrlWithKIO(); + void ftpUrlWithKIO(); + + void takeOverAfterMimeTypeFound(); + +private: + void writeApplicationDesktopFile(const QString &filePath); + + QStringList m_filesToRemove; + QTemporaryDir m_tempDir; +}; + +#endif /* OPENURLJOBTEST_H */ + diff --git a/src/core/kurlauthorized.h b/src/core/kurlauthorized.h index 816c2df7..3abace3c 100644 --- a/src/core/kurlauthorized.h +++ b/src/core/kurlauthorized.h @@ -1,137 +1,138 @@ /* This file is part of the KDE libraries Copyright (C) 1997 Matthias Kalle Dalheimer (kalle@kde.org) Copyright (c) 1998, 1999 Waldo Bastian 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. */ #ifndef KURLAUTHORIZED_H #define KURLAUTHORIZED_H #include "kiocore_export.h" #include class QUrl; class QString; /** * The functions in this namespace allow actions to be restricted based * on the URL they operate on (see the KAuthorized namespace in * KConfig). * * As with KAuthorized functions, the relevant settings are read from * the application's KSharedConfig instance, so actions can be disabled * on a per-application or global basis (by using the kdeglobals file). * * URLs can be matched based on protocol, host and path, and the * referring URL can be taken into account. * * URL-based restrictions are recorded using this syntax: * @verbatim [KDE URL Restrictions] rule_count= rule_1=,,,,,,, ... rule_N=,,,,,,, @endverbatim * * The following standard actions are defined: * * - redirect: A common example is a web page redirecting to another web * page. By default, internet protocols are not permitted * to redirect to the "file" protocol, but you could * override this for a specific host, for example: * @verbatim [KDE URL Restrictions] rule_count=1 rule_1=redirect,http,myhost.example.com,,file,,,true @endverbatim * - list: Determines whether a URL can be browsed, in an "open" or * "save" dialog, for example. If a user should only be * able to browse files under home directory one could use: * @verbatim [KDE URL Restrictions] rule_count=2 rule_1=list,,,,file,,,false rule_2=list,,,,file,,$HOME,true @endverbatim * The first rule disables browsing any directories on the * local filesystem. The second rule then enables browsing * the users home directory. * - open: This controls which files can be opened by the user in * applications. It also affects where users can save files. * To only allow a user to open the files in his own home * directory one could use: * @verbatim [KDE URL Restrictions] rule_count=3 rule_1=open,,,,file,,,false rule_2=open,,,,file,,$HOME,true rule_3=open,,,,file,,$TMP,true @endverbatim * Note that with the above, users would still be able to * open files from the internet. Note also that the user is * also given access to $TMP in order to ensure correct * operation of KDE applications. $TMP is replaced with the * temporary directory that KDE uses for this user. * - link: Determines whether a URL can be linked to. * * Some remarks: * - empty entries match everything * - host names may start with a wildcard, e.g. "*.acme.com" * - a protocol also matches similar protocols that start with the same name, * e.g. "http" matches both http and https. You can use "http!" if you only want to * match http (and not https) * - specifying a path matches all URLs that start with the same path. For better results * you should not include a trailing slash. If you want to specify one specific path, you can * add an exclamation mark. E.g. "/srv" matches both "/srv" and "/srv/www" but "/srv!" only * matches "/srv" and not "/srv/www". */ namespace KUrlAuthorized { /** * Returns whether a certain URL related action is authorized. * * @param action The name of the action, typically one of "list", * "link", "open" or "redirect". * @param baseUrl The url where the action originates from. * @param destUrl The object of the action. * @return @c true if the action is authorized, @c false * otherwise. * * @see allowUrlAction() * @since 5.0 */ KIOCORE_EXPORT bool authorizeUrlAction(const QString &action, const QUrl &baseUrl, const QUrl &destUrl); /** * Override Kiosk restrictions to allow a given URL action. * * This can be useful if your application needs to ensure access to an * application-specific directory that may otherwise be subject to Kiosk * restrictions. * * @param action The name of the action. * @param baseUrl The url where the action originates from. * @param destUrl The object of the action. * * @see authorizeUrlAction() * @since 5.0 */ -KIOCORE_EXPORT void allowUrlAction(const QString &action, const QUrl &baseUrl, const QUrl &destUrl); +KIOCORE_EXPORT void allowUrlAction(const QString &action, const QUrl &baseUrl, const QUrl &destUrl); + } #endif diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 665bfb72..46944af3 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1,74 +1,77 @@ configure_file(config-kiogui.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kiogui.h) set(kiogui_SRCS - faviconrequestjob.cpp applicationlauncherjob.cpp commandlauncherjob.cpp + faviconrequestjob.cpp + openurljob.cpp + openurljobhandlerinterface.cpp kprocessrunner.cpp ) ecm_qt_declare_logging_category(kiogui_SRCS HEADER kiogui_debug.h IDENTIFIER KIO_GUI CATEGORY_NAME kf5.kio.gui DESCRIPTION "KIOGui (KIO)" EXPORT KIO ) ecm_qt_declare_logging_category(kiogui_SRCS HEADER favicons_debug.h IDENTIFIER FAVICONS_LOG CATEGORY_NAME kf5.kio.favicons DESCRIPTION "FavIcons (KIO)" EXPORT KIO ) add_library(KF5KIOGui ${kiogui_SRCS}) generate_export_header(KF5KIOGui BASE_NAME KIOGui) add_library(KF5::KIOGui ALIAS KF5KIOGui) target_include_directories(KF5KIOGui INTERFACE "$") target_link_libraries(KF5KIOGui PUBLIC KF5::KIOCore KF5::WindowSystem Qt5::Gui PRIVATE KF5::I18n ) set_target_properties(KF5KIOGui PROPERTIES VERSION ${KIO_VERSION_STRING} SOVERSION ${KIO_SOVERSION} EXPORT_NAME KIOGui ) # Headers prefixed with KIO/ ecm_generate_headers(KIOGui_CamelCase_HEADERS HEADER_NAMES - FavIconRequestJob ApplicationLauncherJob CommandLauncherJob + FavIconRequestJob + OpenUrlJob PREFIX KIO REQUIRED_HEADERS KIO_namespaced_gui_HEADERS ) install(FILES ${KIOGui_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOGui/KIO COMPONENT Devel) install(TARGETS KF5KIOGui EXPORT KF5KIOTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${KIO_namespaced_gui_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOGui/kio COMPONENT Devel) install(FILES ${KIOGui_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/kiogui_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOGui COMPONENT Devel) # make available to ecm_add_qch in parent folder set(KIOGui_QCH_SOURCES ${KIOGui_HEADERS} ${KIO_namespaced_gui_HEADERS} PARENT_SCOPE) include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KIOGui LIB_NAME KF5KIOGui DEPS "KIOCore" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOGui) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/gui/applicationlauncherjob.cpp b/src/gui/applicationlauncherjob.cpp index 772f7c18..7be2b4f5 100644 --- a/src/gui/applicationlauncherjob.cpp +++ b/src/gui/applicationlauncherjob.cpp @@ -1,236 +1,239 @@ /* This file is part of the KDE libraries Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "applicationlauncherjob.h" #include "kprocessrunner_p.h" #include "untrustedprogramhandlerinterface.h" #include "kiogui_debug.h" #include "../core/global.h" #include #include #include #include +// KF6 TODO: Remove static KIO::UntrustedProgramHandlerInterface *s_untrustedProgramHandler = nullptr; namespace KIO { // Hidden API because in KF6 we'll just check if the job's uiDelegate implements UntrustedProgramHandlerInterface. KIOGUI_EXPORT void setDefaultUntrustedProgramHandler(KIO::UntrustedProgramHandlerInterface *iface) { s_untrustedProgramHandler = iface; } +// For OpenUrlJob +KIO::UntrustedProgramHandlerInterface *defaultUntrustedProgramHandler() { return s_untrustedProgramHandler; } } #include class KIO::ApplicationLauncherJobPrivate { public: explicit ApplicationLauncherJobPrivate(const KService::Ptr &service) : m_service(service) {} void slotStarted(KIO::ApplicationLauncherJob *q, KProcessRunner *processRunner) { m_pids.append(processRunner->pid()); if (--m_numProcessesPending == 0) { q->emitResult(); } } KService::Ptr m_service; QList m_urls; KIO::ApplicationLauncherJob::RunFlags m_runFlags; QString m_suggestedFileName; QByteArray m_startupId; QVector m_pids; QVector m_processRunners; int m_numProcessesPending = 0; }; KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KService::Ptr &service, QObject *parent) : KJob(parent), d(new ApplicationLauncherJobPrivate(service)) { } KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KServiceAction &serviceAction, QObject *parent) : ApplicationLauncherJob(serviceAction.service(), parent) { Q_ASSERT(d->m_service); d->m_service.detach(); d->m_service->setExec(serviceAction.exec()); } KIO::ApplicationLauncherJob::~ApplicationLauncherJob() { // Do *NOT* delete the KProcessRunner instances here. // We need it to keep running so it can terminate startup notification on process exit. } void KIO::ApplicationLauncherJob::setUrls(const QList &urls) { d->m_urls = urls; } void KIO::ApplicationLauncherJob::setRunFlags(RunFlags runFlags) { d->m_runFlags = runFlags; } void KIO::ApplicationLauncherJob::setSuggestedFileName(const QString &suggestedFileName) { d->m_suggestedFileName = suggestedFileName; } void KIO::ApplicationLauncherJob::setStartupId(const QByteArray &startupId) { d->m_startupId = startupId; } void KIO::ApplicationLauncherJob::emitUnauthorizedError() { setError(KJob::UserDefinedError); setErrorText(i18n("You are not authorized to execute this file.")); emitResult(); } void KIO::ApplicationLauncherJob::start() { emit description(this, i18nc("Launching application", "Launching %1", d->m_service->name()), {}, {}); // First, the security checks if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) { // KIOSK restriction, cannot be circumvented emitUnauthorizedError(); return; } if (!d->m_service->entryPath().isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(d->m_service->entryPath())) { // We can use QStandardPaths::findExecutable to resolve relative pathnames // but that gets rid of the command line arguments. QString program = QFileInfo(d->m_service->exec()).canonicalFilePath(); if (program.isEmpty()) { // e.g. due to command line arguments program = d->m_service->exec(); } if (!s_untrustedProgramHandler) { emitUnauthorizedError(); return; } connect(s_untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, this, [this](bool result) { if (result) { // Assume that service is an absolute path since we're being called (relative paths // would have been allowed unless Kiosk said no, therefore we already know where the // .desktop file is. Now add a header to it if it doesn't already have one // and add the +x bit. QString errorString; if (s_untrustedProgramHandler->makeServiceFileExecutable(d->m_service->entryPath(), errorString)) { proceedAfterSecurityChecks(); } else { QString serviceName = d->m_service->name(); if (serviceName.isEmpty()) { serviceName = d->m_service->genericName(); } setError(KJob::UserDefinedError); setErrorText(i18n("Unable to make the service %1 executable, aborting execution.\n%2.", serviceName, errorString)); emitResult(); } } else { setError(KIO::ERR_USER_CANCELED); emitResult(); } }); s_untrustedProgramHandler->showUntrustedProgramWarning(this, d->m_service->name()); return; } proceedAfterSecurityChecks(); } void KIO::ApplicationLauncherJob::proceedAfterSecurityChecks() { if (d->m_urls.count() > 1 && !d->m_service->allowMultipleFiles()) { // We need to launch the application N times. // We ignore the result for application 2 to N. // For the first file we launch the application in the // usual way. The reported result is based on this application. d->m_numProcessesPending = d->m_urls.count(); d->m_processRunners.reserve(d->m_numProcessesPending); for (int i = 1; i < d->m_urls.count(); ++i) { auto *processRunner = new KProcessRunner(d->m_service, { d->m_urls.at(i) }, d->m_runFlags, d->m_suggestedFileName, QByteArray()); d->m_processRunners.push_back(processRunner); connect(processRunner, &KProcessRunner::processStarted, this, [this, processRunner]() { d->slotStarted(this, processRunner); }); } d->m_urls = { d->m_urls.at(0) }; } else { d->m_numProcessesPending = 1; } auto *processRunner = new KProcessRunner(d->m_service, d->m_urls, d->m_runFlags, d->m_suggestedFileName, d->m_startupId); d->m_processRunners.push_back(processRunner); connect(processRunner, &KProcessRunner::error, this, [this](const QString &errorText) { setError(KJob::UserDefinedError); setErrorText(errorText); emitResult(); }); connect(processRunner, &KProcessRunner::processStarted, this, [this, processRunner]() { d->slotStarted(this, processRunner); }); } // For KRun bool KIO::ApplicationLauncherJob::waitForStarted() { if (error() != KJob::NoError) { return false; } if (d->m_processRunners.isEmpty()) { // Maybe we're in the security prompt... // Can't avoid the nested event loop // This fork of KJob::exec doesn't set QEventLoop::ExcludeUserInputEvents const bool wasAutoDelete = isAutoDelete(); setAutoDelete(false); QEventLoop loop; connect(this, &KJob::result, this, [&](KJob *job) { loop.exit(job->error()); }); const int ret = loop.exec(); if (wasAutoDelete) { deleteLater(); } return ret != KJob::NoError; } const bool ret = std::all_of(d->m_processRunners.cbegin(), d->m_processRunners.cend(), [](KProcessRunner *r) { return r->waitForStarted(); }); for (KProcessRunner *r : qAsConst(d->m_processRunners)) { qApp->sendPostedEvents(r); // so slotStarted gets called } return ret; } qint64 KIO::ApplicationLauncherJob::pid() const { return d->m_pids.at(0); } QVector KIO::ApplicationLauncherJob::pids() const { return d->m_pids; } diff --git a/src/gui/applicationlauncherjob.h b/src/gui/applicationlauncherjob.h index 40fd4408..9964ed37 100644 --- a/src/gui/applicationlauncherjob.h +++ b/src/gui/applicationlauncherjob.h @@ -1,167 +1,168 @@ /* This file is part of the KDE libraries Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIO_APPLICATIONLAUNCHERJOB_H #define KIO_APPLICATIONLAUNCHERJOB_H #include "kiogui_export.h" #include #include #include class KRun; // KF6 REMOVE class ApplicationLauncherJobTest; // KF6 REMOVE namespace KIO { class ApplicationLauncherJobPrivate; /** * @class ApplicationLauncherJob applicationlauncherjob.h * * @brief ApplicationLauncherJob runs an application and watches it while running. * * It creates a startup notification and finishes it on success or on error (for the taskbar). * It also emits an error message if necessary (e.g. "program not found"). * * When passing multiple URLs to an application that doesn't support opening * multiple files, the application will be launched once for each URL. * * The job finishes when the application is successfully started. At that point you can * query the PID(s). * * For error handling, either connect to the result() signal, or for a simple messagebox on error, * you can do * @code * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this)); * @endcode * Using JobUiDelegate (which is widgets based) also enables the feature of asking the user * in case the executable or desktop file isn't marked as executable. Otherwise the job will * just refuse executing those files. * * @since 5.69 */ class KIOGUI_EXPORT ApplicationLauncherJob : public KJob { public: /** * Creates an ApplicationLauncherJob. * @param service the service (application desktop file) to run * @param parent the parent QObject */ explicit ApplicationLauncherJob(const KService::Ptr &service, QObject *parent = nullptr); /** * Creates an ApplicationLauncherJob. * @param serviceAction the service action to run * @param parent the parent QObject */ explicit ApplicationLauncherJob(const KServiceAction &serviceAction, QObject *parent = nullptr); /** * Destructor * * Note that jobs auto-delete themselves after emitting result. * Deleting/killing the job will not stop the started application. */ ~ApplicationLauncherJob() override; /** * Specifies the URLs to be passed to the application. * @param urls list of files (local or remote) to open * * Note that when passing multiple URLs to an application that doesn't support opening * multiple files, the application will be launched once for each URL. */ void setUrls(const QList &urls); /** * @see RunFlag */ enum RunFlag { DeleteTemporaryFiles = 0x1, ///< the URLs passed to the service will be deleted when it exits (if the URLs are local files) }; /** * Stores a combination of #RunFlag values. */ Q_DECLARE_FLAGS(RunFlags, RunFlag) /** * Specifies various flags. * @param runFlags the flags to be set. For instance, whether the URLs are temporary files that should be deleted after execution. */ void setRunFlags(RunFlags runFlags); /** * Sets the file name to use in the case of downloading the file to a tempfile * in order to give to a non-URL-aware application. * Some apps rely on the extension to determine the mimetype of the file. * Usually the file name comes from the URL, but in the case of the * HTTP Content-Disposition header, we need to override the file name. * @param suggestedFileName the file name */ void setSuggestedFileName(const QString &suggestedFileName); /** * Sets the startup notification id of the application launch. * @param startupId startup notification id, if any (otherwise ""). */ void setStartupId(const QByteArray &startupId); /** * Starts the job. * You must call this, after having done all the setters. + * This is (potentially) a GUI job, never use exec(), it would block user interaction. */ void start() override; /** * @return the PID of the application that was started * * Convenience method for pids().at(0). You should only use this when specifying zero or one URL, * or when you are sure that the application supports opening multiple files. Otherwise use pids(). * Available after the job emits result(). */ qint64 pid() const; /** * @return the PIDs of the applications that were started * * Available after the job emits result(). */ QVector pids() const; private: friend class ::KRun; // KF6 REMOVE friend class ::ApplicationLauncherJobTest; // KF6 REMOVE /** * Blocks until the process has started. Only exists for KRun, will disappear in KF6. */ bool waitForStarted(); void emitUnauthorizedError(); void proceedAfterSecurityChecks(); friend class ApplicationLauncherJobPrivate; QScopedPointer d; }; } // namespace KIO #endif diff --git a/src/gui/openurljob.cpp b/src/gui/openurljob.cpp new file mode 100644 index 00000000..61e6cc55 --- /dev/null +++ b/src/gui/openurljob.cpp @@ -0,0 +1,649 @@ +/* + 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 "openurljob.h" +#include "openurljobhandlerinterface.h" +#include "global.h" +#include "job.h" // for buildErrorString +#include "commandlauncherjob.h" +#include "desktopexecparser.h" +#include "untrustedprogramhandlerinterface.h" +#include "kiogui_debug.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +static KIO::OpenUrlJobHandlerInterface *s_openUrlJobHandler = nullptr; +namespace KIO { +// Hidden API because in KF6 we'll just check if the job's uiDelegate implements OpenUrlJobHandlerInterface. +KIOGUI_EXPORT void setDefaultOpenUrlJobHandler(KIO::OpenUrlJobHandlerInterface *iface) { s_openUrlJobHandler = iface; } +} + +class KIO::OpenUrlJobPrivate +{ +public: + explicit OpenUrlJobPrivate(const QUrl &url, OpenUrlJob *qq) + : m_url(url), q(qq) { + q->setCapabilities(KJob::Killable); + } + + void emitAccessDenied(); + void runUrlWithMimeType(); + QString externalBrowser() const; + bool runExternalBrowser(const QString &exe); + void useSchemeHandler(); + void determineLocalMimeType(); + void statFile(); + void scanFileWithGet(); + + QUrl m_url; + KIO::OpenUrlJob * const q; + QString m_suggestedFileName; + QByteArray m_startupId; + QString m_mimeTypeName; + KService::Ptr m_preferredService; + bool m_deleteTemporaryFile = false; + bool m_runExecutables = false; + bool m_externalBrowserEnabled = true; + bool m_followRedirections = true; + +private: + void executeCommand(); + bool handleExecutables(const QMimeType &mimeType); + void runLink(const QString &filePath, const QString &urlStr, const QString &optionalServiceName); + void showOpenWithDialog(); + void startService(const KService::Ptr &service); +}; + +KIO::OpenUrlJob::OpenUrlJob(const QUrl &url, QObject *parent) + : KCompositeJob(parent), d(new OpenUrlJobPrivate(url, this)) +{ +} + +KIO::OpenUrlJob::OpenUrlJob(const QUrl &url, const QString &mimeType, QObject *parent) + : KCompositeJob(parent), d(new OpenUrlJobPrivate(url, this)) +{ + d->m_mimeTypeName = mimeType; +} + +KIO::OpenUrlJob::~OpenUrlJob() +{ +} + +void KIO::OpenUrlJob::setDeleteTemporaryFile(bool b) +{ + d->m_deleteTemporaryFile = b; +} + +void KIO::OpenUrlJob::setSuggestedFileName(const QString &suggestedFileName) +{ + d->m_suggestedFileName = suggestedFileName; +} + +void KIO::OpenUrlJob::setStartupId(const QByteArray &startupId) +{ + d->m_startupId = startupId; +} + +void KIO::OpenUrlJob::setRunExecutables(bool allow) +{ + d->m_runExecutables = allow; +} + +void KIO::OpenUrlJob::setEnableExternalBrowser(bool b) +{ + d->m_externalBrowserEnabled = b; +} + +void KIO::OpenUrlJob::setFollowRedirections(bool b) +{ + d->m_followRedirections = b; +} + +static bool checkNeedPortalSupport() +{ + return !QStandardPaths::locate(QStandardPaths::RuntimeLocation, + QLatin1String("flatpak-info")).isEmpty() || + qEnvironmentVariableIsSet("SNAP"); +} + +void KIO::OpenUrlJob::start() +{ + if (!d->m_url.isValid() || d->m_url.scheme().isEmpty()) { + const QString error = !d->m_url.isValid() ? d->m_url.errorString() : d->m_url.toDisplayString(); + setError(KIO::ERR_MALFORMED_URL); + setErrorText(i18n("Malformed URL\n%1", error)); + emitResult(); + return; + } + if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), d->m_url)) { + d->emitAccessDenied(); + return; + } + if (d->m_externalBrowserEnabled && checkNeedPortalSupport()) { + // Use the function from QDesktopServices as it handles portals correctly + // Note that it falls back to "normal way" if the portal service isn't running. + if (!QDesktopServices::openUrl(d->m_url)) { + // Is this an actual error, or USER_CANCELED? + setError(KJob::UserDefinedError); + setErrorText(i18n("Failed to open %1", d->m_url.toDisplayString())); + } + emitResult(); + return; + } + + // If we know the mimetype, proceed + if (!d->m_mimeTypeName.isEmpty()) { + d->runUrlWithMimeType(); + return; + } + + if (d->m_externalBrowserEnabled && d->m_url.scheme().startsWith(QLatin1String("http"))) { + const QString externalBrowser = d->externalBrowser(); + if (!externalBrowser.isEmpty() && d->runExternalBrowser(externalBrowser)) { + return; + } + } + if (KIO::DesktopExecParser::hasSchemeHandler(d->m_url)) { + d->useSchemeHandler(); + return; + } + + if (!KProtocolManager::supportsListing(d->m_url)) { + // No support for listing => it can't be a directory (example: http) + d->scanFileWithGet(); + return; + } + + // It may be a directory or a file, let's use stat to find out + d->statFile(); +} + +bool KIO::OpenUrlJob::doKill() +{ + return true; +} + +QString KIO::OpenUrlJobPrivate::externalBrowser() const +{ + if (!m_externalBrowserEnabled) { + return QString(); + } + + const QString browserApp = KConfigGroup(KSharedConfig::openConfig(), "General").readEntry("BrowserApplication"); + if (!browserApp.isEmpty()) { + return browserApp; + } + + // If a default browser isn't set in kdeglobals, fall back to mimeapps.list + KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); + const KConfigGroup defaultApps(profile, "Default Applications"); + + QString externalBrowser = defaultApps.readEntry("x-scheme-handler/https"); + if (externalBrowser.isEmpty()) { + externalBrowser = defaultApps.readEntry("x-scheme-handler/http"); + } + return externalBrowser; +} + +bool KIO::OpenUrlJobPrivate::runExternalBrowser(const QString &exec) +{ + if (exec.startsWith(QLatin1Char('!'))) { + // Literal command + const QString command = exec.midRef(1) + QLatin1String(" %u"); + KService::Ptr service(new KService(QString(), command, QString())); + startService(service); + return true; + } else { + // Name of desktop file + KService::Ptr service = KService::serviceByStorageId(exec); + if (service) { + startService(service); + return true; + } + } + return false; +} + +void KIO::OpenUrlJobPrivate::useSchemeHandler() +{ + // look for an application associated with x-scheme-handler/ + const KService::Ptr service = KApplicationTrader::preferredService(QLatin1String("x-scheme-handler/") + m_url.scheme()); + if (service) { + startService(service); + return; + } + // fallback, look for associated helper protocol + Q_ASSERT(KProtocolInfo::isHelperProtocol(m_url.scheme())); + const auto exec = KProtocolInfo::exec(m_url.scheme()); + if (exec.isEmpty()) { + // use default mimetype opener for file + m_mimeTypeName = KProtocolManager::defaultMimetype(m_url); + runUrlWithMimeType(); + } else { + KService::Ptr service(new KService(QString(), exec, QString())); + startService(service); + } +} + +void KIO::OpenUrlJobPrivate::statFile() +{ + Q_ASSERT(m_mimeTypeName.isEmpty()); + + KIO::StatJob *job = KIO::statDetails(m_url, KIO::StatJob::SourceSide, KIO::StatBasic, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + QObject::connect(job, &KJob::result, + q, [=]() { + const int errCode = job->error(); + if (errCode) { + // ERR_NO_CONTENT is not an error, but an indication no further + // actions needs to be taken. + if (errCode != KIO::ERR_NO_CONTENT) { + q->setError(errCode); + q->setErrorText(KIO::buildErrorString(errCode, job->errorText())); + } + q->emitResult(); + return; + } + if (m_followRedirections) { // Update our URL in case of a redirection + m_url = job->url(); + } + + const KIO::UDSEntry entry = job->statResult(); + + const QString localPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); + if (!localPath.isEmpty()) { + m_url = QUrl::fromLocalFile(localPath); + } + + // mimetype already known? (e.g. print:/manager) + m_mimeTypeName = entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE); + if (!m_mimeTypeName.isEmpty()) { + runUrlWithMimeType(); + return; + } + + const mode_t mode = entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE); + if ((mode & QT_STAT_MASK) == QT_STAT_DIR) { // it's a dir + m_mimeTypeName = QStringLiteral("inode/directory"); + runUrlWithMimeType(); + } else { // it's a file + // Start the timer. Once we get the timer event this + // protocol server is back in the pool and we can reuse it. + // This gives better performance than starting a new slave + QTimer::singleShot(0, q, [this] { scanFileWithGet(); }); + } + }); +} + +void KIO::OpenUrlJobPrivate::startService(const KService::Ptr &service) +{ + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service, q); + job->setUrls({m_url}); + job->setRunFlags(m_deleteTemporaryFile ? KIO::ApplicationLauncherJob::DeleteTemporaryFiles : KIO::ApplicationLauncherJob::RunFlags{}); + job->setSuggestedFileName(m_suggestedFileName); + job->setStartupId(m_startupId); + job->setUiDelegate(q->uiDelegate()); + q->addSubjob(job); + job->start(); +} + +static QMimeType fixupMimeType(const QString &mimeType, const QString &fileName) +{ + QMimeDatabase db; + QMimeType mime = db.mimeTypeForName(mimeType); + if ((!mime.isValid() || mime.isDefault()) && !fileName.isEmpty()) { + mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchExtension); + } + return mime; +} + +void KIO::OpenUrlJobPrivate::scanFileWithGet() +{ + Q_ASSERT(m_mimeTypeName.isEmpty()); + + // First, let's check for well-known extensions + // Not over HTTP and not when there is a query in the URL, in any case. + if (!m_url.hasQuery() && !m_url.scheme().startsWith(QLatin1String("http"))) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForUrl(m_url); + if (!mime.isDefault()) { + //qDebug() << "Scanfile: MIME TYPE is " << mime.name(); + m_mimeTypeName = mime.name(); + runUrlWithMimeType(); + return; + } + } + + // No mimetype found, and the URL is not local (or fast mode not allowed). + // We need to apply the 'KIO' method, i.e. either asking the server or + // getting some data out of the file, to know what mimetype it is. + + if (!KProtocolManager::supportsReading(m_url)) { + qCWarning(KIO_GUI) << "#### NO SUPPORT FOR READING!"; + q->setError(KIO::ERR_CANNOT_READ); + q->setErrorText(m_url.toDisplayString()); + q->emitResult(); + return; + } + //qDebug() << this << "Scanning file" << url; + + KIO::TransferJob *job = KIO::get(m_url, KIO::NoReload /*reload*/, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + QObject::connect(job, &KJob::result, + q, [=]() { + const int errCode = job->error(); + if (errCode) { + // ERR_NO_CONTENT is not an error, but an indication no further + // actions needs to be taken. + if (errCode != KIO::ERR_NO_CONTENT) { + q->setError(errCode); + q->setErrorText(job->errorText()); + } + q->emitResult(); + } + // if the job succeeded, we certainly hope it emitted mimetype()... + }); + QObject::connect(job, QOverload::of(&KIO::TransferJob::mimetype), + q, [=](KIO::Job *, const QString &mimetype) { + if (m_followRedirections) { // Update our URL in case of a redirection + m_url = job->url(); + } + if (mimetype.isEmpty()) { + qCWarning(KIO_GUI) << "get() didn't emit a mimetype! Probably a kioslave bug, please check the implementation of" << m_url.scheme(); + } + m_mimeTypeName = mimetype; + + // If the current mime-type is the default mime-type, then attempt to + // determine the "real" mimetype from the file name (bug #279675) + QMimeType mime = fixupMimeType(m_mimeTypeName, m_suggestedFileName.isEmpty() ? m_url.fileName() : m_suggestedFileName); + if (mime.isValid() && mime.name() != m_mimeTypeName) { + m_mimeTypeName = mime.name(); + } + + if (m_suggestedFileName.isEmpty()) { + m_suggestedFileName = job->queryMetaData(QStringLiteral("content-disposition-filename")); + } + + job->putOnHold(); + KIO::Scheduler::publishSlaveOnHold(); + runUrlWithMimeType(); + }); +} + +void KIO::OpenUrlJobPrivate::runLink(const QString &filePath, const QString &urlStr, const QString &optionalServiceName) +{ + if (urlStr.isEmpty()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("The desktop entry file\n%1\nis of type Link but has no URL=... entry.", filePath)); + q->emitResult(); + return; + } + + m_url = QUrl::fromUserInput(urlStr); + m_mimeTypeName.clear(); + + // X-KDE-LastOpenedWith holds the service desktop entry name that + // should be preferred for opening this URL if possible. + // This is used by the Recent Documents menu for instance. + if (!optionalServiceName.isEmpty()) { + m_preferredService = KService::serviceByDesktopName(optionalServiceName); + } + + // Restart from scratch with the target of the link + q->start(); +} + +void KIO::OpenUrlJobPrivate::emitAccessDenied() +{ + q->setError(KIO::ERR_ACCESS_DENIED); + q->setErrorText(KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, m_url.toDisplayString())); + q->emitResult(); +} + +// was: KRun::isExecutable. Feel free to make public if needed. +static bool isExecutableMime(const QMimeType &mimeType) +{ + return (mimeType.inherits(QLatin1String("application/x-desktop")) || + mimeType.inherits(QLatin1String("application/x-executable")) || + /* See https://bugs.freedesktop.org/show_bug.cgi?id=97226 */ + mimeType.inherits(QLatin1String("application/x-sharedlib")) || + mimeType.inherits(QLatin1String("application/x-ms-dos-executable")) || + mimeType.inherits(QLatin1String("application/x-shellscript"))); +} + +// Helper function that returns whether a file has the execute bit set or not. +static bool hasExecuteBit(const QString &fileName) +{ + QFileInfo file(fileName); + return file.isExecutable(); +} + +namespace KIO { +extern KIO::UntrustedProgramHandlerInterface *defaultUntrustedProgramHandler(); +} + +// Return true if handled in any way (success or error) +// Return false if the caller should proceed +bool KIO::OpenUrlJobPrivate::handleExecutables(const QMimeType &mimeType) +{ + if (!KAuthorized::authorize(QStringLiteral("shell_access"))) { + emitAccessDenied(); + return true; // handled + } + + // Check whether file is executable script +#ifdef Q_OS_WIN + const bool isNativeBinary = !mimeType.inherits(QStringLiteral("text/plain")); +#else + const bool isNativeBinary = !mimeType.inherits(QStringLiteral("text/plain")) && !mimeType.inherits(QStringLiteral("application/x-ms-dos-executable")); +#endif + + if (!m_url.isLocalFile() || !m_runExecutables) { + if (isNativeBinary) { + // Show warning for executables that aren't scripts + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("The file \"%1\" is an executable program. " + "For safety it will not be started.", m_url.toDisplayString())); + q->emitResult(); + return true; // handled + } + // Let scripts be open as text files, if remote, or no exec allowed + return false; + } + + const QString localPath = m_url.toLocalFile(); + + // For executables that aren't scripts and without execute bit, + // show prompt asking user if he wants to run the program. + if (!hasExecuteBit(localPath)) { + if (!isNativeBinary) { + // Don't try to run scripts/exes without execute bit, instead + // open them with default application + return false; + } + KIO::UntrustedProgramHandlerInterface *untrustedProgramHandler = defaultUntrustedProgramHandler(); + if (!untrustedProgramHandler) { + // No way to ask the user to make it executable + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("The program \"%1\" needs to have executable permission before it can be launched.", localPath)); + q->emitResult(); + return true; + } + QObject::connect(untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, q, [=](bool result) { + if (result) { + QString errorString; + if (untrustedProgramHandler->setExecuteBit(localPath, errorString)) { + executeCommand(); + } else { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to make file \"%1\" executable.\n%2.", localPath, errorString)); + q->emitResult(); + } + } else { + q->setError(KIO::ERR_USER_CANCELED); + q->emitResult(); + } + }); + untrustedProgramHandler->showUntrustedProgramWarning(q, m_url.fileName()); + return true; + } + + // Local executable with execute bit, proceed + executeCommand(); + return true; // handled +} + +void KIO::OpenUrlJobPrivate::executeCommand() +{ + // Execute the URL as a command. This is how we start scripts and executables + KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(m_url.toLocalFile()); + job->setUiDelegate(q->uiDelegate()); + job->setStartupId(m_startupId); + job->setWorkingDirectory(m_url.adjusted(QUrl::RemoveFilename).toLocalFile()); + q->addSubjob(job); + job->start(); + + // TODO implement deleting the file if tempFile==true + // CommandLauncherJob doesn't support that, unlike ApplicationLauncherJob + // We'd have to do it in KProcessRunner. +} + +void KIO::OpenUrlJobPrivate::runUrlWithMimeType() +{ + // Tell the app, in case it wants us to stop here + Q_EMIT q->mimeTypeFound(m_mimeTypeName); + if (q->error() == KJob::KilledJobError) { + q->emitResult(); + return; + } + + // Support for preferred service setting, see setPreferredService + if (m_preferredService && m_preferredService->hasMimeType(m_mimeTypeName)) { + startService(m_preferredService); + return; + } + + // Local desktop file + if (m_url.isLocalFile() && m_mimeTypeName == QLatin1String("application/x-desktop")) { + if (m_url.fileName() == QLatin1String(".directory")) { + // We cannot execute a .directory file. Open with a text editor instead. + m_mimeTypeName = QStringLiteral("text/plain"); + } else { + const QString filePath = m_url.toLocalFile(); + KDesktopFile cfg(filePath); + KConfigGroup cfgGroup = cfg.desktopGroup(); + if (!cfgGroup.hasKey("Type")) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("The desktop entry file %1 has no Type=... entry.", filePath)); + q->emitResult(); + return; + } + if ((cfg.hasApplicationType() + || cfg.readType() == QLatin1String("Service"))// for kio_settings + && !cfgGroup.readEntry("Exec").isEmpty() + && m_runExecutables) { + KService::Ptr service(new KService(filePath)); + startService(service); + return; + } else if (cfg.hasLinkType()) { + runLink(filePath, cfg.readUrl(), cfg.desktopGroup().readEntry("X-KDE-LastOpenedWith")); + return; + } + } + } + + // Scripts and executables + QMimeDatabase db; + QMimeType mimeType = db.mimeTypeForName(m_mimeTypeName); + if (isExecutableMime(mimeType)) { + if (handleExecutables(mimeType)) { + return; + } + } + + // General case: look up associated application + KService::Ptr service = KApplicationTrader::preferredService(m_mimeTypeName); + if (service) { + startService(service); + } else { + showOpenWithDialog(); + } +} + +void KIO::OpenUrlJobPrivate::showOpenWithDialog() +{ + if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("You are not authorized to select an application to open this file.")); + q->emitResult(); + return; + } + + if (!s_openUrlJobHandler || QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Windows) { + // As KDE on windows doesn't know about the windows default applications offers will be empty in nearly all cases. + // So we use QDesktopServices::openUrl to let windows decide how to open the file. + // It's also our fallback if there's no handler to show an open-with dialog. + if (!QDesktopServices::openUrl(m_url)) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Failed to open the file.")); + } + q->emitResult(); + return; + } + + QObject::connect(s_openUrlJobHandler, &KIO::OpenUrlJobHandlerInterface::canceled, q, [this]() { + q->setError(KIO::ERR_USER_CANCELED); + q->emitResult(); + }); + + QObject::connect(s_openUrlJobHandler, &KIO::OpenUrlJobHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) { + startService(service); + }); + + s_openUrlJobHandler->promptUserForApplication(q, m_url, m_mimeTypeName); +} + +void KIO::OpenUrlJob::slotResult(KJob *job) +{ + // This is only used for the final application/launcher job, so we're done when it's done + const int errCode = job->error(); + if (errCode) { + setError(errCode); + setErrorText(KIO::buildErrorString(errCode, job->errorText())); + } + emitResult(); +} diff --git a/src/gui/openurljob.h b/src/gui/openurljob.h new file mode 100644 index 00000000..79174584 --- /dev/null +++ b/src/gui/openurljob.h @@ -0,0 +1,140 @@ +/* + This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KIO_OPENURLJOB_H +#define KIO_OPENURLJOB_H + +#include "kiogui_export.h" +#include "applicationlauncherjob.h" +#include +#include + +class QUrl; + +namespace KIO { + +class OpenUrlJobPrivate; + +/** + * @class OpenUrlJob openurljob.h + * + * @brief OpenUrlJob finds out the right way to "open" a URL. + * This includes finding out its mimetype, and then the associated application, + * or running desktop files, executables, etc. + * It also honours the "use this webbrowser for all http(s) URLs" setting. + * @since 5.71 + */ +class KIOGUI_EXPORT OpenUrlJob : public KCompositeJob +{ + Q_OBJECT +public: + /** + * @brief Creates a OpenUrlJob in order to open a URL. + * @param url the URL of the file/directory to open + */ + explicit OpenUrlJob(const QUrl &url, QObject *parent = nullptr); + + /** + * @brief Creates a OpenUrlJob for the case where the mimeType is already known + * @param url the URL of the file/directory to open + * @param mimeType the type of file/directory. See QMimeType. + */ + explicit OpenUrlJob(const QUrl &url, const QString &mimeType, QObject *parent = nullptr); + + /** + * Destructor + * + * Note that jobs auto-delete themselves after emitting result. + */ + ~OpenUrlJob() override; + + /** + * Specifies that the URL passed to the application will be deleted when it exits (if the URL is a local file) + */ + void setDeleteTemporaryFile(bool b); + + /** + * Sets the file name to use in the case of downloading the file to a tempfile + * in order to give to a non-URL-aware application. + * Some apps rely on the extension to determine the mimetype of the file. + * Usually the file name comes from the URL, but in the case of the + * HTTP Content-Disposition header, we need to override the file name. + * @param suggestedFileName the file name + */ + void setSuggestedFileName(const QString &suggestedFileName); + + /** + * Sets the startup notification id of the application launch. + * @param startupId startup notification id, if any (otherwise ""). + */ + void setStartupId(const QByteArray &startupId); + + /** + * Set this to true if this class should allow the user to run executables. + * Unlike KF5's KRun, this setting is OFF by default here for security reasons. + * File managers can enable this, but e.g. web browsers, mail clients etc. shouldn't. + */ + void setRunExecutables(bool allow); + + /** + * Sets whether the external webbrowser setting should be honoured. + * This is enabled by default. + * This should only be disabled in webbrowser applications. + * @param b whether to enable the external browser or not. + */ + void setEnableExternalBrowser(bool b); + + /** + * Sets whether the job should follow URL redirections. + * This is enabled by default. + * @param b whether to follow redirections or not. + */ + void setFollowRedirections(bool b); + + /** + * Starts the job. + * You must call this, after having done all the setters. + * This is a GUI job, never use exec(), it would block user interaction. + */ + void start() override; + +Q_SIGNALS: + /** + * Emitted when the mimeType was determined. + * This can be used for special cases like webbrowsers + * who want to embed the URL in some cases, rather than starting a different + * application. In that case they can kill the job. + */ + void mimeTypeFound(const QString &mimeType); + +protected: + bool doKill() override; + +private: + void slotResult(KJob *job) override; + + friend class OpenUrlJobPrivate; + QScopedPointer d; +}; + +} // namespace KIO + +#endif // OPENURLJOB_H diff --git a/src/gui/openurljobhandlerinterface.cpp b/src/gui/openurljobhandlerinterface.cpp new file mode 100644 index 00000000..0d715520 --- /dev/null +++ b/src/gui/openurljobhandlerinterface.cpp @@ -0,0 +1,41 @@ +/* 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 "openurljobhandlerinterface.h" + +#include +#include +#include "kiocoredebug.h" + +using namespace KIO; + +class KIO::OpenUrlJobHandlerInterfacePrivate {}; + +OpenUrlJobHandlerInterface::OpenUrlJobHandlerInterface() = default; + +OpenUrlJobHandlerInterface::~OpenUrlJobHandlerInterface() = default; + +void OpenUrlJobHandlerInterface::promptUserForApplication(OpenUrlJob *job, const QUrl &url, const QString &mimeType) +{ + Q_UNUSED(job) + Q_UNUSED(url) + Q_UNUSED(mimeType) + Q_EMIT canceled(); +} diff --git a/src/gui/openurljobhandlerinterface.h b/src/gui/openurljobhandlerinterface.h new file mode 100644 index 00000000..78a2e8b2 --- /dev/null +++ b/src/gui/openurljobhandlerinterface.h @@ -0,0 +1,91 @@ +/* This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef OPENURLJOBHANDLERINTERFACE_H +#define OPENURLJOBHANDLERINTERFACE_H + +#include +#include +#include +class QString; + +namespace KIO { +class OpenUrlJob; +class OpenUrlJobHandlerInterfacePrivate; + +/** + * @class OpenUrlJobHandlerInterface openurljobhandlerinterface.h + * @brief The OpenUrlJobHandlerInterface class allows OpenUrlJob to + * prompt the user about which application to use to open URLs that do not + * have an associated application (the "Open With" dialog). + * + * This extension mechanism for jobs is similar to KIO::JobUiDelegateExtension + * and UntrustedProgramHandlerInterface. + * + * @since 5.71 + */ +class KIOGUI_EXPORT OpenUrlJobHandlerInterface : public QObject +{ + Q_OBJECT +protected: + /** + * Constructor + */ + OpenUrlJobHandlerInterface(); + + /** + * Destructor + */ + ~OpenUrlJobHandlerInterface() override; + +public: + /** + * Show the "Open With" dialog. + * @param job the job calling this. Useful to get all is properties + * @param url the URL to open + * @param mimeType the mimeType of the URL + * + * Implementations of this method must emit either serviceSelected or canceled. + * + * The default implementation in this base class simply emits cancelled(). + * Any application using KIO::JobUiDelegate (KIOWidgets) will benefit from an + * automatically registered subclass which implements this method using KOpenWithDialog. + */ + virtual void promptUserForApplication(KIO::OpenUrlJob *job, const QUrl &url, const QString &mimeType); + +Q_SIGNALS: + /** + * Emitted by promptUserForApplication() once the user chose an application. + * @param service the application chosen by the user + */ + void serviceSelected(const KService::Ptr &service); + + /** + * Emitted by promptUserForApplication() if the user canceled the application selection dialog. + */ + void canceled(); + +private: + QScopedPointer d; +}; + +} + +#endif // OPENURLJOBHANDLERINTERFACE_H diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 5d28cc94..672ea19e 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -1,225 +1,226 @@ project(KIOWidgets) #include (ConfigureChecks.cmake) find_package(ACL) set(HAVE_LIBACL ${ACL_FOUND}) set(HAVE_POSIX_ACL ${ACL_FOUND}) set_package_properties(ACL PROPERTIES DESCRIPTION "LibACL" URL "ftp://oss.sgi.com/projects/xfs/cmd_tars" TYPE RECOMMENDED PURPOSE "Support for manipulating access control lists") configure_file(config-kiowidgets.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kiowidgets.h) set(kiowidgets_SRCS accessmanager.cpp accessmanagerreply_p.cpp fileundomanager.cpp kacleditwidget.cpp kpropertiesdialog.cpp kurlrequesterdialog.cpp kurlcombobox.cpp kfileitemactions.cpp delegateanimationhandler.cpp imagefilter.cpp kfileitemdelegate.cpp kdesktopfileactions.cpp kopenwithdialog.cpp kfile.cpp pastedialog.cpp paste.cpp clipboardupdater.cpp kabstractfileitemactionplugin.cpp koverlayiconplugin.cpp kbuildsycocaprogressdialog.cpp kurlrequester.cpp krun.cpp sslui.cpp kurlpixmapprovider.cpp pixmaploader.cpp thumbsequencecreator.cpp thumbcreator.cpp kshellcompletion.cpp kurlcompletion.cpp kurifilter.cpp dropjob.cpp openfilemanagerwindowjob.cpp pastejob.cpp previewjob.cpp renamedialog.cpp ksslcertificatebox.cpp kdynamicjobtracker.cpp ksslinfodialog.cpp joburlcache.cpp skipdialog.cpp jobuidelegate.cpp kdirlister.cpp kdirmodel.cpp executablefileopendialog.cpp dndpopupmenuplugin.cpp kurifiltersearchprovideractions.cpp renamefiledialog.cpp widgetsuntrustedprogramhandler.cpp + widgetsopenurljobhandler.cpp ) if (WIN32) list(APPEND kiowidgets_SRCS krun_win.cpp ) else() list(APPEND kiowidgets_SRCS kautomount.cpp ) endif() ecm_qt_declare_logging_category(kiowidgets_SRCS HEADER kio_widgets_debug.h IDENTIFIER KIO_WIDGETS CATEGORY_NAME kf5.kio.widgets DESCRIPTION "KIOWidgets (KIO)" EXPORT KIO ) ecm_qt_export_logging_category( IDENTIFIER category CATEGORY_NAME kf5.kio.kdirmodel DESCRIPTION "KDirModel (KIO)" EXPORT KIO ) qt5_add_dbus_adaptor(kiowidgets_SRCS org.kde.kio.FileUndoManager.xml fileundomanager_p.h KIO::FileUndoManagerPrivate fileundomanager_adaptor KIOFileUndoManagerAdaptor) qt5_add_dbus_interface(kiowidgets_SRCS org.kde.kuiserver.xml kuiserver_interface) ki18n_wrap_ui(kiowidgets_SRCS checksumswidget.ui certificateparty.ui sslinfo.ui kpropertiesdesktopadvbase.ui kpropertiesdesktopbase.ui ) add_library(KF5KIOWidgets ${kiowidgets_SRCS}) add_library(KF5::KIOWidgets ALIAS KF5KIOWidgets) ecm_generate_export_header(KF5KIOWidgets BASE_NAME KIOWidgets GROUP_BASE_NAME KF VERSION ${KF5_VERSION} DEPRECATED_BASE_VERSION 0 DEPRECATION_VERSIONS 4.0 4.1 4.3 4.4 4.5 4.6 4.7 5.0 5.4 5.6 5.25 5.31 5.32 5.64 5.66 ) # TODO: add support for EXCLUDE_DEPRECATED_BEFORE_AND_AT to all KIO libs # needs fixing of undeprecated API being still implemented using own deprecated API target_include_directories(KF5KIOWidgets INTERFACE "$") target_link_libraries(KF5KIOWidgets PUBLIC KF5::KIOGui KF5::KIOCore KF5::JobWidgets KF5::Service Qt5::Network # SSL KF5::Completion # KUrlCompletion uses KCompletion KF5::WidgetsAddons # keditlistwidget PRIVATE Qt5::Concurrent Qt5::DBus KF5::I18n KF5::IconThemes # KIconLoader KF5::WindowSystem # KStartupInfo KF5::ConfigWidgets # KColorScheme ) if(ACL_FOUND) target_link_libraries(KF5KIOWidgets PRIVATE ${ACL_LIBS}) endif() set_target_properties(KF5KIOWidgets PROPERTIES VERSION ${KIO_VERSION_STRING} SOVERSION ${KIO_SOVERSION} EXPORT_NAME KIOWidgets ) # Headers not prefixed with KIO/ ecm_generate_headers(KIOWidgets_HEADERS HEADER_NAMES KPropertiesDialog KUrlRequesterDialog KUrlComboBox KFileItemActions KFileItemDelegate KAutoMount KDesktopFileActions KOpenWithDialog KAbstractFileItemActionPlugin KOverlayIconPlugin KBuildSycocaProgressDialog KFile KUrlRequester KRun KUrlPixmapProvider KSslCertificateBox KSslInfoDialog KDirLister KDirModel KShellCompletion KUrlCompletion KUriFilter REQUIRED_HEADERS KIOWidgets_HEADERS ) # Headers prefixed with KIO/ ecm_generate_headers(KIOWidgets_CamelCase_HEADERS HEADER_NAMES AccessManager SslUi ThumbSequenceCreator ThumbCreator DropJob DndPopupMenuPlugin OpenFileManagerWindowJob PasteJob PreviewJob RenameDialog SkipDialog JobUiDelegate FileUndoManager Paste PixmapLoader KUriFilterSearchProviderActions # KF6: fix and move to non-KIO prefixed install folder RenameFileDialog PREFIX KIO REQUIRED_HEADERS KIO_namespaced_widgets_HEADERS ) install(FILES ${KIOWidgets_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOWidgets/KIO COMPONENT Devel) install(TARGETS KF5KIOWidgets EXPORT KF5KIOTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES org.kde.kio.FileUndoManager.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} RENAME kf5_org.kde.kio.FileUndoManager.xml) install(FILES ${KIO_namespaced_widgets_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOWidgets/kio COMPONENT Devel) install(FILES ${KIOWidgets_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/kiowidgets_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOWidgets COMPONENT Devel) install(FILES kfileitemactionplugin.desktop kpropertiesdialogplugin.desktop kurifilterplugin.desktop konqpopupmenuplugin.desktop kiodndpopupmenuplugin.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR} ) # make available to ecm_add_qch in parent folder set(KIOWidgets_QCH_SOURCES ${KIOWidgets_HEADERS} ${KIO_namespaced_widgets_HEADERS} PARENT_SCOPE) include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KIOWidgets LIB_NAME KF5KIOWidgets DEPS "KIOGui KIOCore KBookmarks KXmlGui Solid" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOWidgets) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/widgets/jobuidelegate.cpp b/src/widgets/jobuidelegate.cpp index d0f0302e..269b7bd3 100644 --- a/src/widgets/jobuidelegate.cpp +++ b/src/widgets/jobuidelegate.cpp @@ -1,449 +1,453 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow David Faure Copyright (C) 2006 Kevin Ottens Copyright (C) 2013 Dawit Alemayehu 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 "jobuidelegate.h" #include #include "kio_widgets_debug.h" #include "kiogui_export.h" #include "widgetsuntrustedprogramhandler.h" +#include "widgetsopenurljobhandler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kio/scheduler.h" class Q_DECL_HIDDEN KIO::JobUiDelegate::Private { public: }; namespace KIO { KIOGUI_EXPORT void setDefaultUntrustedProgramHandler(KIO::UntrustedProgramHandlerInterface *iface); +KIOGUI_EXPORT void setDefaultOpenUrlJobHandler(KIO::OpenUrlJobHandlerInterface *iface); } KIO::JobUiDelegate::JobUiDelegate() : d(new Private()) { // KF6 TODO: remove, inherit from WidgetsUntrustedProgramHandler instead static WidgetsUntrustedProgramHandler s_handler; KIO::setDefaultUntrustedProgramHandler(&s_handler); + static WidgetsOpenUrlJobHandler s_openUrlHandler; + KIO::setDefaultOpenUrlJobHandler(&s_openUrlHandler); } KIO::JobUiDelegate::~JobUiDelegate() { delete d; } /* Returns the top most window associated with widget. Unlike QWidget::window(), this function does its best to find and return the main application window associated with the given widget. If widget itself is a dialog or its parent is a dialog, and that dialog has a parent widget then this function will iterate through all those widgets to find the top most window, which most of the time is the main window of the application. By contrast, QWidget::window() would simply return the first file dialog it encountered since it is the "next ancestor widget that has (or could have) a window-system frame". */ static QWidget *topLevelWindow(QWidget *widget) { QWidget *w = widget; while (w && w->parentWidget()) { w = w->parentWidget(); } return (w ? w->window() : nullptr); } class JobUiDelegateStatic : public QObject { Q_OBJECT public: void registerWindow(QWidget *wid) { if (!wid) { return; } QWidget *window = topLevelWindow(wid); QObject *obj = static_cast(window); if (!m_windowList.contains(obj)) { // We must store the window Id because by the time // the destroyed signal is emitted we can no longer // access QWidget::winId() (already destructed) WId windowId = window->winId(); m_windowList.insert(obj, windowId); connect(window, &QObject::destroyed, this, &JobUiDelegateStatic::slotUnregisterWindow); QDBusInterface(QStringLiteral("org.kde.kded5"), QStringLiteral("/kded"), QStringLiteral("org.kde.kded5")). call(QDBus::NoBlock, QStringLiteral("registerWindowId"), qlonglong(windowId)); } } public Q_SLOTS: void slotUnregisterWindow(QObject *obj) { if (!obj) { return; } QMap::Iterator it = m_windowList.find(obj); if (it == m_windowList.end()) { return; } WId windowId = it.value(); disconnect(it.key(), &QObject::destroyed, this, &JobUiDelegateStatic::slotUnregisterWindow); m_windowList.erase(it); QDBusInterface(QStringLiteral("org.kde.kded5"), QStringLiteral("/kded"), QStringLiteral("org.kde.kded5")). call(QDBus::NoBlock, QStringLiteral("unregisterWindowId"), qlonglong(windowId)); } private: QMap m_windowList; }; Q_GLOBAL_STATIC(JobUiDelegateStatic, s_static) KIO::JobUiDelegate::JobUiDelegate(KJobUiDelegate::Flags flags, QWidget *window) : KDialogJobUiDelegate(flags, window), d(new Private()) { s_static()->registerWindow(window); } void KIO::JobUiDelegate::setWindow(QWidget *window) { KDialogJobUiDelegate::setWindow(window); s_static()->registerWindow(window); } void KIO::JobUiDelegate::unregisterWindow(QWidget *window) { s_static()->slotUnregisterWindow(window); } KIO::RenameDialog_Result KIO::JobUiDelegate::askFileRename(KJob *job, const QString &caption, const QUrl &src, const QUrl &dest, KIO::RenameDialog_Options options, QString &newDest, KIO::filesize_t sizeSrc, KIO::filesize_t sizeDest, const QDateTime &ctimeSrc, const QDateTime &ctimeDest, const QDateTime &mtimeSrc, const QDateTime &mtimeDest) { //qDebug() << "job=" << job; // We now do it in process, so that opening the rename dialog // doesn't start uiserver for nothing if progressId=0 (e.g. F2 in konq) KIO::RenameDialog dlg(KJobWidgets::window(job), caption, src, dest, options, sizeSrc, sizeDest, ctimeSrc, ctimeDest, mtimeSrc, mtimeDest); dlg.setWindowModality(Qt::WindowModal); connect(job, &KJob::finished, &dlg, &QDialog::reject); // #192976 KIO::RenameDialog_Result res = static_cast(dlg.exec()); if (res == R_AUTO_RENAME) { newDest = dlg.autoDestUrl().path(); } else { newDest = dlg.newDestUrl().path(); } return res; } KIO::SkipDialog_Result KIO::JobUiDelegate::askSkip(KJob *job, KIO::SkipDialog_Options options, const QString &error_text) { KIO::SkipDialog dlg(KJobWidgets::window(job), options, error_text); dlg.setWindowModality(Qt::WindowModal); connect(job, &KJob::finished, &dlg, &QDialog::reject); // #192976 return static_cast(dlg.exec()); } bool KIO::JobUiDelegate::askDeleteConfirmation(const QList &urls, DeletionType deletionType, ConfirmationType confirmationType) { QString keyName; bool ask = (confirmationType == ForceConfirmation); if (!ask) { KSharedConfigPtr kioConfig = KSharedConfig::openConfig(QStringLiteral("kiorc"), KConfig::NoGlobals); // The default value for confirmations is true for delete and false // for trash. If you change this, please also update: // dolphin/src/settings/general/confirmationssettingspage.cpp bool defaultValue = true; switch (deletionType) { case Delete: keyName = QStringLiteral("ConfirmDelete"); break; case Trash: keyName = QStringLiteral("ConfirmTrash"); defaultValue = false; break; case EmptyTrash: keyName = QStringLiteral("ConfirmEmptyTrash"); break; } ask = kioConfig->group("Confirmations").readEntry(keyName, defaultValue); } if (ask) { QStringList prettyList; prettyList.reserve(urls.size()); for (const QUrl &url : urls) { if (url.scheme() == QLatin1String("trash")) { QString path = url.path(); // HACK (#98983): remove "0-foo". Note that it works better than // displaying KFileItem::name(), for files under a subdir. path.remove(QRegularExpression(QStringLiteral("^/[0-9]*-"))); prettyList.append(path); } else { prettyList.append(url.toDisplayString(QUrl::PreferLocalFile)); } } int result; QWidget *widget = window(); const KMessageBox::Options options(KMessageBox::Notify | KMessageBox::WindowModal); switch (deletionType) { case Delete: if (prettyList.count() == 1) { result = KMessageBox::warningContinueCancel( widget, xi18nc("@info", "Do you really want to permanently delete this item?%1This action cannot be undone.", prettyList.first()), i18n("Delete Permanently"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), keyName, options); } else { result = KMessageBox::warningContinueCancelList( widget, xi18ncp("@info", "Do you really want to permanently delete this item?This action cannot be undone.", "Do you really want to permanently delete these %1 items?This action cannot be undone.", prettyList.count()), prettyList, i18n("Delete Permanently"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), keyName, options); } break; case EmptyTrash: result = KMessageBox::warningContinueCancel( widget, xi18nc("@info", "Do you want to permanently delete all items from the Trash?This action cannot be undone."), i18n("Delete Permanently"), KGuiItem(i18nc("@action:button", "Empty Trash"), QIcon::fromTheme(QStringLiteral("user-trash"))), KStandardGuiItem::cancel(), keyName, options); break; case Trash: default: if (prettyList.count() == 1) { result = KMessageBox::warningContinueCancel( widget, xi18nc("@info", "Do you really want to move this item to the Trash?%1", prettyList.first()), i18n("Move to Trash"), KGuiItem(i18n("Move to Trash"), QStringLiteral("user-trash")), KStandardGuiItem::cancel(), keyName, options); } else { result = KMessageBox::warningContinueCancelList( widget, i18np("Do you really want to move this item to the Trash?", "Do you really want to move these %1 items to the Trash?", prettyList.count()), prettyList, i18n("Move to Trash"), KGuiItem(i18n("Move to Trash"), QStringLiteral("user-trash")), KStandardGuiItem::cancel(), keyName, options); } } if (!keyName.isEmpty()) { // Check kmessagebox setting... erase & copy to konquerorrc. KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup notificationGroup(config, "Notification Messages"); if (!notificationGroup.readEntry(keyName, true)) { notificationGroup.writeEntry(keyName, true); notificationGroup.sync(); KSharedConfigPtr kioConfig = KSharedConfig::openConfig(QStringLiteral("kiorc"), KConfig::NoGlobals); kioConfig->group("Confirmations").writeEntry(keyName, false); } } return (result == KMessageBox::Continue); } return true; } int KIO::JobUiDelegate::requestMessageBox(KIO::JobUiDelegate::MessageBoxType type, const QString &text, const QString &caption, const QString &buttonYes, const QString &buttonNo, const QString &iconYes, const QString &iconNo, const QString &dontAskAgainName, const KIO::MetaData &metaData) { int result = -1; //qDebug() << type << text << "caption=" << caption; KConfig config(QStringLiteral("kioslaverc")); KMessageBox::setDontShowAgainConfig(&config); const KGuiItem buttonYesGui(buttonYes, iconYes); const KGuiItem buttonNoGui(buttonNo, iconNo); KMessageBox::Options options(KMessageBox::Notify | KMessageBox::WindowModal); switch (type) { case QuestionYesNo: result = KMessageBox::questionYesNo( window(), text, caption, buttonYesGui, buttonNoGui, dontAskAgainName, options); break; case WarningYesNo: result = KMessageBox::warningYesNo( window(), text, caption, buttonYesGui, buttonNoGui, dontAskAgainName, options | KMessageBox::Dangerous); break; case WarningYesNoCancel: result = KMessageBox::warningYesNoCancel( window(), text, caption, buttonYesGui, buttonNoGui, KStandardGuiItem::cancel(), dontAskAgainName, options); break; case WarningContinueCancel: result = KMessageBox::warningContinueCancel( window(), text, caption, buttonYesGui, KStandardGuiItem::cancel(), dontAskAgainName, options); break; case Information: KMessageBox::information(window(), text, caption, dontAskAgainName, options); result = 1; // whatever break; case SSLMessageBox: { QPointer kid(new KSslInfoDialog(window())); //### this is boilerplate code and appears in khtml_part.cpp almost unchanged! #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const QStringList sl = metaData.value(QStringLiteral("ssl_peer_chain")).split(QLatin1Char('\x01'), QString::SkipEmptyParts); #else const QStringList sl = metaData.value(QStringLiteral("ssl_peer_chain")).split(QLatin1Char('\x01'), Qt::SkipEmptyParts); #endif QList certChain; bool decodedOk = true; for (const QString &s : sl) { certChain.append(QSslCertificate(s.toLatin1())); //or is it toLocal8Bit or whatever? if (certChain.last().isNull()) { decodedOk = false; break; } } if (decodedOk) { result = 1; // whatever kid->setSslInfo(certChain, metaData.value(QStringLiteral("ssl_peer_ip")), text, // the URL metaData.value(QStringLiteral("ssl_protocol_version")), metaData.value(QStringLiteral("ssl_cipher")), metaData.value(QStringLiteral("ssl_cipher_used_bits")).toInt(), metaData.value(QStringLiteral("ssl_cipher_bits")).toInt(), KSslInfoDialog::certificateErrorsFromString(metaData.value(QStringLiteral("ssl_cert_errors")))); kid->exec(); } else { result = -1; KMessageBox::information(window(), i18n("The peer SSL certificate chain appears to be corrupt."), i18n("SSL"), QString(), options); } // KSslInfoDialog deletes itself (Qt::WA_DeleteOnClose). delete kid; break; } case WarningContinueCancelDetailed: { const QString details = metaData.value(QStringLiteral("privilege_conf_details")); result = KMessageBox::warningContinueCancelDetailed( window(), text, caption, KStandardGuiItem::cont(), KStandardGuiItem::cancel(), dontAskAgainName, options | KMessageBox::Dangerous, details); break; } default: qCWarning(KIO_WIDGETS) << "Unknown type" << type; result = 0; break; } KMessageBox::setDontShowAgainConfig(nullptr); return result; } KIO::ClipboardUpdater *KIO::JobUiDelegate::createClipboardUpdater(Job *job, ClipboardUpdaterMode mode) { if (qobject_cast(qApp)) { return new KIO::ClipboardUpdater(job, mode); } return nullptr; } void KIO::JobUiDelegate::updateUrlInClipboard(const QUrl &src, const QUrl &dest) { if (qobject_cast(qApp)) { KIO::ClipboardUpdater::update(src, dest); } } class KIOWidgetJobUiDelegateFactory : public KIO::JobUiDelegateFactory { public: KJobUiDelegate *createDelegate() const override { return new KIO::JobUiDelegate; } }; Q_GLOBAL_STATIC(KIOWidgetJobUiDelegateFactory, globalUiDelegateFactory) Q_GLOBAL_STATIC(KIO::JobUiDelegate, globalUiDelegate) // Simply linking to this library, creates a GUI job delegate and delegate extension for all KIO jobs static void registerJobUiDelegate() { KIO::setDefaultJobUiDelegateFactory(globalUiDelegateFactory()); KIO::setDefaultJobUiDelegateExtension(globalUiDelegate()); } Q_CONSTRUCTOR_FUNCTION(registerJobUiDelegate) #include "jobuidelegate.moc" diff --git a/src/widgets/kdesktopfileactions.cpp b/src/widgets/kdesktopfileactions.cpp index 7e38aa7c..0d0e7f1d 100644 --- a/src/widgets/kdesktopfileactions.cpp +++ b/src/widgets/kdesktopfileactions.cpp @@ -1,323 +1,324 @@ /* This file is part of the KDE libraries * Copyright (C) 1999 Waldo Bastian * David Faure * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation; * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. **/ #include "kdesktopfileactions.h" #include "../core/config-kmountpoint.h" // for HAVE_VOLMGT (yes I cheat a bit) #include "kio_widgets_debug.h" #include #include "krun.h" #include "kautomount.h" #include #include #include #include #include #include #include #include #include #include enum BuiltinServiceType { ST_MOUNT = 0x0E1B05B0, ST_UNMOUNT = 0x0E1B05B1 }; // random numbers static bool runFSDevice(const QUrl &_url, const KDesktopFile &cfg, const QByteArray &asn); static bool runLink(const QUrl &_url, const KDesktopFile &cfg, const QByteArray &asn); - +// TODO KF6 remove, use OpenUrlJob instead bool KDesktopFileActions::run(const QUrl &u, bool _is_local) { return runWithStartup(u, _is_local, QByteArray()); } +// TODO KF6 remove, use OpenUrlJob instead bool KDesktopFileActions::runWithStartup(const QUrl &u, bool _is_local, const QByteArray &asn) { // It might be a security problem to run external untrusted desktop // entry files if (!_is_local) { return false; } if (u.fileName() == QLatin1String(".directory")) { // We cannot execute a .directory file. Open with a text editor instead. return KRun::runUrl(u, QStringLiteral("text/plain"), nullptr, KRun::RunFlags(), QString(), asn); } KDesktopFile cfg(u.toLocalFile()); if (!cfg.desktopGroup().hasKey("Type")) { QString tmp = i18n("The desktop entry file %1 " "has no Type=... entry.", u.toLocalFile()); KMessageBox::error(nullptr, tmp); return false; } //qDebug() << "TYPE = " << type.data(); if (cfg.hasDeviceType()) { return runFSDevice(u, cfg, asn); } else if (cfg.hasApplicationType() || (cfg.readType() == QLatin1String("Service") && !cfg.desktopGroup().readEntry("Exec").isEmpty())) { // for kio_settings KService service(u.toLocalFile()); return KRun::runApplication(service, QList(), nullptr /*TODO - window*/, KRun::RunFlags{}, QString(), asn); } else if (cfg.hasLinkType()) { return runLink(u, cfg, asn); } QString tmp = i18n("The desktop entry of type\n%1\nis unknown.", cfg.readType()); KMessageBox::error(nullptr, tmp); return false; } static bool runFSDevice(const QUrl &_url, const KDesktopFile &cfg, const QByteArray &asn) { bool retval = false; QString dev = cfg.readDevice(); if (dev.isEmpty()) { QString tmp = i18n("The desktop entry file\n%1\nis of type FSDevice but has no Dev=... entry.", _url.toLocalFile()); KMessageBox::error(nullptr, tmp); return retval; } KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByDevice(dev); // Is the device already mounted ? if (mp) { const QUrl mpURL = QUrl::fromLocalFile(mp->mountPoint()); // Open a new window retval = KRun::runUrl(mpURL, QStringLiteral("inode/directory"), nullptr /*TODO - window*/, KRun::RunFlags(KRun::RunExecutables), QString(), asn); } else { KConfigGroup cg = cfg.desktopGroup(); bool ro = cg.readEntry("ReadOnly", false); QString fstype = cg.readEntry("FSType"); if (fstype == QLatin1String("Default")) { // KDE-1 thing fstype.clear(); } QString point = cg.readEntry("MountPoint"); #ifndef Q_OS_WIN (void) new KAutoMount(ro, fstype.toLatin1(), dev, point, _url.toLocalFile()); #endif retval = false; } return retval; } static bool runLink(const QUrl &_url, const KDesktopFile &cfg, const QByteArray &asn) { QString u = cfg.readUrl(); if (u.isEmpty()) { QString tmp = i18n("The desktop entry file\n%1\nis of type Link but has no URL=... entry.", _url.toString()); KMessageBox::error(nullptr, tmp); return false; } QUrl url = QUrl::fromUserInput(u); KRun *run = new KRun(url, (QWidget *)nullptr, true, asn); // X-KDE-LastOpenedWith holds the service desktop entry name that - // was should be preferred for opening this URL if possible. + // should be preferred for opening this URL if possible. // This is used by the Recent Documents menu for instance. QString lastOpenedWidth = cfg.desktopGroup().readEntry("X-KDE-LastOpenedWith"); if (!lastOpenedWidth.isEmpty()) { run->setPreferredService(lastOpenedWidth); } return false; } QList KDesktopFileActions::builtinServices(const QUrl &_url) { QList result; if (!_url.isLocalFile()) { return result; } bool offerMount = false; bool offerUnmount = false; KDesktopFile cfg(_url.toLocalFile()); if (cfg.hasDeviceType()) { // url to desktop file const QString dev = cfg.readDevice(); if (dev.isEmpty()) { QString tmp = i18n("The desktop entry file\n%1\nis of type FSDevice but has no Dev=... entry.", _url.toLocalFile()); KMessageBox::error(nullptr, tmp); return result; } KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByDevice(dev); if (mp) { offerUnmount = true; } else { offerMount = true; } } if (offerMount) { KServiceAction mount(QStringLiteral("mount"), i18n("Mount"), QString(), QString(), false); mount.setData(QVariant(ST_MOUNT)); result.append(mount); } if (offerUnmount) { QString text; #ifdef HAVE_VOLMGT /* * Solaris' volume management can only umount+eject */ text = i18n("Eject"); #else text = i18n("Unmount"); #endif KServiceAction unmount(QStringLiteral("unmount"), text, QString(), QString(), false); unmount.setData(QVariant(ST_UNMOUNT)); result.append(unmount); } return result; } QList KDesktopFileActions::userDefinedServices(const QString &path, bool bLocalFiles) { KDesktopFile cfg(path); return userDefinedServices(path, cfg, bLocalFiles); } QList KDesktopFileActions::userDefinedServices(const QString &path, const KDesktopFile &cfg, bool bLocalFiles, const QList &file_list) { Q_UNUSED(path); // this was just for debugging; we use service.entryPath() now. KService service(&cfg); return userDefinedServices(service, bLocalFiles, file_list); } QList KDesktopFileActions::userDefinedServices(const KService &service, bool bLocalFiles, const QList &file_list) { QList result; if (!service.isValid()) { // e.g. TryExec failed return result; } QStringList keys; const QString actionMenu = service.property(QStringLiteral("X-KDE-GetActionMenu"), QVariant::String).toString(); if (!actionMenu.isEmpty()) { const QStringList dbuscall = actionMenu.split(QLatin1Char(' ')); if (dbuscall.count() >= 4) { const QString &app = dbuscall.at(0); const QString &object = dbuscall.at(1); const QString &interface = dbuscall.at(2); const QString &function = dbuscall.at(3); QDBusInterface remote(app, object, interface); // Do NOT use QDBus::BlockWithGui here. It runs a nested event loop, // in which timers can fire, leading to crashes like #149736. QDBusReply reply = remote.call(function, QUrl::toStringList(file_list)); keys = reply; // ensures that the reply was a QStringList if (keys.isEmpty()) { return result; } } else { qCWarning(KIO_WIDGETS) << "The desktop file" << service.entryPath() << "has an invalid X-KDE-GetActionMenu entry." << "Syntax is: app object interface function"; } } // Now, either keys is empty (all actions) or it's set to the actions we want const QList list = service.actions(); for (const KServiceAction &action : list) { if (keys.isEmpty() || keys.contains(action.name())) { const QString exec = action.exec(); if (bLocalFiles || exec.contains(QLatin1String("%U")) || exec.contains(QLatin1String("%u"))) { result.append(action); } } } return result; } // KF6 TODO add QWiget* parameter void KDesktopFileActions::executeService(const QList &urls, const KServiceAction &action) { //qDebug() << "EXECUTING Service " << action.name(); int actionData = action.data().toInt(); if (actionData == ST_MOUNT || actionData == ST_UNMOUNT) { Q_ASSERT(urls.count() == 1); const QString path = urls.first().toLocalFile(); //qDebug() << "MOUNT&UNMOUNT"; KDesktopFile cfg(path); if (cfg.hasDeviceType()) { // path to desktop file const QString dev = cfg.readDevice(); if (dev.isEmpty()) { QString tmp = i18n("The desktop entry file\n%1\nis of type FSDevice but has no Dev=... entry.", path); KMessageBox::error(nullptr /*TODO window*/, tmp); return; } KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByDevice(dev); if (actionData == ST_MOUNT) { // Already mounted? Strange, but who knows ... if (mp) { //qDebug() << "ALREADY Mounted"; return; } const KConfigGroup group = cfg.desktopGroup(); bool ro = group.readEntry("ReadOnly", false); QString fstype = group.readEntry("FSType"); if (fstype == QLatin1String("Default")) { // KDE-1 thing fstype.clear(); } QString point = group.readEntry("MountPoint"); #ifndef Q_OS_WIN (void)new KAutoMount(ro, fstype.toLatin1(), dev, point, path, false); #endif } else if (actionData == ST_UNMOUNT) { // Not mounted? Strange, but who knows ... if (!mp) { return; } #ifndef Q_OS_WIN (void)new KAutoUnmount(mp->mountPoint(), path); #endif } } } else { KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(action); job->setUrls(urls); QObject::connect(job, &KJob::result, qApp, [urls]() { // The action may update the desktop file. Example: eject unmounts (#5129). org::kde::KDirNotify::emitFilesChanged(urls); }); job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr /*TODO window*/)); job->start(); } } diff --git a/src/widgets/kopenwithdialog.cpp b/src/widgets/kopenwithdialog.cpp index 176c6e26..b2115e70 100644 --- a/src/widgets/kopenwithdialog.cpp +++ b/src/widgets/kopenwithdialog.cpp @@ -1,1178 +1,1193 @@ /* This file is part of the KDE libraries Copyright (C) 1997 Torben Weis Copyright (C) 1999 Dirk Mueller Portions copyright (C) 1999 Preston Brown Copyright (C) 2007 Pino Toscano 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 "kopenwithdialog.h" #include "kopenwithdialog_p.h" #include "kio_widgets_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include inline void writeEntry(KConfigGroup &group, const char *key, const KCompletion::CompletionMode &aValue, KConfigBase::WriteConfigFlags flags = KConfigBase::Normal) { group.writeEntry(key, int(aValue), flags); } namespace KDEPrivate { class AppNode { public: AppNode() : isDir(false), parent(nullptr), fetched(false) { } ~AppNode() { qDeleteAll(children); } AppNode(const AppNode &) = delete; AppNode &operator=(const AppNode &) = delete; QString icon; QString text; QString tooltip; QString entryPath; QString exec; bool isDir; AppNode *parent; bool fetched; QList children; }; static bool AppNodeLessThan(KDEPrivate::AppNode *n1, KDEPrivate::AppNode *n2) { if (n1->isDir) { if (n2->isDir) { return n1->text.compare(n2->text, Qt::CaseInsensitive) < 0; } else { return true; } } else { if (n2->isDir) { return false; } else { return n1->text.compare(n2->text, Qt::CaseInsensitive) < 0; } } } } class KApplicationModelPrivate { public: explicit KApplicationModelPrivate(KApplicationModel *qq) : q(qq), root(new KDEPrivate::AppNode()) { } ~KApplicationModelPrivate() { delete root; } void fillNode(const QString &entryPath, KDEPrivate::AppNode *node); KApplicationModel * const q; KDEPrivate::AppNode *root; }; void KApplicationModelPrivate::fillNode(const QString &_entryPath, KDEPrivate::AppNode *node) { KServiceGroup::Ptr root = KServiceGroup::group(_entryPath); if (!root || !root->isValid()) { return; } const KServiceGroup::List list = root->entries(); for (const KSycocaEntry::Ptr &p : list) { QString icon; QString text; QString tooltip; QString entryPath; QString exec; bool isDir = false; if (p->isType(KST_KService)) { const KService::Ptr service(static_cast(p.data())); if (service->noDisplay()) { continue; } icon = service->icon(); text = service->name(); // no point adding a tooltip that only repeats service->name() const QString generic = service->genericName(); tooltip = generic != text ? generic : QString(); exec = service->exec(); entryPath = service->entryPath(); } else if (p->isType(KST_KServiceGroup)) { const KServiceGroup::Ptr serviceGroup(static_cast(p.data())); if (serviceGroup->noDisplay() || serviceGroup->childCount() == 0) { continue; } icon = serviceGroup->icon(); text = serviceGroup->caption(); entryPath = serviceGroup->entryPath(); isDir = true; } else { qCWarning(KIO_WIDGETS) << "KServiceGroup: Unexpected object in list!"; continue; } KDEPrivate::AppNode *newnode = new KDEPrivate::AppNode(); newnode->icon = icon; newnode->text = text; newnode->tooltip = tooltip; newnode->entryPath = entryPath; newnode->exec = exec; newnode->isDir = isDir; newnode->parent = node; node->children.append(newnode); } std::stable_sort(node->children.begin(), node->children.end(), KDEPrivate::AppNodeLessThan); } KApplicationModel::KApplicationModel(QObject *parent) : QAbstractItemModel(parent), d(new KApplicationModelPrivate(this)) { d->fillNode(QString(), d->root); const int nRows = rowCount(); for (int i = 0; i < nRows; i++) { fetchAll(index(i, 0)); } } KApplicationModel::~KApplicationModel() { delete d; } bool KApplicationModel::canFetchMore(const QModelIndex &parent) const { if (!parent.isValid()) { return false; } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); return node->isDir && !node->fetched; } int KApplicationModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return 1; } QVariant KApplicationModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: return node->text; case Qt::DecorationRole: if (!node->icon.isEmpty()) { return QIcon::fromTheme(node->icon); } break; case Qt::ToolTipRole: if (!node->tooltip.isEmpty()) { return node->tooltip; } break; default: ; } return QVariant(); } void KApplicationModel::fetchMore(const QModelIndex &parent) { if (!parent.isValid()) { return; } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); if (!node->isDir) { return; } emit layoutAboutToBeChanged(); d->fillNode(node->entryPath, node); node->fetched = true; emit layoutChanged(); } void KApplicationModel::fetchAll(const QModelIndex &parent) { if (!parent.isValid() || !canFetchMore(parent)) { return; } fetchMore(parent); int childCount = rowCount(parent); for (int i = 0; i < childCount; i++) { const QModelIndex &child = index(i, 0, parent); // Recursively call the function for each child node. fetchAll(child); } } bool KApplicationModel::hasChildren(const QModelIndex &parent) const { if (!parent.isValid()) { return true; } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); return node->isDir; } QVariant KApplicationModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || section != 0) { return QVariant(); } switch (role) { case Qt::DisplayRole: return i18n("Known Applications"); default: return QVariant(); } } QModelIndex KApplicationModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0) { return QModelIndex(); } KDEPrivate::AppNode *node = d->root; if (parent.isValid()) { node = static_cast(parent.internalPointer()); } if (row >= node->children.count()) { return QModelIndex(); } else { return createIndex(row, 0, node->children.at(row)); } } QModelIndex KApplicationModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); if (node->parent->parent) { int id = node->parent->parent->children.indexOf(node->parent); if (id >= 0 && id < node->parent->parent->children.count()) { return createIndex(id, 0, node->parent); } else { return QModelIndex(); } } else { return QModelIndex(); } } int KApplicationModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return d->root->children.count(); } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); return node->children.count(); } QString KApplicationModel::entryPathFor(const QModelIndex &index) const { if (!index.isValid()) { return QString(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); return node->entryPath; } QString KApplicationModel::execFor(const QModelIndex &index) const { if (!index.isValid()) { return QString(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); return node->exec; } bool KApplicationModel::isDirectory(const QModelIndex &index) const { if (!index.isValid()) { return false; } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); return node->isDir; } QTreeViewProxyFilter::QTreeViewProxyFilter(QObject *parent) : QSortFilterProxyModel(parent) { } bool QTreeViewProxyFilter::filterAcceptsRow(int sourceRow, const QModelIndex &parent) const { QModelIndex index = sourceModel()->index(sourceRow, 0, parent); if (!index.isValid()) { return false; } // Match the regexp only on leaf nodes if (!sourceModel()->hasChildren(index) && index.data().toString().contains(filterRegExp())) { return true; } return false; } class KApplicationViewPrivate { public: KApplicationViewPrivate() : appModel(nullptr), m_proxyModel(nullptr) { } KApplicationModel *appModel; QSortFilterProxyModel *m_proxyModel; }; KApplicationView::KApplicationView(QWidget *parent) : QTreeView(parent), d(new KApplicationViewPrivate) { setHeaderHidden(true); } KApplicationView::~KApplicationView() { delete d; } void KApplicationView::setModels(KApplicationModel *model, QSortFilterProxyModel *proxyModel) { if (d->appModel) { disconnect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &KApplicationView::slotSelectionChanged); } QTreeView::setModel(proxyModel); // Here we set the proxy model d->m_proxyModel = proxyModel; // Also store it in a member property to avoid many casts later d->appModel = model; if (d->appModel) { connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &KApplicationView::slotSelectionChanged); } } QSortFilterProxyModel* KApplicationView::proxyModel() { return d->m_proxyModel; } bool KApplicationView::isDirSel() const { if (d->appModel) { QModelIndex index = selectionModel()->currentIndex(); index = d->m_proxyModel->mapToSource(index); return d->appModel->isDirectory(index); } return false; } void KApplicationView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { QTreeView::currentChanged(current, previous); if (d->appModel) { QModelIndex sourceCurrent = d->m_proxyModel->mapToSource(current); if(!d->appModel->isDirectory(sourceCurrent)) { QString exec = d->appModel->execFor(sourceCurrent); if (!exec.isEmpty()) { emit highlighted(d->appModel->entryPathFor(sourceCurrent), exec); } } } } void KApplicationView::slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { Q_UNUSED(deselected) QItemSelection sourceSelected = d->m_proxyModel->mapSelectionToSource(selected); const QModelIndexList indexes = sourceSelected.indexes(); if (indexes.count() == 1) { QString exec = d->appModel->execFor(indexes.at(0)); emit this->selected(d->appModel->entryPathFor(indexes.at(0)), exec); } } /*************************************************************** * * KOpenWithDialog * ***************************************************************/ class KOpenWithDialogPrivate { public: explicit KOpenWithDialogPrivate(KOpenWithDialog *qq) : q(qq), saveNewApps(false) { } KOpenWithDialog * const q; /** * Determine mime type from URLs */ - void setMimeType(const QList &_urls); + void setMimeTypeFromUrls(const QList &_urls); + + void setMimeType(const QString &mimeType); void addToMimeAppsList(const QString &serviceId); /** * Create a dialog that asks for a application to open a given * URL(s) with. * * @param text appears as a label on top of the entry box. * @param value is the initial value of the line */ void init(const QString &text, const QString &value); /** * Called by checkAccept() in order to save the history of the combobox */ void saveComboboxHistory(); /** * Process the choices made by the user, and return true if everything is OK. * Called by KOpenWithDialog::accept(), i.e. when clicking on OK or typing Return. */ bool checkAccept(); // slots void _k_slotDbClick(); void _k_slotFileSelected(); bool saveNewApps; bool m_terminaldirty; KService::Ptr curService; KApplicationView *view; KUrlRequester *edit; QString m_command; QLabel *label; QString qMimeType; QString qMimeTypeComment; KCollapsibleGroupBox *dialogExtension; QCheckBox *terminal; QCheckBox *remember; QCheckBox *nocloseonexit; KService::Ptr m_pService; QDialogButtonBox *buttonBox; }; KOpenWithDialog::KOpenWithDialog(const QList &_urls, QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); setWindowTitle(i18n("Open With")); QString text; if (_urls.count() == 1) { text = i18n("Select the program that should be used to open %1. " "If the program is not listed, enter the name or click " "the browse button.", _urls.first().fileName().toHtmlEscaped()); } else // Should never happen ?? { text = i18n("Choose the name of the program with which to open the selected files."); } - d->setMimeType(_urls); + d->setMimeTypeFromUrls(_urls); d->init(text, QString()); } KOpenWithDialog::KOpenWithDialog(const QList &_urls, const QString &_text, const QString &_value, QWidget *parent) + : KOpenWithDialog(_urls, QString(), _text, _value, parent) +{ +} + +KOpenWithDialog::KOpenWithDialog(const QList &_urls, const QString &mimeType, + const QString &_text, const QString &_value, + QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); QString text = _text; if (text.isEmpty() && !_urls.isEmpty()) { if (_urls.count() == 1) { const QString fileName = KStringHandler::csqueeze(_urls.first().fileName()); text = i18n("Select the program you want to use to open the file
%1
", fileName.toHtmlEscaped()); } else { text = i18np("Select the program you want to use to open the file.", "Select the program you want to use to open the %1 files.", _urls.count()); } } setWindowTitle(i18n("Choose Application")); - d->setMimeType(_urls); + if (mimeType.isEmpty()) { + d->setMimeTypeFromUrls(_urls); + } else { + d->setMimeType(mimeType); + } d->init(text, _value); } KOpenWithDialog::KOpenWithDialog(const QString &mimeType, const QString &value, QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); setWindowTitle(i18n("Choose Application for %1", mimeType)); QString text = i18n("Select the program for the file type: %1. " "If the program is not listed, enter the name or click " "the browse button.", mimeType); - d->qMimeType = mimeType; - QMimeDatabase db; - d->qMimeTypeComment = db.mimeTypeForName(mimeType).comment(); + d->setMimeType(mimeType); d->init(text, value); - if (d->remember) { - d->remember->hide(); - } } KOpenWithDialog::KOpenWithDialog(QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); setWindowTitle(i18n("Choose Application")); QString text = i18n("Select a program. " "If the program is not listed, enter the name or click " "the browse button."); d->qMimeType.clear(); d->init(text, QString()); } -void KOpenWithDialogPrivate::setMimeType(const QList &_urls) +void KOpenWithDialogPrivate::setMimeTypeFromUrls(const QList &_urls) { if (_urls.count() == 1) { QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(_urls.first()); qMimeType = mime.name(); if (mime.isDefault()) { qMimeType.clear(); } else { qMimeTypeComment = mime.comment(); } } else { qMimeType.clear(); } } +void KOpenWithDialogPrivate::setMimeType(const QString &mimeType) +{ + qMimeType = mimeType; + QMimeDatabase db; + qMimeTypeComment = db.mimeTypeForName(mimeType).comment(); +} + void KOpenWithDialogPrivate::init(const QString &_text, const QString &_value) { bool bReadOnly = !KAuthorized::authorize(QStringLiteral("shell_access")); m_terminaldirty = false; view = nullptr; m_pService = nullptr; curService = nullptr; QBoxLayout *topLayout = new QVBoxLayout; q->setLayout(topLayout); label = new QLabel(_text, q); label->setWordWrap(true); topLayout->addWidget(label); if (!bReadOnly) { // init the history combo and insert it into the URL-Requester KHistoryComboBox *combo = new KHistoryComboBox(); combo->setToolTip(i18n("Type to filter the applications below, or specify the name of a command.\nPress down arrow to navigate the results.")); KLineEdit *lineEdit = new KLineEdit(q); lineEdit->setClearButtonEnabled(true); combo->setLineEdit(lineEdit); combo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); combo->setDuplicatesEnabled(false); KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("Open-with settings")); int max = cg.readEntry("Maximum history", 15); combo->setMaxCount(max); int mode = cg.readEntry("CompletionMode", int(KCompletion::CompletionNone)); combo->setCompletionMode(static_cast(mode)); const QStringList list = cg.readEntry("History", QStringList()); combo->setHistoryItems(list, true); edit = new KUrlRequester(combo, q); edit->installEventFilter(q); } else { edit = new KUrlRequester(q); edit->lineEdit()->setReadOnly(true); edit->button()->hide(); } edit->setText(_value); edit->setWhatsThis(i18n( "Following the command, you can have several place holders which will be replaced " "with the actual values when the actual program is run:\n" "%f - a single file name\n" "%F - a list of files; use for applications that can open several local files at once\n" "%u - a single URL\n" "%U - a list of URLs\n" "%d - the directory of the file to open\n" "%D - a list of directories\n" "%i - the icon\n" "%m - the mini-icon\n" "%c - the comment")); topLayout->addWidget(edit); if (edit->comboBox()) { KUrlCompletion *comp = new KUrlCompletion(KUrlCompletion::ExeCompletion); edit->comboBox()->setCompletionObject(comp); edit->comboBox()->setAutoDeleteCompletionObject(true); } QObject::connect(edit, &KUrlRequester::textChanged, q, &KOpenWithDialog::slotTextChanged); QObject::connect(edit, SIGNAL(urlSelected(QUrl)), q, SLOT(_k_slotFileSelected())); QTreeViewProxyFilter *proxyModel = new QTreeViewProxyFilter(view); KApplicationModel *appModel = new KApplicationModel(proxyModel); proxyModel->setSourceModel(appModel); proxyModel->setFilterKeyColumn(0); proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); proxyModel->setRecursiveFilteringEnabled(true); view = new KApplicationView(q); view->setModels(appModel, proxyModel); topLayout->addWidget(view); topLayout->setStretchFactor(view, 1); QObject::connect(view, &KApplicationView::selected, q, &KOpenWithDialog::slotSelected); QObject::connect(view, &KApplicationView::highlighted, q, &KOpenWithDialog::slotHighlighted); QObject::connect(view, SIGNAL(doubleClicked(QModelIndex)), q, SLOT(_k_slotDbClick())); if (!qMimeType.isNull()) { remember = new QCheckBox(i18n("&Remember application association for all files of type\n\"%1\" (%2)", qMimeTypeComment, qMimeType)); // remember->setChecked(true); topLayout->addWidget(remember); } else { remember = nullptr; } //Advanced options dialogExtension = new KCollapsibleGroupBox(q); dialogExtension->setTitle(i18n("Terminal options")); QVBoxLayout *dialogExtensionLayout = new QVBoxLayout; dialogExtensionLayout->setContentsMargins(0, 0, 0, 0); terminal = new QCheckBox(i18n("Run in &terminal"), q); if (bReadOnly) { terminal->hide(); } QObject::connect(terminal, &QAbstractButton::toggled, q, &KOpenWithDialog::slotTerminalToggled); dialogExtensionLayout->addWidget(terminal); QStyleOptionButton checkBoxOption; checkBoxOption.initFrom(terminal); int checkBoxIndentation = terminal->style()->pixelMetric(QStyle::PM_IndicatorWidth, &checkBoxOption, terminal); checkBoxIndentation += terminal->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing, &checkBoxOption, terminal); QBoxLayout *nocloseonexitLayout = new QHBoxLayout(); nocloseonexitLayout->setContentsMargins(0, 0, 0, 0); QSpacerItem *spacer = new QSpacerItem(checkBoxIndentation, 0, QSizePolicy::Fixed, QSizePolicy::Minimum); nocloseonexitLayout->addItem(spacer); nocloseonexit = new QCheckBox(i18n("&Do not close when command exits"), q); nocloseonexit->setChecked(false); nocloseonexit->setDisabled(true); // check to see if we use konsole if not disable the nocloseonexit // because we don't know how to do this on other terminal applications KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General")); QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); if (bReadOnly || preferredTerminal != QLatin1String("konsole")) { nocloseonexit->hide(); } nocloseonexitLayout->addWidget(nocloseonexit); dialogExtensionLayout->addLayout(nocloseonexitLayout); dialogExtension->setLayout(dialogExtensionLayout); topLayout->addWidget(dialogExtension); buttonBox = new QDialogButtonBox(q); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); q->connect(buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept); q->connect(buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); topLayout->addWidget(buttonBox); q->setMinimumSize(q->minimumSizeHint()); //edit->setText( _value ); // The resize is what caused "can't click on items before clicking on Name header" in previous versions. // Probably due to the resizeEvent handler using width(). #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) q->resize( q->minimumWidth(), 0.6 * QApplication::screens().at(0)->availableGeometry().height()); #else q->resize( q->minimumWidth(), 0.6 * q->screen()->availableGeometry().height()); #endif edit->setFocus(); q->slotTextChanged(); } // ---------------------------------------------------------------------- KOpenWithDialog::~KOpenWithDialog() { delete d; } // ---------------------------------------------------------------------- void KOpenWithDialog::slotSelected(const QString & /*_name*/, const QString &_exec) { d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!_exec.isEmpty()); } // ---------------------------------------------------------------------- void KOpenWithDialog::slotHighlighted(const QString &entryPath, const QString &) { d->curService = KService::serviceByDesktopPath(entryPath); if (d->curService && !d->m_terminaldirty) { // ### indicate that default value was restored d->terminal->setChecked(d->curService->terminal()); QString terminalOptions = d->curService->terminalOptions(); d->nocloseonexit->setChecked((terminalOptions.contains(QLatin1String("--noclose")))); d->m_terminaldirty = false; // slotTerminalToggled changed it } } // ---------------------------------------------------------------------- void KOpenWithDialog::slotTextChanged() { // Forget about the service only when the selection is empty // otherwise changing text but hitting the same result clears curService bool selectionEmpty = !d->view->currentIndex().isValid(); if (d->curService && selectionEmpty) { d->curService = nullptr; } d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!d->edit->text().isEmpty() || d->curService); //Update the filter regexp with the new text in the lineedit d->view->proxyModel()->setFilterFixedString(d->edit->text()); //Expand all the nodes when the search string is 3 characters long //If the search string doesn't match anything there will be no nodes to expand if (d->edit->text().size() > 2) { d->view->expandAll(); QAbstractItemModel *model = d->view->model(); if (model->rowCount() == 1) { // Automatically select the result (first leaf node) if the // filter has only one match QModelIndex leafNodeIdx = model->index(0, 0); while (model->hasChildren(leafNodeIdx)) { leafNodeIdx = model->index(0, 0, leafNodeIdx); } d->view->setCurrentIndex(leafNodeIdx); } } else { d->view->collapseAll(); d->view->setCurrentIndex(d->view->rootIndex()); // Unset and deselect all the elements d->curService = nullptr; } } // ---------------------------------------------------------------------- void KOpenWithDialog::slotTerminalToggled(bool) { // ### indicate that default value was overridden d->m_terminaldirty = true; d->nocloseonexit->setDisabled(!d->terminal->isChecked()); } // ---------------------------------------------------------------------- void KOpenWithDialogPrivate::_k_slotDbClick() { // check if a directory is selected if (view->isDirSel()) { return; } q->accept(); } void KOpenWithDialogPrivate::_k_slotFileSelected() { // quote the path to avoid unescaped whitespace, backslashes, etc. edit->setText(KShell::quoteArg(edit->text())); } void KOpenWithDialog::setSaveNewApplications(bool b) { d->saveNewApps = b; } static QString simplifiedExecLineFromService(const QString &fullExec) { QString exec = fullExec; exec.remove(QStringLiteral("%u"), Qt::CaseInsensitive); exec.remove(QStringLiteral("%f"), Qt::CaseInsensitive); exec.remove(QStringLiteral("-caption %c")); exec.remove(QStringLiteral("-caption \"%c\"")); exec.remove(QStringLiteral("%i")); exec.remove(QStringLiteral("%m")); return exec.simplified(); } void KOpenWithDialogPrivate::addToMimeAppsList(const QString &serviceId /*menu id or storage id*/) { KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); // Save the default application according to mime-apps-spec 1.0 KConfigGroup defaultApp(profile, "Default Applications"); defaultApp.writeXdgListEntry(qMimeType, QStringList(serviceId)); KConfigGroup addedApps(profile, "Added Associations"); QStringList apps = addedApps.readXdgListEntry(qMimeType); apps.removeAll(serviceId); apps.prepend(serviceId); // make it the preferred app addedApps.writeXdgListEntry(qMimeType, apps); profile->sync(); // Also make sure the "auto embed" setting for this mimetype is off KSharedConfig::Ptr fileTypesConfig = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals); fileTypesConfig->group("EmbedSettings").writeEntry(QStringLiteral("embed-") + qMimeType, false); fileTypesConfig->sync(); // qDebug() << "rebuilding ksycoca..."; // kbuildsycoca is the one reading mimeapps.list, so we need to run it now KBuildSycocaProgressDialog::rebuildKSycoca(q); // could be nullptr if the user canceled the dialog... m_pService = KService::serviceByStorageId(serviceId); } bool KOpenWithDialogPrivate::checkAccept() { const QString typedExec(edit->text()); QString fullExec(typedExec); QString serviceName; QString initialServiceName; QString preferredTerminal; QString configPath; QString serviceExec; m_pService = curService; if (!m_pService) { // No service selected - check the command line // Find out the name of the service from the command line, removing args and paths serviceName = KIO::DesktopExecParser::executableName(typedExec); if (serviceName.isEmpty()) { KMessageBox::error(q, i18n("Could not extract executable name from '%1', please type a valid program name.", serviceName)); return false; } initialServiceName = serviceName; // Also remember the executableName with a path, if any, for the // check that the executable exists. // qDebug() << "initialServiceName=" << initialServiceName; int i = 1; // We have app, app-2, app-3... Looks better for the user. bool ok = false; // Check if there's already a service by that name, with the same Exec line do { // qDebug() << "looking for service" << serviceName; KService::Ptr serv = KService::serviceByDesktopName(serviceName); ok = !serv; // ok if no such service yet // also ok if we find the exact same service (well, "kwrite" == "kwrite %U") if (serv && !serv->noDisplay() /* #297720 */) { if (serv->isApplication()) { /*// qDebug() << "typedExec=" << typedExec << "serv->exec=" << serv->exec() << "simplifiedExecLineFromService=" << simplifiedExecLineFromService(fullExec);*/ serviceExec = simplifiedExecLineFromService(serv->exec()); if (typedExec == serviceExec) { ok = true; m_pService = serv; // qDebug() << "OK, found identical service: " << serv->entryPath(); } else { // qDebug() << "Exec line differs, service says:" << serviceExec; configPath = serv->entryPath(); serviceExec = serv->exec(); } } else { // qDebug() << "Found, but not an application:" << serv->entryPath(); } } if (!ok) { // service was found, but it was different -> keep looking ++i; serviceName = initialServiceName + QLatin1Char('-') + QString::number(i); } } while (!ok); } if (m_pService) { // Existing service selected serviceName = m_pService->name(); initialServiceName = serviceName; fullExec = m_pService->exec(); } else { const QString binaryName = KIO::DesktopExecParser::executablePath(typedExec); // qDebug() << "binaryName=" << binaryName; // Ensure that the typed binary name actually exists (#81190) if (QStandardPaths::findExecutable(binaryName).isEmpty()) { KMessageBox::error(q, i18n("'%1' not found, please type a valid program name.", binaryName)); return false; } } if (terminal->isChecked()) { KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General")); preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); m_command = preferredTerminal; // only add --noclose when we are sure it is konsole we're using if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { m_command += QStringLiteral(" --noclose"); } m_command += QLatin1String(" -e ") + edit->text(); // qDebug() << "Setting m_command to" << m_command; } if (m_pService && terminal->isChecked() != m_pService->terminal()) { m_pService = nullptr; // It's not exactly this service we're running } const bool bRemember = remember && remember->isChecked(); // qDebug() << "bRemember=" << bRemember << "service found=" << m_pService; if (m_pService) { if (bRemember) { // Associate this app with qMimeType in mimeapps.list Q_ASSERT(!qMimeType.isEmpty()); // we don't show the remember checkbox otherwise addToMimeAppsList(m_pService->storageId()); } } else { const bool createDesktopFile = bRemember || saveNewApps; if (!createDesktopFile) { // Create temp service if (configPath.isEmpty()) { m_pService = new KService(initialServiceName, fullExec, QString()); } else { if (!typedExec.contains(QLatin1String("%u"), Qt::CaseInsensitive) && !typedExec.contains(QLatin1String("%f"), Qt::CaseInsensitive)) { int index = serviceExec.indexOf(QLatin1String("%u"), 0, Qt::CaseInsensitive); if (index == -1) { index = serviceExec.indexOf(QLatin1String("%f"), 0, Qt::CaseInsensitive); } if (index > -1) { fullExec += QLatin1Char(' ') + serviceExec.midRef(index, 2); } } // qDebug() << "Creating service with Exec=" << fullExec; m_pService = new KService(configPath); m_pService->setExec(fullExec); } if (terminal->isChecked()) { m_pService->setTerminal(true); // only add --noclose when we are sure it is konsole we're using if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { m_pService->setTerminalOptions(QStringLiteral("--noclose")); } } } else { // If we got here, we can't seem to find a service for what they wanted. Create one. QString menuId; #ifdef Q_OS_WIN32 // on windows, do not use the complete path, but only the default name. serviceName = QFileInfo(serviceName).fileName(); #endif QString newPath = KService::newServicePath(false /* ignored argument */, serviceName, &menuId); // qDebug() << "Creating new service" << serviceName << "(" << newPath << ")" << "menuId=" << menuId; KDesktopFile desktopFile(newPath); KConfigGroup cg = desktopFile.desktopGroup(); cg.writeEntry("Type", "Application"); cg.writeEntry("Name", initialServiceName); cg.writeEntry("Exec", fullExec); cg.writeEntry("NoDisplay", true); // don't make it appear in the K menu if (terminal->isChecked()) { cg.writeEntry("Terminal", true); // only add --noclose when we are sure it is konsole we're using if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { cg.writeEntry("TerminalOptions", "--noclose"); } } if (!qMimeType.isEmpty()) { cg.writeXdgListEntry("MimeType", QStringList() << qMimeType); } cg.sync(); if (!qMimeType.isEmpty()) { addToMimeAppsList(menuId); } m_pService = new KService(newPath); } } saveComboboxHistory(); return true; } bool KOpenWithDialog::eventFilter(QObject *object, QEvent *event) { // Detect DownArrow to navigate the results in the QTreeView if (object == d->edit && event->type() == QEvent::ShortcutOverride) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Down) { KHistoryComboBox *combo = static_cast(d->edit->comboBox()); // FIXME: Disable arrow down in CompletionPopup and CompletionPopupAuto only when the dropdown list is shown. // When popup completion mode is used the down arrow is used to navigate the dropdown list of results if (combo->completionMode() != KCompletion::CompletionPopup && combo->completionMode() != KCompletion::CompletionPopupAuto) { QModelIndex leafNodeIdx = d->view->model()->index(0, 0); // Check if we have at least one result or the focus is passed to the empty QTreeView if (d->view->model()->hasChildren(leafNodeIdx)) { d->view->setFocus(Qt::OtherFocusReason); QApplication::sendEvent(d->view, keyEvent); return true; } } } } return QDialog::eventFilter(object, event); } void KOpenWithDialog::accept() { if (d->checkAccept()) { QDialog::accept(); } } QString KOpenWithDialog::text() const { if (!d->m_command.isEmpty()) { return d->m_command; } else { return d->edit->text(); } } void KOpenWithDialog::hideNoCloseOnExit() { // uncheck the checkbox because the value could be used when "Run in Terminal" is selected d->nocloseonexit->setChecked(false); d->nocloseonexit->hide(); d->dialogExtension->setVisible(d->nocloseonexit->isVisible() || d->terminal->isVisible()); } void KOpenWithDialog::hideRunInTerminal() { d->terminal->hide(); hideNoCloseOnExit(); } KService::Ptr KOpenWithDialog::service() const { return d->m_pService; } void KOpenWithDialogPrivate::saveComboboxHistory() { KHistoryComboBox *combo = static_cast(edit->comboBox()); if (combo) { combo->addToHistory(edit->text()); KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("Open-with settings")); cg.writeEntry("History", combo->historyItems()); writeEntry(cg, "CompletionMode", combo->completionMode()); // don't store the completion-list, as it contains all of KUrlCompletion's // executables cg.sync(); } } #include "moc_kopenwithdialog.cpp" #include "moc_kopenwithdialog_p.cpp" diff --git a/src/widgets/kopenwithdialog.h b/src/widgets/kopenwithdialog.h index 2c9b3880..997e52f8 100644 --- a/src/widgets/kopenwithdialog.h +++ b/src/widgets/kopenwithdialog.h @@ -1,148 +1,163 @@ /* This file is part of the KDE libraries Copyright (C) 2000 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. */ #ifndef OPENWITHDIALOG_H #define OPENWITHDIALOG_H #include "kiowidgets_export.h" #include #include #include class KOpenWithDialogPrivate; /** * @class KOpenWithDialog kopenwithdialog.h * * "Open With" dialog box. * * @note To let the user choose an application and run it immediately, * use simpler KRun::displayOpenWithDialog(). * * If the Kiosk "shell_access" action is not authorized (see * KAuthorized::authorize()), arbitrary commands are not allowed; instead, the * user must browse to and choose an executable. * * @author David Faure */ class KIOWIDGETS_EXPORT KOpenWithDialog : public QDialog { Q_OBJECT public: /** * Create a dialog that asks for a application to open a given * URL(s) with. * * @param urls the URLs that should be opened. The list can be empty, * if the dialog is used to choose an application but not for some particular URLs. * @param parent parent widget */ explicit KOpenWithDialog(const QList &urls, QWidget *parent = nullptr); /** * Create a dialog that asks for a application to open a given * URL(s) with. * * @param urls is the URL that should be opened * @param text appears as a label on top of the entry box. Leave empty for default text (since 5.20). * @param value is the initial value of the line * @param parent parent widget */ KOpenWithDialog(const QList &urls, const QString &text, const QString &value, QWidget *parent = nullptr); /** * Create a dialog to select a service for a given mimetype. * Note that this dialog doesn't apply to URLs. * * @param mimeType the mime type we want to choose an application for. * @param value is the initial value of the line * @param parent parent widget */ KOpenWithDialog(const QString &mimeType, const QString &value, QWidget *parent = nullptr); + /** + * Create a dialog that asks for a application for opening a given + * URL (or more than one), when we already know the mimetype of the URL(s). + * + * @param urls is the URLs that should be opened + * @param mimeType the MIME type of the URL + * @param text appears as a label on top of the entry box. + * @param value is the initial value of the line + * @param parent parent widget + * @since 5.71 + */ + KOpenWithDialog(const QList &urls, const QString &mimeType, + const QString &text, const QString &value, + QWidget *parent = nullptr); + /** * Create a dialog to select an application * Note that this dialog doesn't apply to URLs. * * @param parent parent widget */ KOpenWithDialog(QWidget *parent = nullptr); /** * Destructor */ ~KOpenWithDialog(); /** * @return the text the user entered */ QString text() const; /** * Hide the "Do not &close when command exits" Checkbox */ void hideNoCloseOnExit(); /** * Hide the "Run in &terminal" Checkbox */ void hideRunInTerminal(); /** * @return the chosen service in the application tree * Can be null, if the user typed some text and didn't select a service. */ KService::Ptr service() const; /** * Set whether a new .desktop file should be created if the user selects an * application for which no corresponding .desktop file can be found. * * Regardless of this setting a new .desktop file may still be created if * the user has chosen to remember the file association. * * The default is false: no .desktop files are created. */ void setSaveNewApplications(bool b); public Q_SLOTS: // TODO KDE5: move all those slots to the private class! void slotSelected(const QString &_name, const QString &_exec); void slotHighlighted(const QString &_name, const QString &_exec); void slotTextChanged(); void slotTerminalToggled(bool); protected Q_SLOTS: /** * Reimplemented from QDialog::accept() */ void accept() override; private: bool eventFilter(QObject *object, QEvent *event) override; friend class KOpenWithDialogPrivate; KOpenWithDialogPrivate *const d; Q_DISABLE_COPY(KOpenWithDialog) Q_PRIVATE_SLOT(d, void _k_slotDbClick()) Q_PRIVATE_SLOT(d, void _k_slotFileSelected()) }; #endif diff --git a/src/widgets/krun.cpp b/src/widgets/krun.cpp index 8a786acf..9a1919fd 100644 --- a/src/widgets/krun.cpp +++ b/src/widgets/krun.cpp @@ -1,1133 +1,1054 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Torben Weis Copyright (C) 2006 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 "krun.h" #include "krun_p.h" #include "kio_widgets_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kio/job.h" #include "kio/global.h" #include "kio/scheduler.h" #include "kopenwithdialog.h" #include "krecentdocument.h" #include "kdesktopfileactions.h" #include #include "kprocessrunner_p.h" // for KIOGuiPrivate::checkStartupNotify #include "applicationlauncherjob.h" #include "jobuidelegate.h" #include "widgetsuntrustedprogramhandler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include +#include KRunPrivate::KRunPrivate(KRun *parent) : q(parent), m_showingDialog(false) { } void KRunPrivate::startTimer() { m_timer->start(0); } // --------------------------------------------------------------------------- static KService::Ptr schemeService(const QString &protocol) { return KMimeTypeTrader::self()->preferredService(QLatin1String("x-scheme-handler/") + protocol); } static bool checkNeedPortalSupport() { return !QStandardPaths::locate(QStandardPaths::RuntimeLocation, QLatin1String("flatpak-info")).isEmpty() || qEnvironmentVariableIsSet("SNAP"); } qint64 KRunPrivate::runCommandLauncherJob(KIO::CommandLauncherJob *job, QWidget *widget) { QObject *receiver = widget ? static_cast(widget) : static_cast(qApp); QObject::connect(job, &KJob::result, receiver, [widget](KJob *job) { if (job->error()) { QEventLoopLocker locker; KMessageBox::sorry(widget, job->errorString()); } }); job->start(); job->waitForStarted(); return job->error() ? 0 : job->pid(); } // --------------------------------------------------------------------------- // Helper function that returns whether a file has the execute bit set or not. static bool hasExecuteBit(const QString &fileName) { QFileInfo file(fileName); return file.isExecutable(); } bool KRun::isExecutableFile(const QUrl &url, const QString &mimetype) { if (!url.isLocalFile()) { return false; } // While isExecutable performs similar check to this one, some users depend on // this method not returning true for application/x-desktop QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName(mimetype); if (!mimeType.inherits(QStringLiteral("application/x-executable")) && !mimeType.inherits(QStringLiteral("application/x-ms-dos-executable")) && !mimeType.inherits(QStringLiteral("application/x-executable-script")) && !mimeType.inherits(QStringLiteral("application/x-sharedlib"))) { return false; } if (!hasExecuteBit(url.toLocalFile()) && !mimeType.inherits(QStringLiteral("application/x-ms-dos-executable"))) { return false; } return true; } void KRun::handleInitError(int kioErrorCode, const QString &errorMsg) { Q_UNUSED(kioErrorCode); d->m_showingDialog = true; KMessageBox::error(d->m_window, errorMsg); d->m_showingDialog = false; } void KRun::handleError(KJob *job) { Q_ASSERT(job); if (job) { d->m_showingDialog = true; job->uiDelegate()->showErrorMessage(); d->m_showingDialog = false; } } bool KRun::runUrl(const QUrl &url, const QString &mimetype, QWidget *window, bool tempFile, bool runExecutables, const QString &suggestedFileName, const QByteArray &asn) { RunFlags flags = tempFile ? KRun::DeleteTemporaryFiles : RunFlags(); if (runExecutables) { flags |= KRun::RunExecutables; } return runUrl(url, mimetype, window, flags, suggestedFileName, asn); } // This is called by foundMimeType, since it knows the mimetype of the URL bool KRun::runUrl(const QUrl &u, const QString &_mimetype, QWidget *window, RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { - const QMimeDatabase db; const bool runExecutables = flags.testFlag(KRun::RunExecutables); const bool tempFile = flags.testFlag(KRun::DeleteTemporaryFiles); - bool noRun = false; - bool noAuth = false; - if (_mimetype == QLatin1String("application/x-desktop")) { - if (u.isLocalFile() && runExecutables) { - return KDesktopFileActions::runWithStartup(u, true, asn); - } - } else if (isExecutable(_mimetype)) { - // Check whether file is executable script - const QMimeType mime = db.mimeTypeForName(_mimetype); -#ifdef Q_OS_WIN - bool isNativeBinary = !mime.inherits(QStringLiteral("text/plain")); -#else - bool isNativeBinary = !mime.inherits(QStringLiteral("text/plain")) && !mime.inherits(QStringLiteral("application/x-ms-dos-executable")); -#endif - // Only run local files - if (u.isLocalFile() && runExecutables) { - if (KAuthorized::authorize(QStringLiteral("shell_access"))) { - - bool canRun = true; - bool isFileExecutable = hasExecuteBit(u.toLocalFile()); - - // For executables that aren't scripts and without execute bit, - // show prompt asking user if he wants to run the program. - if (!isFileExecutable && isNativeBinary) { - canRun = false; - KIO::WidgetsUntrustedProgramHandler handler; - if (handler.execUntrustedProgramWarning(window, u.fileName())) { - QString errorString; - if (!handler.setExecuteBit(u.toLocalFile(), errorString)) { - KMessageBox::sorry( - window, - i18n("Unable to make file %1 executable.\n%2.", - u.toLocalFile(), errorString) - ); - } else { - canRun = true; - } - } - } else if (!isFileExecutable && !isNativeBinary) { - // Don't try to run scripts/exes without execute bit, instead - // open them with default application - canRun = false; - } - - if (canRun) { - qDebug() << "Execute the URL as a command"; - return (KRun::runCommand(KShell::quoteArg(u.toLocalFile()), QString(), QString(), - window, asn, u.adjusted(QUrl::RemoveFilename).toLocalFile())); // just execute the url as a command - // ## TODO implement deleting the file if tempFile==true - } - - } else { - // Show no permission warning - noAuth = true; - } - } else if (isNativeBinary) { - // Show warning for executables that aren't scripts - noRun = true; - } - } - - if (noRun) { - KMessageBox::sorry(window, - i18n("The file %1 is an executable program. " - "For safety it will not be started.", u.toDisplayString().toHtmlEscaped())); - return false; - } - if (noAuth) { - KMessageBox::error(window, - i18n("You do not have permission to run %1.", u.toDisplayString().toHtmlEscaped())); - return false; - } - - QList lst; - lst.append(u); - KService::Ptr offer = KMimeTypeTrader::self()->preferredService(_mimetype); - - if (!offer) { -#ifdef Q_OS_WIN - // As KDE on windows doesn't know about the windows default applications offers will be empty in nearly all cases. - // So we use QDesktopServices::openUrl to let windows decide how to open the file - return QDesktopServices::openUrl(u); -#else - // Open-with dialog - // TODO : pass the mimetype as a parameter, to show it (comment field) in the dialog ! - // Hmm, in fact KOpenWithDialog::setServiceType already guesses the mimetype from the first URL of the list... - return displayOpenWithDialog(lst, window, tempFile, suggestedFileName, asn); -#endif - } - - return KRun::runApplication(*offer, lst, window, flags, suggestedFileName, asn); + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(u, _mimetype); + job->setSuggestedFileName(suggestedFileName); + job->setStartupId(asn); + job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); + job->setDeleteTemporaryFile(tempFile); + job->setRunExecutables(runExecutables); + job->start(); + return true; } bool KRun::displayOpenWithDialog(const QList &lst, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) { KMessageBox::sorry(window, i18n("You are not authorized to select an application to open this file.")); return false; } #ifdef Q_OS_WIN KConfigGroup cfgGroup(KSharedConfig::openConfig(), QStringLiteral("KOpenWithDialog Settings")); if (cfgGroup.readEntry("Native", true)) { return KRunPrivate::displayNativeOpenWithDialog(lst, window, tempFiles, suggestedFileName, asn); } #endif + + // TODO : pass the mimetype as a parameter, to show it (comment field) in the dialog ! + // Hmm, in fact KOpenWithDialog::setServiceType already guesses the mimetype from the first URL of the list... KOpenWithDialog dialog(lst, QString(), QString(), window); dialog.setWindowModality(Qt::WindowModal); if (dialog.exec()) { KService::Ptr service = dialog.service(); if (!service) { //qDebug() << "No service set, running " << dialog.text(); service = KService::Ptr(new KService(QString() /*name*/, dialog.text(), QString() /*icon*/)); } const RunFlags flags = tempFiles ? KRun::DeleteTemporaryFiles : RunFlags(); return KRun::runApplication(*service, lst, window, flags, suggestedFileName, asn); } return false; } void KRun::shellQuote(QString &_str) { // Credits to Walter, says Bernd G. :) if (_str.isEmpty()) { // Don't create an explicit empty parameter return; } const QChar q = QLatin1Char('\''); _str.replace(q, QLatin1String("'\\''")).prepend(q).append(q); } QStringList KRun::processDesktopExec(const KService &_service, const QList &_urls, bool tempFiles, const QString &suggestedFileName) { KIO::DesktopExecParser parser(_service, _urls); parser.setUrlsAreTempFiles(tempFiles); parser.setSuggestedFileName(suggestedFileName); return parser.resultingArguments(); } QString KRun::binaryName(const QString &execLine, bool removePath) { return removePath ? KIO::DesktopExecParser::executableName(execLine) : KIO::DesktopExecParser::executablePath(execLine); } // This code is also used in klauncher. // TODO: port klauncher to KIOGuiPrivate::checkStartupNotify once this lands // TODO: then deprecate this method, and remove in KF6 bool KRun::checkStartupNotify(const QString & /*binName*/, const KService *service, bool *silent_arg, QByteArray *wmclass_arg) { return KIOGuiPrivate::checkStartupNotify(service, silent_arg, wmclass_arg); } bool KRun::run(const KService &_service, const QList &_urls, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { const RunFlags flags = tempFiles ? KRun::DeleteTemporaryFiles : RunFlags(); return runApplication(_service, _urls, window, flags, suggestedFileName, asn) != 0; } qint64 KRun::runApplication(const KService &service, const QList &urls, QWidget *window, RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { KService::Ptr servicePtr(new KService(service)); // clone // QTBUG-59017 Calling winId() on an embedded widget will break interaction // with it on high-dpi multi-screen setups (cf. also Bug 363548), hence using // its parent window instead if (window) { window = window->window(); } KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr); job->setUrls(urls); if (flags & DeleteTemporaryFiles) { job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); } job->setSuggestedFileName(suggestedFileName); job->setStartupId(asn); job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); job->start(); job->waitForStarted(); return job->error() ? 0 : job->pid(); } qint64 KRun::runService(const KService &_service, const QList &_urls, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { return runApplication(_service, _urls, window, tempFiles ? RunFlags(DeleteTemporaryFiles) : RunFlags(), suggestedFileName, asn); } bool KRun::run(const QString &_exec, const QList &_urls, QWidget *window, const QString &_name, const QString &_icon, const QByteArray &asn) { KService::Ptr service(new KService(_name, _exec, _icon)); return runApplication(*service, _urls, window, RunFlags{}, QString(), asn); } bool KRun::runCommand(const QString &cmd, QWidget *window, const QString &workingDirectory) { if (cmd.isEmpty()) { qCWarning(KIO_WIDGETS) << "Command was empty, nothing to run"; return false; } const QStringList args = KShell::splitArgs(cmd); if (args.isEmpty()) { qCWarning(KIO_WIDGETS) << "Command could not be parsed."; return false; } const QString &bin = args.first(); return KRun::runCommand(cmd, bin, bin /*iconName*/, window, QByteArray(), workingDirectory); } bool KRun::runCommand(const QString &cmd, const QString &execName, const QString &iconName, QWidget *window, const QByteArray &asn) { return runCommand(cmd, execName, iconName, window, asn, QString()); } bool KRun::runCommand(const QString &cmd, const QString &execName, const QString &iconName, QWidget *window, const QByteArray &asn, const QString &workingDirectory) { auto *job = new KIO::CommandLauncherJob(cmd); job->setExecutable(execName); job->setIcon(iconName); job->setStartupId(asn); job->setWorkingDirectory(workingDirectory); if (window) { window = window->window(); } return KRunPrivate::runCommandLauncherJob(job, window); } KRun::KRun(const QUrl &url, QWidget *window, bool showProgressInfo, const QByteArray &asn) : d(new KRunPrivate(this)) { d->m_timer = new QTimer(this); d->m_timer->setObjectName(QStringLiteral("KRun::timer")); d->m_timer->setSingleShot(true); d->init(url, window, showProgressInfo, asn); } void KRunPrivate::init(const QUrl &url, QWidget *window, bool showProgressInfo, const QByteArray &asn) { m_bFault = false; m_bAutoDelete = true; m_bProgressInfo = showProgressInfo; m_bFinished = false; m_job = nullptr; m_strURL = url; m_bScanFile = false; m_bIsDirectory = false; m_runExecutables = true; m_followRedirections = true; m_window = window; m_asn = asn; q->setEnableExternalBrowser(true); // Start the timer. This means we will return to the event // loop and do initialization afterwards. // Reason: We must complete the constructor before we do anything else. m_bCheckPrompt = false; m_bInit = true; q->connect(m_timer, &QTimer::timeout, q, &KRun::slotTimeout); startTimer(); //qDebug() << "new KRun" << q << url << "timer=" << m_timer; } void KRun::init() { //qDebug() << "INIT called"; if (!d->m_strURL.isValid() || d->m_strURL.scheme().isEmpty()) { const QString error = !d->m_strURL.isValid() ? d->m_strURL.errorString() : d->m_strURL.toString(); handleInitError(KIO::ERR_MALFORMED_URL, i18n("Malformed URL\n%1", error)); qCWarning(KIO_WIDGETS) << "Malformed URL:" << error; d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), d->m_strURL)) { QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, d->m_strURL.toDisplayString()); handleInitError(KIO::ERR_ACCESS_DENIED, msg); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } if (d->m_externalBrowserEnabled && checkNeedPortalSupport()) { // use the function from QDesktopServices as it handles portals correctly d->m_bFault = !QDesktopServices::openUrl(d->m_strURL); d->m_bFinished = true; d->startTimer(); return; } if (!d->m_externalBrowser.isEmpty() && d->m_strURL.scheme().startsWith(QLatin1String("http"))) { if (d->runExternalBrowser(d->m_externalBrowser)) { return; } } else if (d->m_strURL.isLocalFile() && (d->m_strURL.host().isEmpty() || (d->m_strURL.host() == QLatin1String("localhost")) || (d->m_strURL.host().compare(QHostInfo::localHostName(), Qt::CaseInsensitive) == 0))) { const QString localPath = d->m_strURL.toLocalFile(); if (!QFile::exists(localPath)) { handleInitError(KIO::ERR_DOES_NOT_EXIST, i18n("Unable to run the command specified. " "The file or folder %1 does not exist.", localPath.toHtmlEscaped())); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(d->m_strURL); //qDebug() << "MIME TYPE is " << mime.name(); if (mime.isDefault() && !QFileInfo(localPath).isReadable()) { // Unknown mimetype because the file is unreadable, no point in showing an open-with dialog (#261002) const QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, localPath); handleInitError(KIO::ERR_ACCESS_DENIED, msg); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } else { mimeTypeDetermined(mime.name()); return; } } else if (KIO::DesktopExecParser::hasSchemeHandler(d->m_strURL)) { // looks for an application associated with x-scheme-handler/ const KService::Ptr service = schemeService(d->m_strURL.scheme()); if (service) { // if there's one... if (runApplication(*service, QList() << d->m_strURL, d->m_window, RunFlags{}, QString(), d->m_asn)) { d->m_bFinished = true; d->startTimer(); return; } } else { // fallback, look for associated helper protocol Q_ASSERT(KProtocolInfo::isHelperProtocol(d->m_strURL.scheme())); const auto exec = KProtocolInfo::exec(d->m_strURL.scheme()); if (exec.isEmpty()) { // use default mimetype opener for file mimeTypeDetermined(KProtocolManager::defaultMimetype(d->m_strURL)); return; } else { if (run(exec, QList() << d->m_strURL, d->m_window, QString(), QString(), d->m_asn)) { d->m_bFinished = true; d->startTimer(); return; } } } } #if 0 // removed for KF5 (for portability). Reintroduce a bool or flag if useful. // Did we already get the information that it is a directory ? if ((d->m_mode & QT_STAT_MASK) == QT_STAT_DIR) { mimeTypeDetermined("inode/directory"); return; } #endif // Let's see whether it is a directory if (!KProtocolManager::supportsListing(d->m_strURL)) { // No support for listing => it can't be a directory (example: http) if (!KProtocolManager::supportsReading(d->m_strURL)) { // No support for reading files either => we can't do anything (example: mailto URL, with no associated app) handleInitError(KIO::ERR_UNSUPPORTED_ACTION, i18n("Could not find any application or handler for %1", d->m_strURL.toDisplayString())); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } scanFile(); return; } //qDebug() << "Testing directory (stating)"; // It may be a directory or a file, let's stat KIO::JobFlags flags = d->m_bProgressInfo ? KIO::DefaultFlags : KIO::HideProgressInfo; KIO::StatJob *job = KIO::statDetails(d->m_strURL, KIO::StatJob::SourceSide, KIO::StatBasic, flags); KJobWidgets::setWindow(job, d->m_window); connect(job, &KJob::result, this, &KRun::slotStatResult); d->m_job = job; //qDebug() << "Job" << job << "is about stating" << d->m_strURL; } KRun::~KRun() { //qDebug() << this; d->m_timer->stop(); killJob(); //qDebug() << this << "done"; delete d; } bool KRunPrivate::runExternalBrowser(const QString &_exec) { QList urls; urls.append(m_strURL); if (_exec.startsWith(QLatin1Char('!'))) { // Literal command const QString exec = _exec.midRef(1) + QLatin1String(" %u"); if (KRun::run(exec, urls, m_window, QString(), QString(), m_asn)) { m_bFinished = true; startTimer(); return true; } } else { KService::Ptr service = KService::serviceByStorageId(_exec); if (service && KRun::runApplication(*service, urls, m_window, KRun::RunFlags{}, QString(), m_asn)) { m_bFinished = true; startTimer(); return true; } } return false; } void KRunPrivate::showPrompt() { ExecutableFileOpenDialog *dialog = new ExecutableFileOpenDialog(promptMode(), q->window()); dialog->setAttribute(Qt::WA_DeleteOnClose); QObject::connect(dialog, &ExecutableFileOpenDialog::finished, q, [this, dialog](int result){ onDialogFinished(result, dialog->isDontAskAgainChecked()); }); dialog->show(); } bool KRunPrivate::isPromptNeeded() { if (m_strURL == QUrl(QStringLiteral("remote:/x-wizard_service.desktop"))) { return false; } const QMimeDatabase db; const QMimeType mime = db.mimeTypeForUrl(m_strURL); const bool isFileExecutable = (KRun::isExecutableFile(m_strURL, mime.name()) || mime.inherits(QStringLiteral("application/x-desktop"))); if (isFileExecutable) { KConfigGroup cfgGroup(KSharedConfig::openConfig(QStringLiteral("kiorc")), "Executable scripts"); const QString value = cfgGroup.readEntry("behaviourOnLaunch", "alwaysAsk"); if (value == QLatin1String("alwaysAsk")) { return true; } else { q->setRunExecutables(value == QLatin1String("execute")); } } return false; } ExecutableFileOpenDialog::Mode KRunPrivate::promptMode() { const QMimeDatabase db; const QMimeType mime = db.mimeTypeForUrl(m_strURL); if (mime.inherits(QStringLiteral("text/plain"))) { return ExecutableFileOpenDialog::OpenOrExecute; } #ifndef Q_OS_WIN if (mime.inherits(QStringLiteral("application/x-ms-dos-executable"))) { return ExecutableFileOpenDialog::OpenAsExecute; } #endif return ExecutableFileOpenDialog::OnlyExecute; } void KRunPrivate::onDialogFinished(int result, bool isDontAskAgainSet) { if (result == ExecutableFileOpenDialog::Rejected) { m_bFinished = true; m_bInit = false; startTimer(); return; } q->setRunExecutables(result == ExecutableFileOpenDialog::ExecuteFile); if (isDontAskAgainSet) { QString output = result == ExecutableFileOpenDialog::OpenFile ? QStringLiteral("open") : QStringLiteral("execute"); KConfigGroup cfgGroup(KSharedConfig::openConfig(QStringLiteral("kiorc")), "Executable scripts"); cfgGroup.writeEntry("behaviourOnLaunch", output); } startTimer(); } void KRun::scanFile() { //qDebug() << d->m_strURL; // First, let's check for well-known extensions // Not when there is a query in the URL, in any case. if (!d->m_strURL.hasQuery()) { QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(d->m_strURL); if (!mime.isDefault() || d->m_strURL.isLocalFile()) { //qDebug() << "Scanfile: MIME TYPE is " << mime.name(); mimeTypeDetermined(mime.name()); return; } } // No mimetype found, and the URL is not local (or fast mode not allowed). // We need to apply the 'KIO' method, i.e. either asking the server or // getting some data out of the file, to know what mimetype it is. if (!KProtocolManager::supportsReading(d->m_strURL)) { qCWarning(KIO_WIDGETS) << "#### NO SUPPORT FOR READING!"; d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } //qDebug() << this << "Scanning file" << d->m_strURL; KIO::JobFlags flags = d->m_bProgressInfo ? KIO::DefaultFlags : KIO::HideProgressInfo; KIO::TransferJob *job = KIO::get(d->m_strURL, KIO::NoReload /*reload*/, flags); KJobWidgets::setWindow(job, d->m_window); connect(job, &KJob::result, this, &KRun::slotScanFinished); connect(job, QOverload::of(&KIO::TransferJob::mimetype), this, &KRun::slotScanMimeType); d->m_job = job; //qDebug() << "Job" << job << "is about getting from" << d->m_strURL; } // When arriving in that method there are 6 possible states: // must_show_prompt, must_init, must_scan_file, found_dir, done+error or done+success. void KRun::slotTimeout() { if (d->m_bCheckPrompt) { d->m_bCheckPrompt = false; if (d->isPromptNeeded()) { d->showPrompt(); return; } } if (d->m_bInit) { d->m_bInit = false; init(); return; } if (d->m_bFault) { emit error(); } if (d->m_bFinished) { emit finished(); } else { if (d->m_bScanFile) { d->m_bScanFile = false; scanFile(); return; } else if (d->m_bIsDirectory) { d->m_bIsDirectory = false; mimeTypeDetermined(QStringLiteral("inode/directory")); return; } } if (d->m_bAutoDelete) { deleteLater(); return; } } void KRun::slotStatResult(KJob *job) { d->m_job = nullptr; const int errCode = job->error(); if (errCode) { // ERR_NO_CONTENT is not an error, but an indication no further // actions needs to be taken. if (errCode != KIO::ERR_NO_CONTENT) { qCWarning(KIO_WIDGETS) << this << "ERROR" << job->error() << job->errorString(); handleError(job); //qDebug() << this << " KRun returning from showErrorDialog, starting timer to delete us"; d->m_bFault = true; } d->m_bFinished = true; // will emit the error and autodelete this d->startTimer(); } else { //qDebug() << "Finished"; KIO::StatJob *statJob = qobject_cast(job); if (!statJob) { qFatal("Fatal Error: job is a %s, should be a StatJob", typeid(*job).name()); } // Update our URL in case of a redirection setUrl(statJob->url()); const KIO::UDSEntry entry = statJob->statResult(); const mode_t mode = entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE); if ((mode & QT_STAT_MASK) == QT_STAT_DIR) { d->m_bIsDirectory = true; // it's a dir } else { d->m_bScanFile = true; // it's a file } d->m_localPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); // mimetype already known? (e.g. print:/manager) const QString knownMimeType = entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE); if (!knownMimeType.isEmpty()) { mimeTypeDetermined(knownMimeType); d->m_bFinished = true; } // We should have found something assert(d->m_bScanFile || d->m_bIsDirectory); // Start the timer. Once we get the timer event this // protocol server is back in the pool and we can reuse it. // This gives better performance than starting a new slave d->startTimer(); } } void KRun::slotScanMimeType(KIO::Job *, const QString &mimetype) { if (mimetype.isEmpty()) { qCWarning(KIO_WIDGETS) << "get() didn't emit a mimetype! Probably a kioslave bug, please check the implementation of" << url().scheme(); } mimeTypeDetermined(mimetype); d->m_job = nullptr; } void KRun::slotScanFinished(KJob *job) { d->m_job = nullptr; const int errCode = job->error(); if (errCode) { // ERR_NO_CONTENT is not an error, but an indication no further // actions needs to be taken. if (errCode != KIO::ERR_NO_CONTENT) { qCWarning(KIO_WIDGETS) << this << "ERROR (stat):" << job->error() << ' ' << job->errorString(); handleError(job); d->m_bFault = true; } d->m_bFinished = true; // will emit the error and autodelete this d->startTimer(); } } void KRun::mimeTypeDetermined(const QString &mimeType) { // foundMimeType reimplementations might show a dialog box; // make sure some timer doesn't kill us meanwhile (#137678, #156447) Q_ASSERT(!d->m_showingDialog); d->m_showingDialog = true; foundMimeType(mimeType); d->m_showingDialog = false; // We cannot assume that we're finished here. Some reimplementations // start a KIO job and call setFinished only later. } void KRun::foundMimeType(const QString &type) { //qDebug() << "Resulting mime type is " << type; QMimeDatabase db; KIO::TransferJob *job = qobject_cast(d->m_job); if (job) { // Update our URL in case of a redirection if (d->m_followRedirections) { setUrl(job->url()); } job->putOnHold(); KIO::Scheduler::publishSlaveOnHold(); d->m_job = nullptr; } Q_ASSERT(!d->m_bFinished); // Support for preferred service setting, see setPreferredService if (!d->m_preferredService.isEmpty()) { //qDebug() << "Attempting to open with preferred service: " << d->m_preferredService; KService::Ptr serv = KService::serviceByDesktopName(d->m_preferredService); if (serv && serv->hasMimeType(type)) { QList lst; lst.append(d->m_strURL); if (KRun::runApplication(*serv, lst, d->m_window, RunFlags{}, QString(), d->m_asn)) { setFinished(true); return; } /// Note: if that service failed, we'll go to runUrl below to /// maybe find another service, even though an error dialog box was /// already displayed. That's good if runUrl tries another service, /// but it's not good if it tries the same one :} } } // Resolve .desktop files from media:/, remote:/, applications:/ etc. QMimeType mime = db.mimeTypeForName(type); if (!mime.isValid()) { - qCWarning(KIO_WIDGETS) << "Unknown mimetype " << type; + qCWarning(KIO_WIDGETS) << "Unknown mimetype" << type; } else if (mime.inherits(QStringLiteral("application/x-desktop")) && !d->m_localPath.isEmpty()) { d->m_strURL = QUrl::fromLocalFile(d->m_localPath); } KRun::RunFlags runFlags; if (d->m_runExecutables) { runFlags |= KRun::RunExecutables; } if (!KRun::runUrl(d->m_strURL, type, d->m_window, runFlags, d->m_suggestedFileName, d->m_asn)) { d->m_bFault = true; } setFinished(true); } void KRun::killJob() { if (d->m_job) { //qDebug() << this << "m_job=" << d->m_job; d->m_job->kill(); d->m_job = nullptr; } } void KRun::abort() { if (d->m_bFinished) { return; } //qDebug() << this << "m_showingDialog=" << d->m_showingDialog; killJob(); // If we're showing an error message box, the rest will be done // after closing the msgbox -> don't autodelete nor emit signals now. if (d->m_showingDialog) { return; } d->m_bFault = true; d->m_bFinished = true; d->m_bInit = false; d->m_bScanFile = false; // will emit the error and autodelete this d->startTimer(); } QWidget *KRun::window() const { return d->m_window; } bool KRun::hasError() const { return d->m_bFault; } bool KRun::hasFinished() const { return d->m_bFinished; } bool KRun::autoDelete() const { return d->m_bAutoDelete; } void KRun::setAutoDelete(bool b) { d->m_bAutoDelete = b; } void KRun::setEnableExternalBrowser(bool b) { d->m_externalBrowserEnabled = b; if (d->m_externalBrowserEnabled) { d->m_externalBrowser = KConfigGroup(KSharedConfig::openConfig(), "General").readEntry("BrowserApplication"); // If a default browser isn't set in kdeglobals, fall back to mimeapps.list if (!d->m_externalBrowser.isEmpty()) { return; } KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); KConfigGroup defaultApps(profile, "Default Applications"); d->m_externalBrowser = defaultApps.readEntry("x-scheme-handler/https"); if (d->m_externalBrowser.isEmpty()) { d->m_externalBrowser = defaultApps.readEntry("x-scheme-handler/http"); } } else { d->m_externalBrowser.clear(); } } void KRun::setPreferredService(const QString &desktopEntryName) { d->m_preferredService = desktopEntryName; } void KRun::setRunExecutables(bool b) { d->m_runExecutables = b; } void KRun::setSuggestedFileName(const QString &fileName) { d->m_suggestedFileName = fileName; } void KRun::setShowScriptExecutionPrompt(bool showPrompt) { d->m_bCheckPrompt = showPrompt; } void KRun::setFollowRedirections(bool followRedirections) { d->m_followRedirections = followRedirections; } QString KRun::suggestedFileName() const { return d->m_suggestedFileName; } bool KRun::isExecutable(const QString &mimeTypeName) { QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName(mimeTypeName); return (mimeType.inherits(QLatin1String("application/x-desktop")) || mimeType.inherits(QLatin1String("application/x-executable")) || /* See https://bugs.freedesktop.org/show_bug.cgi?id=97226 */ mimeType.inherits(QLatin1String("application/x-sharedlib")) || mimeType.inherits(QLatin1String("application/x-ms-dos-executable")) || mimeType.inherits(QLatin1String("application/x-shellscript"))); } void KRun::setUrl(const QUrl &url) { d->m_strURL = url; } QUrl KRun::url() const { return d->m_strURL; } void KRun::setError(bool error) { d->m_bFault = error; } void KRun::setProgressInfo(bool progressInfo) { d->m_bProgressInfo = progressInfo; } bool KRun::progressInfo() const { return d->m_bProgressInfo; } void KRun::setFinished(bool finished) { d->m_bFinished = finished; if (finished) { d->startTimer(); } } void KRun::setJob(KIO::Job *job) { d->m_job = job; } KIO::Job *KRun::job() { return d->m_job; } QTimer &KRun::timer() { return *d->m_timer; } void KRun::setDoScanFile(bool scanFile) { d->m_bScanFile = scanFile; } bool KRun::doScanFile() const { return d->m_bScanFile; } void KRun::setIsDirecory(bool isDirectory) { d->m_bIsDirectory = isDirectory; } bool KRun::isDirectory() const { return d->m_bIsDirectory; } void KRun::setInitializeNextAction(bool initialize) { d->m_bInit = initialize; } bool KRun::initializeNextAction() const { return d->m_bInit; } bool KRun::isLocalFile() const { return d->m_strURL.isLocalFile(); } #include "moc_krun.cpp" #include "moc_krun_p.cpp" diff --git a/src/widgets/widgetsopenurljobhandler.cpp b/src/widgets/widgetsopenurljobhandler.cpp new file mode 100644 index 00000000..b224013c --- /dev/null +++ b/src/widgets/widgetsopenurljobhandler.cpp @@ -0,0 +1,55 @@ +/* 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 "kopenwithdialog.h" +#include "openurljob.h" +#include "widgetsopenurljobhandler.h" + +#include +#include +#include + +#include + +KIO::WidgetsOpenUrlJobHandler::WidgetsOpenUrlJobHandler() + : KIO::OpenUrlJobHandlerInterface() +{ +} + +KIO::WidgetsOpenUrlJobHandler::~WidgetsOpenUrlJobHandler() = default; + +void KIO::WidgetsOpenUrlJobHandler::promptUserForApplication(KIO::OpenUrlJob *job, const QUrl &url, const QString &mimeType) +{ + QWidget *parentWidget = job ? KJobWidgets::window(job) : qApp->activeWindow(); + + KOpenWithDialog *dialog = new KOpenWithDialog({url}, mimeType, QString(), QString(), parentWidget); + dialog->setAttribute(Qt::WA_DeleteOnClose); + connect(dialog, &QDialog::accepted, this, [=]() { + KService::Ptr service = dialog->service(); + if (!service) { + service = KService::Ptr(new KService(QString() /*name*/, dialog->text(), QString() /*icon*/)); + } + Q_EMIT serviceSelected(service); + }); + connect(dialog, &QDialog::rejected, this, [this]() { + Q_EMIT canceled(); + }); + dialog->show(); +} diff --git a/src/widgets/widgetsopenurljobhandler.h b/src/widgets/widgetsopenurljobhandler.h new file mode 100644 index 00000000..23e03d44 --- /dev/null +++ b/src/widgets/widgetsopenurljobhandler.h @@ -0,0 +1,50 @@ +/* This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef WIDGETSOPENURLJOBHANDLER_H +#define WIDGETSOPENURLJOBHANDLER_H + +#include "openurljobhandlerinterface.h" + +class QDialog; +class QWidget; + +namespace KIO { + +// TODO KF6: Make KIO::JobUiDelegate inherit from WidgetsOpenUrlJobHandler +// (or even merge the two classes) +// so that setDelegate(new KIO::JobUiDelegate) provides both dialog boxes on error +// and the open with dialog. + +class WidgetsOpenUrlJobHandler : public OpenUrlJobHandlerInterface +{ +public: + WidgetsOpenUrlJobHandler(); + ~WidgetsOpenUrlJobHandler() override; + + void promptUserForApplication(KIO::OpenUrlJob *job, const QUrl &url, const QString &mimeType) override; + +private: + // Note: no d pointer because not exported at this point +}; + +} + +#endif // WIDGETSOPENURLJOBHANDLER_H