diff --git a/autotests/applicationlauncherjobtest.cpp b/autotests/applicationlauncherjobtest.cpp index 73646c86..7583d88a 100644 --- a/autotests/applicationlauncherjobtest.cpp +++ b/autotests/applicationlauncherjobtest.cpp @@ -1,366 +1,441 @@ /* This file is part of the KDE libraries Copyright (c) 2014, 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "applicationlauncherjobtest.h" #include "applicationlauncherjob.h" #include #include +#include #include "kiotesthelper.h" // createTestFile etc. #include #include #include #ifdef Q_OS_UNIX #include // kill #endif #include #include #include #include QTEST_GUILESS_MAIN(ApplicationLauncherJobTest) namespace KIO { KIOGUI_EXPORT void setDefaultUntrustedProgramHandler(KIO::UntrustedProgramHandlerInterface *iface); +KIOGUI_EXPORT void setDefaultOpenWithHandler(KIO::OpenWithHandlerInterface *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; +class TestOpenWithHandler : public KIO::OpenWithHandlerInterface +{ +public: + void promptUserForApplication(KJob *job, const QList &url, const QString &mimeType) override + { + Q_UNUSED(job); + m_urls << url; + m_mimeTypes << mimeType; + if (m_chosenService) { + Q_EMIT serviceSelected(m_chosenService); + } else { + Q_EMIT canceled(); + } + } + QList m_urls; + QStringList m_mimeTypes; + KService::Ptr m_chosenService; +}; +static TestOpenWithHandler s_openWithHandler; + void ApplicationLauncherJobTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); } void ApplicationLauncherJobTest::cleanupTestCase() { std::for_each(m_filesToRemove.begin(), m_filesToRemove.end(), [](const QString & f) { QFile::remove(f); }); } static const char s_tempServiceName[] = "applicationlauncherjobtest_service.desktop"; static void createSrcFile(const QString path) { QFile srcFile(path); QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString())); srcFile.write("Hello world\n"); } void ApplicationLauncherJobTest::startProcess_data() { QTest::addColumn("tempFile"); QTest::addColumn("useExec"); QTest::addColumn("numFiles"); QTest::newRow("1_file_exec") << false << true << 1; QTest::newRow("1_file_waitForStarted") << false << false << 1; QTest::newRow("1_tempfile_exec") << true << true << 1; QTest::newRow("1_tempfile_waitForStarted") << true << false << 1; QTest::newRow("2_files_exec") << false << true << 2; QTest::newRow("2_files_waitForStarted") << false << false << 2; QTest::newRow("2_tempfiles_exec") << true << true << 2; QTest::newRow("2_tempfiles_waitForStarted") << true << false << 2; } void ApplicationLauncherJobTest::startProcess() { QFETCH(bool, tempFile); QFETCH(bool, useExec); QFETCH(int, numFiles); // Given a service desktop file and a number of source files const QString path = createTempService(); QTemporaryDir tempDir; const QString srcDir = tempDir.path(); QList urls; for (int i = 0; i < numFiles; ++i) { const QString srcFile = srcDir + "/srcfile" + QString::number(i + 1); createSrcFile(srcFile); QVERIFY(QFile::exists(srcFile)); urls.append(QUrl::fromLocalFile(srcFile)); } // When running a ApplicationLauncherJob KService::Ptr servicePtr(new KService(path)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); job->setUrls(urls); if (tempFile) { job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); } if (useExec) { QVERIFY2(job->exec(), qPrintable(job->errorString())); } else { job->start(); QVERIFY(job->waitForStarted()); } const QVector pids = job->pids(); // Then the service should be executed (which copies the source file to "dest") QCOMPARE(pids.count(), numFiles); QVERIFY(!pids.contains(0)); for (int i = 0; i < numFiles; ++i) { const QString dest = srcDir + "/dest_srcfile" + QString::number(i + 1); QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); QVERIFY(QFile::exists(srcDir + "/srcfile" + QString::number(i + 1))); // if tempfile is true, kioexec will delete it... in 3 minutes. QVERIFY(QFile::remove(dest)); // cleanup } #ifdef Q_OS_UNIX // Kill the running kioexec processes for (qint64 pid : pids) { ::kill(pid, SIGTERM); } #endif // The kioexec processes that are waiting for 3 minutes and got killed above, // will now trigger KProcessRunner::slotProcessError, KProcessRunner::slotProcessExited and delete the KProcessRunner. // We wait for that to happen otherwise it gets confusing to see that output from later tests. QTRY_COMPARE(KProcessRunner::instanceCount(), 0); } void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile_data() { QTest::addColumn("withHandler"); QTest::addColumn("handlerRetVal"); QTest::addColumn("useExec"); QTest::newRow("no_handler_exec") << false << false << true; QTest::newRow("handler_false_exec") << true << false << true; QTest::newRow("handler_true_exec") << true << true << true; QTest::newRow("no_handler_waitForStarted") << false << false << false; QTest::newRow("handler_false_waitForStarted") << true << false << false; QTest::newRow("handler_true_waitForStarted") << true << true << false; } void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile() { QFETCH(bool, useExec); QFETCH(bool, withHandler); QFETCH(bool, handlerRetVal); // Given a .desktop file in a temporary directory (outside the trusted paths) QTemporaryDir tempDir; const QString srcDir = tempDir.path(); const QString desktopFilePath = srcDir + "/shouldfail.desktop"; writeTempServiceDesktopFile(desktopFilePath); m_filesToRemove.append(desktopFilePath); const QString srcFile = srcDir + "/srcfile"; createSrcFile(srcFile); const QList urls{QUrl::fromLocalFile(srcFile)}; KService::Ptr servicePtr(new KService(desktopFilePath)); s_handler.m_calls.clear(); s_handler.setRetVal(handlerRetVal); KIO::setDefaultUntrustedProgramHandler(withHandler ? &s_handler : nullptr); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); job->setUrls(urls); bool success; if (useExec) { success = job->exec(); } else { job->start(); success = job->waitForStarted(); } if (!withHandler) { QVERIFY(!success); QCOMPARE(job->error(), KJob::UserDefinedError); QCOMPARE(job->errorString(), QStringLiteral("You are not authorized to execute this file.")); } else { if (handlerRetVal) { QVERIFY(success); + const QString dest = srcDir + "/dest_srcfile"; + QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); + // The actual shell process will race against the deletion of the QTemporaryDir, // so don't be surprised by stderr like getcwd: cannot access parent directories: No such file or directory QTest::qWait(50); // this helps a bit } else { QVERIFY(!success); QCOMPARE(job->error(), KIO::ERR_USER_CANCELED); } } if (withHandler) { // check that the handler was called QCOMPARE(s_handler.m_calls.count(), 1); QCOMPARE(s_handler.m_calls.at(0), QStringLiteral("KRunUnittestService")); } } void ApplicationLauncherJobTest::shouldFailOnNonExistingExecutable_data() { QTest::addColumn("tempFile"); QTest::addColumn("fullPath"); QTest::newRow("file") << false << false; QTest::newRow("tempFile") << true << false; QTest::newRow("file_fullPath") << false << true; QTest::newRow("tempFile_fullPath") << true << true; } void ApplicationLauncherJobTest::shouldFailOnNonExistingExecutable() { QFETCH(bool, tempFile); QFETCH(bool, fullPath); const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/non_existing_executable.desktop"); KDesktopFile file(desktopFilePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestService"); group.writeEntry("Type", "Service"); if (fullPath) { group.writeEntry("Exec", "/usr/bin/does_not_exist %f %d/dest_%n"); } else { group.writeEntry("Exec", "does_not_exist %f %d/dest_%n"); } file.sync(); KService::Ptr servicePtr(new KService(desktopFilePath)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); job->setUrls({QUrl::fromLocalFile(desktopFilePath)}); // just to have one URL as argument, as the desktop file expects if (tempFile) { job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); } QVERIFY(!job->exec()); QCOMPARE(job->error(), KJob::UserDefinedError); if (fullPath) { QCOMPARE(job->errorString(), QStringLiteral("Could not find the program '/usr/bin/does_not_exist'")); } else { QCOMPARE(job->errorString(), QStringLiteral("Could not find the program 'does_not_exist'")); } QFile::remove(desktopFilePath); } void ApplicationLauncherJobTest::shouldFailOnInvalidService() { const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); KDesktopFile file(desktopFilePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestService"); group.writeEntry("Type", "NoSuchType"); group.writeEntry("Exec", "does_not_exist"); file.sync(); QTest::ignoreMessage(QtWarningMsg, QRegularExpression("The desktop entry file \".*\" has Type.*\"NoSuchType\" instead of \"Application\" or \"Service\"")); KService::Ptr servicePtr(new KService(desktopFilePath)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); QVERIFY(!job->exec()); QCOMPARE(job->error(), KJob::UserDefinedError); QCOMPARE(job->errorString(), QStringLiteral("The desktop entry file\n%1\nis not valid.").arg(desktopFilePath)); QFile::remove(desktopFilePath); } void ApplicationLauncherJobTest::shouldFailOnServiceWithNoExec() { const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); KDesktopFile file(desktopFilePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestServiceNoExec"); group.writeEntry("Type", "Service"); file.sync(); QTest::ignoreMessage(QtWarningMsg, qPrintable(QString("No Exec field in \"%1\"").arg(desktopFilePath))); KService::Ptr servicePtr(new KService(desktopFilePath)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); QVERIFY(!job->exec()); QCOMPARE(job->error(), KJob::UserDefinedError); QCOMPARE(job->errorString(), QStringLiteral("No Exec field in %1").arg(desktopFilePath)); QFile::remove(desktopFilePath); } void ApplicationLauncherJobTest::shouldFailOnExecutableWithoutPermissions() { #ifdef Q_OS_UNIX // Given an executable shell script that copies "src" to "dest" (we'll cheat with the mimetype to treat it like a native binary) QTemporaryDir tempDir; const QString dir = tempDir.path(); const QString scriptFilePath = dir + QStringLiteral("/script.sh"); QFile scriptFile(scriptFilePath); QVERIFY(scriptFile.open(QIODevice::WriteOnly)); scriptFile.write("#!/bin/sh\ncp src dest"); scriptFile.close(); // Note that it's missing executable permissions const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); KDesktopFile file(desktopFilePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestServiceNoPermission"); group.writeEntry("Type", "Service"); group.writeEntry("Exec", scriptFilePath); file.sync(); KService::Ptr servicePtr(new KService(desktopFilePath)); KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); QVERIFY(!job->exec()); QCOMPARE(job->error(), KJob::UserDefinedError); QCOMPARE(job->errorString(), QStringLiteral("The program '%1' is missing executable permissions.").arg(scriptFilePath)); QFile::remove(desktopFilePath); #else QSKIP("This test is not run on Windows"); #endif } +void ApplicationLauncherJobTest::showOpenWithDialog_data() +{ + QTest::addColumn("handlerRetVal"); + + QTest::newRow("false_canceled") << false; + QTest::newRow("true_service_selected") << true; +} + +void ApplicationLauncherJobTest::showOpenWithDialog() +{ +#ifdef Q_OS_UNIX + QFETCH(bool, handlerRetVal); + + // Given a local text file (we could test multiple files, too...) + QTemporaryDir tempDir; + const QString srcDir = tempDir.path(); + const QString srcFile = srcDir + QLatin1String("/file.txt"); + createSrcFile(srcFile); + + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(this); + job->setUrls({QUrl::fromLocalFile(srcFile)}); + + KService::Ptr service = KService::serviceByDesktopName(QString(s_tempServiceName).remove(".desktop")); + QVERIFY(service); + s_openWithHandler.m_urls.clear(); + s_openWithHandler.m_mimeTypes.clear(); + s_openWithHandler.m_chosenService = handlerRetVal ? service : KService::Ptr{}; + KIO::setDefaultOpenWithHandler(&s_openWithHandler); + + const bool success = job->exec(); + + // Then --- it depends on what the user says via the handler + + QCOMPARE(s_openWithHandler.m_urls.count(), 1); + QCOMPARE(s_openWithHandler.m_mimeTypes.count(), 1); + QCOMPARE(s_openWithHandler.m_mimeTypes.at(0), QString()); // the job doesn't have the information + if (handlerRetVal) { + QVERIFY2(success, qPrintable(job->errorString())); + // If the user chose a service, it should be executed (it writes to "dest") + const QString dest = srcDir + "/dest_file.txt"; + QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); + } else { + QVERIFY(!success); + QCOMPARE(job->error(), KIO::ERR_USER_CANCELED); + } +#else + QSKIP("Test skipped on Windows because the code ends up in QDesktopServices::openUrl") +#endif +} + void ApplicationLauncherJobTest::writeTempServiceDesktopFile(const QString &filePath) { if (!QFile::exists(filePath)) { KDesktopFile file(filePath); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "KRunUnittestService"); group.writeEntry("Type", "Service"); #ifdef Q_OS_WIN group.writeEntry("Exec", "copy.exe %f %d/dest_%n"); #else group.writeEntry("Exec", "cd %d ; cp %f %d/dest_%n"); // cd is just to show that we can't do QFile::exists(binary) #endif file.sync(); } } QString ApplicationLauncherJobTest::createTempService() { const QString fileName = s_tempServiceName; const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/") + fileName; writeTempServiceDesktopFile(fakeService); m_filesToRemove.append(fakeService); return fakeService; } diff --git a/autotests/applicationlauncherjobtest.h b/autotests/applicationlauncherjobtest.h index 17a105cf..54b7b8f5 100644 --- a/autotests/applicationlauncherjobtest.h +++ b/autotests/applicationlauncherjobtest.h @@ -1,58 +1,61 @@ /* This file is part of the KDE libraries Copyright (c) 2014, 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef APPLICATIONLAUNCHERJOBTEST_H #define APPLICATIONLAUNCHERJOBTEST_H #include #include class ApplicationLauncherJobTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void startProcess_data(); void startProcess(); void shouldFailOnNonExecutableDesktopFile_data(); void shouldFailOnNonExecutableDesktopFile(); void shouldFailOnNonExistingExecutable_data(); void shouldFailOnNonExistingExecutable(); void shouldFailOnInvalidService(); void shouldFailOnServiceWithNoExec(); void shouldFailOnExecutableWithoutPermissions(); + void showOpenWithDialog_data(); + void showOpenWithDialog(); + private: QString createTempService(); void writeTempServiceDesktopFile(const QString &filePath); QStringList m_filesToRemove; }; #endif /* APPLICATIONLAUNCHERJOBTEST_H */ diff --git a/src/gui/applicationlauncherjob.cpp b/src/gui/applicationlauncherjob.cpp index 7be2b4f5..22e8f12a 100644 --- a/src/gui/applicationlauncherjob.cpp +++ b/src/gui/applicationlauncherjob.cpp @@ -1,239 +1,283 @@ /* 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 "openwithhandlerinterface.h" #include "../core/global.h" #include #include #include #include // KF6 TODO: Remove static KIO::UntrustedProgramHandlerInterface *s_untrustedProgramHandler = nullptr; + +extern KIO::OpenWithHandlerInterface *s_openWithHandler; // defined in openurljob.cpp + 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) {} + explicit ApplicationLauncherJobPrivate(KIO::ApplicationLauncherJob *job, const KService::Ptr &service) + : m_service(service), q(job) {} - void slotStarted(KIO::ApplicationLauncherJob *q, KProcessRunner *processRunner) { + void slotStarted(KProcessRunner *processRunner) { m_pids.append(processRunner->pid()); if (--m_numProcessesPending == 0) { q->emitResult(); } } + + void showOpenWithDialog(); + 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 *q; }; KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KService::Ptr &service, QObject *parent) - : KJob(parent), d(new ApplicationLauncherJobPrivate(service)) + : KJob(parent), d(new ApplicationLauncherJobPrivate(this, 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(QObject *parent) + : KJob(parent), d(new ApplicationLauncherJobPrivate(this, {})) +{ +} + 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() { + if (!d->m_service) { + d->showOpenWithDialog(); + return; + } 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->slotStarted(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); + d->slotStarted(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; } + +void KIO::ApplicationLauncherJobPrivate::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; + } + + QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() { + q->setError(KIO::ERR_USER_CANCELED); + q->emitResult(); + }); + + QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) { + Q_ASSERT(service); + m_service = service; + q->start(); + }); + + QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() { + q->emitResult(); + }); + + s_openWithHandler->promptUserForApplication(q, m_urls, QString() /* mimetype name unknown */); +} diff --git a/src/gui/applicationlauncherjob.h b/src/gui/applicationlauncherjob.h index 9964ed37..2c1d4fb8 100644 --- a/src/gui/applicationlauncherjob.h +++ b/src/gui/applicationlauncherjob.h @@ -1,168 +1,174 @@ /* 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); + /** + * Creates an ApplicationLauncherJob which will prompt the user for which application to use (the "open with" dialog). + * @param parent the parent QObject + */ + explicit ApplicationLauncherJob(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 index b7bdfa9c..93818d15 100644 --- a/src/gui/openurljob.cpp +++ b/src/gui/openurljob.cpp @@ -1,651 +1,655 @@ /* 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 "openwithhandlerinterface.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::OpenWithHandlerInterface *s_openWithHandler = nullptr; +KIO::OpenWithHandlerInterface *s_openWithHandler = nullptr; namespace KIO { // Hidden API because in KF6 we'll just check if the job's uiDelegate implements OpenWithHandlerInterface. KIOGUI_EXPORT void setDefaultOpenWithHandler(KIO::OpenWithHandlerInterface *iface) { s_openWithHandler = 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, const QList &urls); void startService(const KService::Ptr &service) { startService(service, {m_url}); } }; 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; } if (entry.isDir()) { 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, const QList &urls) { KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service, q); job->setUrls(urls); 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) const QMimeType mime = fixupMimeType(m_mimeTypeName, m_suggestedFileName.isEmpty() ? m_url.fileName() : m_suggestedFileName); const QString mimeName = mime.name(); if (mime.isValid() && mimeName != m_mimeTypeName) { m_mimeTypeName = mimeName; } 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) { return QFileInfo(fileName).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 an 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; const 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_openWithHandler || 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_openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() { q->setError(KIO::ERR_USER_CANCELED); q->emitResult(); }); QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) { startService(service); }); + QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() { + q->emitResult(); + }); + s_openWithHandler->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/openwithhandlerinterface.cpp b/src/gui/openwithhandlerinterface.cpp index 64fdd600..d7bbd174 100644 --- a/src/gui/openwithhandlerinterface.cpp +++ b/src/gui/openwithhandlerinterface.cpp @@ -1,41 +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 "openwithhandlerinterface.h" #include #include #include "kiocoredebug.h" using namespace KIO; class KIO::OpenWithHandlerInterfacePrivate {}; OpenWithHandlerInterface::OpenWithHandlerInterface() = default; OpenWithHandlerInterface::~OpenWithHandlerInterface() = default; -void OpenWithHandlerInterface::promptUserForApplication(OpenUrlJob *job, const QList &urls, const QString &mimeType) +void OpenWithHandlerInterface::promptUserForApplication(KJob *job, const QList &urls, const QString &mimeType) { Q_UNUSED(job) Q_UNUSED(urls) Q_UNUSED(mimeType) Q_EMIT canceled(); } diff --git a/src/gui/openwithhandlerinterface.h b/src/gui/openwithhandlerinterface.h index d56d3c36..d8acca24 100644 --- a/src/gui/openwithhandlerinterface.h +++ b/src/gui/openwithhandlerinterface.h @@ -1,91 +1,98 @@ /* 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 OPENWITHHANDLERINTERFACE_H #define OPENWITHHANDLERINTERFACE_H #include #include #include class QString; +class KJob; + namespace KIO { -class OpenUrlJob; class OpenWithHandlerInterfacePrivate; /** * @class OpenWithHandlerInterface openwithhandlerinterface.h * @brief The OpenWithHandlerInterface class allows OpenUrlJob to * prompt the user about which application to use to open URLs that do not * have an associated application (via the "Open With" dialog). * * This extension mechanism for jobs is similar to KIO::JobUiDelegateExtension * and UntrustedProgramHandlerInterface. * * @since 5.71 */ class KIOGUI_EXPORT OpenWithHandlerInterface : public QObject { Q_OBJECT protected: /** * Constructor */ OpenWithHandlerInterface(); /** * Destructor */ ~OpenWithHandlerInterface() override; public: /** * Show the "Open With" dialog. * @param job the job calling this. Useful to get all its properties * @param urls the URLs to open * @param mimeType the mimeType of the URLs, if known. Can be empty otherwise. * * Implementations of this method must emit either serviceSelected or canceled. * * The default implementation in this base class simply emits canceled(). * Any application using KIO::JobUiDelegate (from KIOWidgets) will benefit from an * automatically registered subclass which implements this method using KOpenWithDialog. */ - virtual void promptUserForApplication(KIO::OpenUrlJob *job, const QList &urls, const QString &mimeType); + virtual void promptUserForApplication(KJob *job, const QList &urls, const QString &mimeType); Q_SIGNALS: /** * Emitted by promptUserForApplication() once the user chooses 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(); + /** + * Emitted by promptUserForApplication() if it fully handled it including launching the app. + * This is a special case for the native Windows open-with dialog. + */ + void handled(); + private: QScopedPointer d; }; } #endif // OPENWITHHANDLERINTERFACE_H diff --git a/src/widgets/krun.h b/src/widgets/krun.h index 8d4bfa6f..c1dadb7e 100644 --- a/src/widgets/krun.h +++ b/src/widgets/krun.h @@ -1,792 +1,807 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (C) 2006 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 KRUN_H #define KRUN_H #include "kiowidgets_export.h" #include #include #include class KService; class KJob; class QTimer; class KRunPrivate; namespace KIO { class Job; } /** * @class KRun krun.h * * To open files with their associated applications in KDE, use KRun. * * It can execute any desktop entry, as well as any file, using * the default application or another application "bound" to the file type * (or URL protocol). * * In that example, the mimetype of the file is not known by the application, * so a KRun instance must be created. It will determine the mimetype by itself. * If the mimetype is known, or if you even know the service (application) to * use for this file, use one of the static methods. * * By default KRun uses auto deletion. It causes the KRun instance to delete * itself when the it finished its task. If you allocate the KRun * object on the stack you must disable auto deletion, otherwise it will crash. * * This respects the "shell_access", "openwith" and "run_desktop_files" Kiosk * action restrictions (see KAuthorized::authorize()). * * @short Opens files with their associated applications in KDE */ class KIOWIDGETS_EXPORT KRun : public QObject { Q_OBJECT public: /** * @param url the URL of the file or directory to 'run' * * @param window * The top-level widget of the app that invoked this object. * It is used to make sure private information like passwords * are properly handled per application. * * @param showProgressInfo * Whether to show progress information when determining the * type of the file (i.e. when using KIO::stat and KIO::mimetype) * Before you set this to false to avoid a dialog box, think about * a very slow FTP server... * It is always better to provide progress info in such cases. * * @param asn * Application startup notification id, if available (otherwise ""). * * @deprecated since 5.71, use KIO::OpenUrlJob(url) (except for KRun subclasses, for now) */ KRun(const QUrl &url, QWidget *window, bool showProgressInfo = true, const QByteArray &asn = QByteArray()); /** * Destructor. Don't call it yourself, since a KRun object auto-deletes * itself. */ virtual ~KRun(); /** * Abort this KRun. This kills any jobs launched by it, * and leads to deletion if auto-deletion is on. * This is much safer than deleting the KRun (in case it's * currently showing an error dialog box, for instance) */ void abort(); /** * Returns true if the KRun instance has an error. * @return true when an error occurred * @see error() */ bool hasError() const; /** * Returns true if the KRun instance has finished. * @return true if the KRun instance has finished * @see finished() */ bool hasFinished() const; /** * Checks whether auto delete is activated. * Auto-deletion causes the KRun instance to delete itself * when it finished its task. * By default auto deletion is on. * @return true if auto deletion is on, false otherwise */ bool autoDelete() const; /** * Enables or disabled auto deletion. * Auto deletion causes the KRun instance to delete itself * when it finished its task. If you allocate the KRun * object on the stack you must disable auto deletion. * By default auto deletion is on. * @param b true to enable auto deletion, false to disable */ void setAutoDelete(bool b); /** * Set the preferred service for opening this URL, after * its mimetype will have been found by KRun. IMPORTANT: the service is * only used if its configuration says it can handle this mimetype. * This is used for instance for the X-KDE-LastOpenedWith key in * the recent documents list, or for the app selection in * KParts::BrowserOpenOrSaveQuestion. * @param desktopEntryName the desktopEntryName of the service, e.g. "kate". */ void setPreferredService(const QString &desktopEntryName); /** * Sets whether executables, .desktop files or shell scripts should * be run by KRun. This is enabled by default. * @param b whether to run executable files or not. * @see isExecutable() */ void setRunExecutables(bool b); /** * Sets whether KRun should follow URLs redirections. * This is enabled by default * @param b whether to follow redirections or not. * @since 5.55 */ void setFollowRedirections(bool b); /** * 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 the file name to use in the case of downloading the file to a tempfile * in order to give to a non-url-aware application. Some apps rely on the extension * to determine the mimetype of the file. Usually the file name comes from the URL, * but in the case of the HTTP Content-Disposition header, we need to override the * file name. */ void setSuggestedFileName(const QString &fileName); /** * Sets whether a prompt should be shown before executing scripts or desktop files. * If enabled, KRun uses the "kiorc" configuration file to decide whether to open the * file, execute it or show a prompt. * @since 5.4 */ void setShowScriptExecutionPrompt(bool showPrompt); /** * Suggested file name given by the server (e.g. HTTP content-disposition) */ QString suggestedFileName() const; /** * Associated window, as passed to the constructor * @since 4.9.3 */ QWidget *window() const; #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 6) /** * Open a list of URLs with a certain service (application). * * @param service the service to run * @param urls the list of URLs, can be empty (app launched * without argument) * @param window The top-level widget of the app that invoked this object. * @param tempFiles if true and urls are local files, they will be deleted * when the application exits. * @param suggestedFileName see setSuggestedFileName * @param asn Application startup notification id, if any (otherwise ""). * @return @c true on success, @c false on error * * @deprecated since 5.6. Since 5.71 use ApplicationLauncherJob, otherwise runApplication instead. * @code * KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service); * job->setUrls(urls); * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * if (tempFiles) { * job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); * } * job->setSuggestedFileName(suggestedFileName); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 6, "Use KIO::ApplicationLauncherJob, see API docs for a code sample") static bool run(const KService &service, const QList &urls, QWidget *window, bool tempFiles = false, const QString &suggestedFileName = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Open a list of URLs with a certain service (application). * * Prefer runApplication(), unless you need to wait for the application * to register to D-Bus before this method returns (but that should rather * be done with D-Bus activation). * * @param service the service to run * @param urls the list of URLs, can be empty (app launched * without argument) * @param window The top-level widget of the app that invoked this object. * @param tempFiles if true and urls are local files, they will be deleted * when the application exits. * @param suggestedFileName see setSuggestedFileName * @param asn Application startup notification id, if any (otherwise ""). * @return 0 on error, the process ID on success * @since 5.6 * @deprecated since 5.71, use ApplicationLauncherJob instead. * @code * KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service); * job->setUrls(urls); * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * if (tempFiles) { * job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); * } * job->setSuggestedFileName(suggestedFileName); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::ApplicationLauncherJob, see API docs for a code sample") static qint64 runService(const KService &service, const QList &urls, QWidget *window, bool tempFiles = false, const QString &suggestedFileName = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * @see RunFlags */ enum RunFlag { DeleteTemporaryFiles = 0x1, ///< the URLs passed to the service will be deleted when it exits (if the URLs are local files) RunExecutables = 0x2, ///< Whether to run URLs that are executable scripts or binaries @see isExecutableFile() @since 5.31 }; /** * Stores a combination of #RunFlag values. * @deprecated since 5.71, see porting instructions in the respective methods */ Q_DECLARE_FLAGS(RunFlags, RunFlag) #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Run an application (known from its .desktop file, i.e. as a KService) * * If you need to wait for the application to register to D-Bus, use D-Bus activation instead. * * If you don't need the prompt for asking the user whether to add the executable bit for * desktop files or binaries that don't have it, you can use KIO::ApplicationLauncherJob from KIOGui directly. * * @param service the service to run * @param urls the list of URLs, can be empty (app launched * without argument) * @param window The top-level widget of the app that invoked this object. * @param flags various flags * @param suggestedFileName see setSuggestedFileName * @param asn Application startup notification id, if any (otherwise ""). * @return 0 on error, the process ID on success * @since 5.24 * * @deprecated since 5.71, use ApplicationLauncherJob instead. * @code * KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service); * job->setUrls(urls); * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setSuggestedFileName(suggestedFileName); * job->setRunFlags(flags); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::ApplicationLauncherJob, see API docs for a code sample") static qint64 runApplication(const KService &service, const QList &urls, QWidget *window, RunFlags flags = RunFlags(), const QString &suggestedFileName = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Open a list of URLs with an executable. * * @param exec the name of the executable, for example * "/usr/bin/netscape %u". * Don't forget to include the %u if you know that the applications * supports URLs. Otherwise, non-local urls will first be downloaded * to a temp file (using kioexec). * @param urls the list of URLs to open, can be empty (app launched without argument) * @param window The top-level widget of the app that invoked this object. * @param name the logical name of the application, for example * "Netscape 4.06". * @param icon the icon which should be used by the application. * @param asn Application startup notification id, if any (otherwise ""). * @return @c true on success, @c false on error * * @deprecated since 5.71, use KIO::ApplicationLauncherJob with a temporary KService * @code * KService::Ptr service(new KService(name, exec, icon)); * KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service); * job->setUrls(urls); * job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::ApplicationLauncherJob with a temporary KService, see API docs for a code sample") static bool run(const QString &exec, const QList &urls, QWidget *window, const QString &name = QString(), const QString &icon = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 31) /** * Open the given URL. * * This function is used after the mime type * is found out. It will search for all services which can handle * the mime type and call run() afterwards. * @param url the URL to open * @param mimetype the mime type of the resource * @param window The top-level widget of the app that invoked this object. * @param tempFile if true and url is a local file, it will be deleted * when the launched application exits. * @param runExecutables if false then local .desktop files, * executables and shell scripts will not be run. * See also isExecutable(). * @param suggestedFileName see setSuggestedFileName * @param asn Application startup notification id, if any (otherwise ""). * @return @c true on success, @c false on error * @deprecated since 5.31. Since 5.71 use OpenUrlJob, otherwise runUrl() with RunFlags. * @code * KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mimetype); * job->setSuggestedFileName(suggestedFileName); * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setRunExecutables(runExecutables); * job->setDeleteTemporaryFile(...); // depending on the old RunFlags * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 31, "Use KIO::OpenUrlJob, see API docs for a code sample") static bool runUrl(const QUrl &url, const QString &mimetype, QWidget *window, bool tempFile = false, bool runExecutables = true, const QString &suggestedFileName = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Open the given URL. * * This function can be used after the mime type has been found out. * It will search for all services which can handle the mime type and call run() afterwards. * * @param url The URL to open. * @param mimetype The mime type of the resource. * @param window The top-level widget of the app that invoked this object. * @param flags Various run flags. * @param suggestedFileName See setSuggestedFileName() * @param asn Application startup notification id, if any (otherwise ""). * @return @c true on success, @c false on error * @since 5.31 * @deprecated since 5.71, use KIO::OpenUrlJob: * @code * KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mimetype); * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setRunExecutables(runExecutables); * job->setDeleteTemporaryFile(...); // depending on the old RunFlags * job->setSuggestedFileName(suggestedFileName); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::OpenUrlJob, see API docs for a code sample") static bool runUrl(const QUrl &url, const QString &mimetype, QWidget *window, RunFlags flags, const QString &suggestedFileName = QString(), const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Run the given shell command and notifies KDE of the starting * of the application. If the program to be called doesn't exist, * an error box will be displayed. * * Use only when you know the full command line. Otherwise use the other * static methods, or KRun's constructor. * * @param cmd must be a shell command. You must not append "&" * to it, since the function will do that for you. * @param window The top-level widget of the app that invoked this object. * @param workingDirectory directory to use for relative paths, so that * a command like "kwrite file.txt" finds file.txt from the right place * * @return @c true on success, @c false on error * @deprecated since 5.71, use KIO::CommandLauncherJob * @code * KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(cmd); * job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setWorkingDirectory(workingDirectory); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::CommandLauncherJob, see API docs for a code sample") static bool runCommand(const QString &cmd, QWidget *window, const QString &workingDirectory = QString()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Same as the other runCommand(), but it also takes the name of the * binary, to display an error message in case it couldn't find it. * * @param cmd must be a shell command. You must not append "&" * to it, since the function will do that for you. * @param execName the name of the executable * @param icon icon for app starting notification * @param window The top-level widget of the app that invoked this object. * @param asn Application startup notification id, if any (otherwise ""). * @return @c true on success, @c false on error * @deprecated since 5.71, use KIO::CommandLauncherJob * @code * KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(cmd); * job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setWorkingDirectory(workingDirectory); * job->setExecutable(execName); * job->setIcon(icon); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::CommandLauncherJob, see API docs for a code sample") static bool runCommand(const QString &cmd, const QString &execName, const QString &icon, QWidget *window, const QByteArray &asn = QByteArray()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Overload that also takes a working directory, so that a command like * "kwrite file.txt" finds file.txt from the right place. * @param workingDirectory the working directory for the started process. The default * (if passing an empty string) is the user's document path. * @since 4.4 * @deprecated since 5.71, use KIO::CommandLauncherJob instead * @code * KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(cmd); * job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); * job->setWorkingDirectory(workingDirectory); * job->setExecutable(execName); * job->setIcon(icon); * job->setStartupId(asn); * job->start(); * @endcode */ KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::CommandLauncherJob, see API docs for a code sample") static bool runCommand(const QString &cmd, const QString &execName, const QString &icon, QWidget *window, const QByteArray &asn, const QString &workingDirectory); #endif +#if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 71) /** * Display the Open-With dialog for those URLs, and run the chosen application. - * @param lst the list of applications to run + * @param lst the list of URLs to open * @param window The top-level widget of the app that invoked this object. * @param tempFiles if true and lst are local files, they will be deleted * when the application exits. * @param suggestedFileName see setSuggestedFileName * @param asn Application startup notification id, if any (otherwise ""). * @return false if the dialog was canceled + * @deprecated since 5.71, use KIO::ApplicationLauncherJob with no service argument (or a null service) + * @code + * KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(); + * job->setUrls(urls); + * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); + * if (tempFiles) { + * job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); + * } + * job->setSuggestedFileName(suggestedFileName); + * job->setStartupId(asn); + * job->start(); + * @endcode */ + KIOWIDGETS_DEPRECATED_VERSION(5, 71, "Use KIO::OpenUrlJob, see API docs for a code sample") static bool displayOpenWithDialog(const QList &lst, QWidget *window, bool tempFiles = false, const QString &suggestedFileName = QString(), - const QByteArray &asn = QByteArray()); // TODO deprecate and provide RunFlags() overload + const QByteArray &asn = QByteArray()); +#endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 0) /** * Quotes a string for the shell. * An empty string will @em not be quoted. * * @param str the string to quote. The quoted string will be written here * * @deprecated Since 4.0, use KShell::quoteArg() instead. @em Note that this function * behaves differently for empty arguments and returns the result * differently. */ KIOWIDGETS_DEPRECATED_VERSION(4, 0, "Use KShell::quoteArg(...)") static void shellQuote(QString &str); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 0) /** * Processes a Exec= line as found in .desktop files. * @param _service the service to extract information from. * @param _urls The urls the service should open. * @param tempFiles if true and urls are local files, they will be deleted * when the application exits. * @param suggestedFileName see setSuggestedFileName * * @return a list of arguments suitable for QProcess. * @deprecated since 5.0, use KIO::DesktopExecParser */ KIOWIDGETS_DEPRECATED_VERSION(5, 0, "Use KIO::DesktopExecParser") static QStringList processDesktopExec(const KService &_service, const QList &_urls, bool tempFiles = false, const QString &suggestedFileName = QString()); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(5, 0) /** * Given a full command line (e.g. the Exec= line from a .desktop file), * extract the name of the binary being run. * @param execLine the full command line * @param removePath if true, remove a (relative or absolute) path. E.g. /usr/bin/ls becomes ls. * @return the name of the executable to run * @deprecated since 5.0, use KIO::DesktopExecParser::executableName if removePath was true, * or KIO::DesktopExecParser::executablePath if removePath was false. */ KIOWIDGETS_DEPRECATED_VERSION(5, 0, "See API docs") static QString binaryName(const QString &execLine, bool removePath); #endif /** * Returns whether @p mimeType refers to an executable program instead * of a data file. */ static bool isExecutable(const QString &mimeType); /** * Returns whether the @p url of @p mimetype is executable. * To be executable the file must pass the following rules: * -# Must reside on the local filesystem. * -# Must be marked as executable for the user by the filesystem. * -# The mime type must inherit application/x-executable, application/x-executable-script or application/x-sharedlib. * To allow a script to run when the above rules are satisfied add the entry * @code * X-KDE-IsAlso=application/x-executable-script * @endcode * to the mimetype's desktop file. */ static bool isExecutableFile(const QUrl &url, const QString &mimetype); /** * @internal */ static bool checkStartupNotify(const QString &binName, const KService *service, bool *silent_arg, QByteArray *wmclass_arg); Q_SIGNALS: /** * Emitted when the operation finished. * This signal is emitted in all cases of completion, whether successful or with error. * @see hasFinished() */ void finished(); /** * Emitted when the operation had an error. * @see hasError() */ void error(); protected Q_SLOTS: /** * All following protected slots are used by subclasses of KRun! */ /** * This slot is called whenever the internal timer fired, * in order to move on to the next step. */ void slotTimeout(); /** * This slot is called when the scan job is finished. */ void slotScanFinished(KJob *); /** * This slot is called when the scan job has found out * the mime type. */ void slotScanMimeType(KIO::Job *, const QString &type); /** * Call this from subclasses when you have determined the mimetype. * It will call foundMimeType, but also sets up protection against deletion during message boxes. * @since 4.0.2 */ void mimeTypeDetermined(const QString &mimeType); /** * This slot is called when the 'stat' job has finished. */ virtual void slotStatResult(KJob *); protected: /** * All following protected methods are used by subclasses of KRun! */ /** * Initializes the krun object. */ virtual void init(); /** * Start scanning a file. */ virtual void scanFile(); /** * Called if the mimetype has been detected. The function runs * the application associated with this mimetype. * Reimplement this method to implement a different behavior, * like opening the component for displaying the URL embedded. * * Important: call setFinished(true) once you are done! * Usually at the end of the foundMimeType reimplementation, but if the * reimplementation is asynchronous (e.g. uses KIO jobs) then * it can be called later instead. */ virtual void foundMimeType(const QString &type); /** * Kills the file scanning job. */ virtual void killJob(); /** * Called when KRun detects an error during the init phase. * * The default implementation shows a message box. * @since 5.0 */ virtual void handleInitError(int kioErrorCode, const QString &errorMsg); /** * Called when a KIO job started by KRun gives an error. * * The default implementation shows a message box. */ virtual void handleError(KJob *job); /** * Sets the url. */ void setUrl(const QUrl &url); /** * Returns the url. */ QUrl url() const; /** * Sets whether an error has occurred. */ void setError(bool error); /** * Sets whether progress information shall be shown. */ void setProgressInfo(bool progressInfo); /** * Returns whether progress information are shown. */ bool progressInfo() const; /** * Marks this 'KRun' instance as finished. */ void setFinished(bool finished); /** * Sets the job. */ void setJob(KIO::Job *job); /** * Returns the job. */ KIO::Job *job(); #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 4) /** * Returns the timer object. * @deprecated Since 4.4. setFinished(true) now takes care of the timer().start(0), * so this can be removed. */ KIOWIDGETS_DEPRECATED_VERSION(4, 4, "See API docs") QTimer &timer(); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 1) /** * Indicate that the next action is to scan the file. * @deprecated Since 4.1. Not useful in public API */ KIOWIDGETS_DEPRECATED_VERSION(4, 1, "Do not use") void setDoScanFile(bool scanFile); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 1) /** * Returns whether the file shall be scanned. * @deprecated Since 4.1. Not useful in public API */ KIOWIDGETS_DEPRECATED_VERSION(4, 1, "Do not use") bool doScanFile() const; #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 1) /** * Sets whether it is a directory. * @deprecated Since 4.1. Typo in the name, and not useful as a public method */ KIOWIDGETS_DEPRECATED_VERSION(4, 1, "Do not use") void setIsDirecory(bool isDirectory); #endif /** * Returns whether it is a directory. */ bool isDirectory() const; #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 1) /** * @deprecated Since 4.1. Not useful in public API */ KIOWIDGETS_DEPRECATED_VERSION(4, 1, "Do not use") void setInitializeNextAction(bool initialize); #endif #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 1) /** * @deprecated Since 4.1. Not useful in public API */ KIOWIDGETS_DEPRECATED_VERSION(4, 1, "Do not use") bool initializeNextAction() const; #endif /** * Returns whether it is a local file. */ bool isLocalFile() const; private: friend class KRunPrivate; KRunPrivate *const d; }; #endif diff --git a/src/widgets/widgetsopenwithhandler.cpp b/src/widgets/widgetsopenwithhandler.cpp index f4147438..4ca44bbb 100644 --- a/src/widgets/widgetsopenwithhandler.cpp +++ b/src/widgets/widgetsopenwithhandler.cpp @@ -1,55 +1,73 @@ /* 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 "widgetsopenwithhandler.h" +#include #include #include -#include +#include #include +#ifdef Q_OS_WIN +#include "widgetsopenwithhandler_win.cpp" +#endif + KIO::WidgetsOpenWithHandler::WidgetsOpenWithHandler() : KIO::OpenWithHandlerInterface() { } KIO::WidgetsOpenWithHandler::~WidgetsOpenWithHandler() = default; -void KIO::WidgetsOpenWithHandler::promptUserForApplication(KIO::OpenUrlJob *job, const QList &urls, const QString &mimeType) +void KIO::WidgetsOpenWithHandler::promptUserForApplication(KJob *job, const QList &urls, const QString &mimeType) { QWidget *parentWidget = job ? KJobWidgets::window(job) : qApp->activeWindow(); +#ifdef Q_OS_WIN + KConfigGroup cfgGroup(KSharedConfig::openConfig(), QStringLiteral("KOpenWithDialog Settings")); + if (cfgGroup.readEntry("Native", true)) { + // Implemented in applicationlauncherjob_win.cpp + if (displayNativeOpenWithDialog(urls, parentWidget)) { + Q_EMIT handled(); + return; + } else { + // Some error happened with the Windows-specific code. Fallback to the KDE one... + } + } +#endif + KOpenWithDialog *dialog = new KOpenWithDialog(urls, 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/widgetsopenwithhandler.h b/src/widgets/widgetsopenwithhandler.h index 94d97832..e93efc80 100644 --- a/src/widgets/widgetsopenwithhandler.h +++ b/src/widgets/widgetsopenwithhandler.h @@ -1,50 +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 WIDGETSOPENWITHHANDLER_H #define WIDGETSOPENWITHHANDLER_H #include "openwithhandlerinterface.h" class QDialog; class QWidget; namespace KIO { // TODO KF6: Make KIO::JobUiDelegate inherit from WidgetsOpenWithHandler // (or even merge the two classes) // so that setDelegate(new KIO::JobUiDelegate) provides both dialog boxes on error // and the open-with dialog. class WidgetsOpenWithHandler : public OpenWithHandlerInterface { public: WidgetsOpenWithHandler(); ~WidgetsOpenWithHandler() override; - void promptUserForApplication(KIO::OpenUrlJob *job, const QList &urls, const QString &mimeType) override; + void promptUserForApplication(KJob *job, const QList &urls, const QString &mimeType) override; private: // Note: no d pointer because not exported at this point }; } #endif // WIDGETSOPENWITHHANDLER_H diff --git a/src/widgets/widgetsopenwithhandler_win.cpp b/src/widgets/widgetsopenwithhandler_win.cpp new file mode 100644 index 00000000..2364614e --- /dev/null +++ b/src/widgets/widgetsopenwithhandler_win.cpp @@ -0,0 +1,96 @@ +/* This file is part of the KDE libraries + Copyright (C) 2008 Jarosław Staniek + + 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 +#include + +#include + +// TODO move to a shared lib +static int runDll(WId windowId, const QString &libraryName, const QByteArray &functionName, + const QString &arguments) +{ + HMODULE libHandle = LoadLibraryW((LPCWSTR)libraryName.utf16()); + if (!libHandle) { + return 0; + } + typedef int (WINAPI * FunctionType)(HWND, HMODULE, LPCWSTR, int); +#ifdef _WIN32_WCE + QString functionNamestr = QString(functionName); + FunctionType function + = (FunctionType)GetProcAddressW(libHandle, functionNamestr.utf16()); +#else + FunctionType function + = (FunctionType)GetProcAddress(libHandle, functionName.constData()); +#endif + if (!function) { + return 0; + } + int result = function((HWND)windowId, libHandle, (LPCWSTR)arguments.utf16(), SW_SHOW); + FreeLibrary(libHandle); + return result; +} + +static int runDll(WId windowId, const QString &libraryName, const QByteArray &functionName, + const QByteArray &arguments) +{ + HMODULE libHandle = LoadLibraryW((LPCWSTR)libraryName.utf16()); + if (!libHandle) { + return 0; + } + typedef int (WINAPI * FunctionType)(HWND, HMODULE, LPCSTR, int); +#ifdef _WIN32_WCE + QString functionNamestr = QString(functionName); + FunctionType function + = (FunctionType)GetProcAddressW(libHandle, functionNamestr.utf16()); +#else + FunctionType function + = (FunctionType)GetProcAddress(libHandle, functionName.constData()); +#endif + if (!function) { + return 0; + } + int result = function((HWND)windowId, libHandle, (LPCSTR)arguments.constData(), SW_SHOW); + FreeLibrary(libHandle); + return result; +} + +// TODO move to a shared lib +static int runDll(QWidget *parent, const QString &libraryName, const QByteArray &functionName, + const QString &arguments) +{ + return runDll(parent ? parent->winId() : 0, libraryName, functionName, arguments); +} + +// Windows implementation using "OpenAs_RunDLL" entry +static bool displayNativeOpenWithDialog(const QList &lst, QWidget *window) +{ + Q_UNUSED(tempFiles); + Q_UNUSED(suggestedFileName); + + QStringList fnames; + for (const QUrl &url : lst) { + fnames += url.isLocalFile() ? QDir::toNativeSeparators(url.toLocalFile()) : url.toString(); + } + int result = runDll(window, + QLatin1String("shell32.dll"), + "OpenAs_RunDLLW", + fnames.join(QLatin1Char(' '))); + return result == 0; +}